Alters

Hacer DELTA con java

Buenas!

¡Dos entradas en dos días! ¿Qué puedo decir?...

Creo que este fin de semana estoy inspirado, y eso es bueno :D
En fin, vamos a pasar a otro temilla rápido que nos ocupará una nueva entrada en este blog... y es el trabajar con DELTA en java.

¡Vamos allá!




Para empezar... ¿qué es un DELTA?

Concepto de DELTA


Pues no me refiero a un ala delta (ba dum tss), si no a un método de comparación de datos que se basa en encontrar los cambios entre dos versiones del mismo conjunto de datos.

¿Nani?

Con un ejemplo se ve mejor (aunque ya sabéis que mis ejemplos suelen ser malísimos, pero me esfuerzo).

Supongamos durante lo que dura esta entrada, que somos los gerentes de una empresa, y queremos ver los cambios que se generan en nuestra plantilla mes a mes (nuestra empresa es pequeña y no tiene sentido mirar los cambios a diario).

Podríamos tener, por ejemplo, la siguiente plantilla

Mes de Enero:


  • Empleado 1
    • Nombre: David
    • Puesto: Gerente
    • Horas semanales: 100
  • Empleado 2
    • Nombre: Oscar
    • Puesto: Encargado de la limpieza
    • Horas semanales: 40
  • Empleado 3
    • Nombre: Solé
    • Puesto: Programador Junior
    • Horas semanales: 20
  • Empleado 5
    • Nombre: González
    • Puesto Programador Senior
    • Horas semanales: 50


Mes de Febrero:


  • Empleado 1
    • Nombre: David
    • Puesto: Gerente
    • Horas semanales: 90
  • Empleado 2
    • Nombre: Oscar
    • Puesto: Encargado de la limpieza
    • Horas semanales: 40
  • Empleado 5
    • Nombre: González
    • Puesto: Analista
    • Horas semanales: 50
  • Empleado 4
    • Nombre: Solé Jr.
    • Puesto: Programador Junior
    • Horas Semanales: 20

Bien, de aquí vamos a sacar unos cuantos conceptos...

  1. Todos los empleados de la plantilla se consideran un conjunto de datos
  2. El conjunto de datos de enero lo llamaremos "antiguo"
  3. El conjunto de datos de febrero lo llamaremos "actual"

Si comparamos "antiguo" y "actual", observamos

  1. David ha reducido su horario (aunque no vamos a tenerlo en cuenta para el DELTA)
  2. González ha cambiado de puesto
  3. Solé ha dejado la empresa
  4. Solé Jr. se ha incorporado a la empresa

Estos cuatro cambios, sería el resultado de aplicar un DELTA a "actual" sobre "antiguo".


Realizando un DELTA en Java

Bien, si aplicamos todo esto a Java, es relativamente sencillo tener un mapa mental claro... cada empleado sería una instancia de una nueva clase Empleado, y simplemente bastaría con hacer un par de operaciones con los conjuntos de datos para obtener los objetos que se han mantenido, y los que han cambiado (y dentro de estos últimos, cuales se han añadido - insert -, cuales han cambiado - update - y cuales se han eliminado - delete -).


Separando los datos comunes

Esta parte es sencilla. Básicamente tomamos "antiguo", y lo recorremos de arriba a abajo. Por cada elemento de "antiguo", miramos si existe en "actual".

Si existe, es un dato común; si no, es un dato no común, y lo almacenamos a parte.

Para los datos no comunes, ya tenemos una parte hecha en el funcionamiento descrito en los párrafos anteriores; lo que nos queda por hacer ahora es hacer la inversa: recorrer "actual" y comparar con "actual", pero solo quedándonos con los datos no comunes (los comunes ya los tenemos localizados).

En este punto, la comparación la debemos hacer de un modo completo... es decir, buscamos cualquier tipo de cambio relevante (cambios en el número de empleado, en el nombre, o en el puesto, pero no en las horas).

Con esto, deberíamos tener tres subconjuntos de datos

  1. Datos que solo existen en "antiguo" (alpha)
  2. Datos que solo existen en "nuevo" (beta)
  3. Datos comunes

Tipología del cambio


Ahora, haremos una comparación similar a la anterior, pero con "alpha" y "beta", y esta vez solo compararemos los datos clave (número de empleado).

Tras esta comparativa, tendremos otros tres subconjuntos de datos

  1. Datos que solo existen en "alpha" (no existen en "beta", por tanto, se han borrado)
  2. Datos que solo existen en "beta" (no existen en "alpha", por tanto, se han añadido)
  3. Datos que existen en "alpha" y "beta" (existen en ambos, por tanto, se han modificado).

Todo este procedimiento lo podemos encapsular en una clase, pero previamente hay que establecer unos requerimientos.


Clase base


Necesitamos garantizar que la clase sobre la que hagamos el DELTA tenga un método que compare un objeto dado con la propia instancia, y que además, podamos indicar si queremos hacer la comparación de todos los datos, o únicamente de los datos clave.

Para ello, crearemos una clase abstracta llamada "Historify", en la que fijaremos nuestros requerimientos.


Get Raw
0001/**
0002 * Historify Class.
0003 * 
0004 * Base Class to be used on Delta Class. It provides the basic methods to be
0005 * used on Delta.
0006 * 
0007 * @author DoHITB
0008 *
0009 */
0010public abstract class Historify{
0011 /**
0012  * Object Id. It identifies each class instance
0013  */
0014 private Object id;
0015 
0016 /**
0017  * Class Constructor.
0018  * 
0019  * It checks that "id" cannot be null.
0020  * @param id: instance ID
0021  * @throws NullIdException if id attribute is null
0022  */
0023 public Historify(Object id) throws NullIdException{
0024  if(id == null)
0025   throw new NullIdException("Invalid ID provided");
0026  
0027  this.id = id;
0028 }
0029 
0030 /**
0031  * Equals method.
0032  * 
0033  * It compares current instance against given object. Both objects must
0034  * have same class to be compared.
0035  * 
0036  * @param o: object to compare against
0037  * @param strict: if true, full comparation will be done (hardEquals)
0038  * @return true if equals; false otherwise
0039  * @throws ClassDifferException if o class differs from current class.
0040  */
0041 public final boolean equals(Historify o, boolean strict) 
0042                                    throws ClassDifferException {
0043  if(!o.getClass().equals(this.getClass()))
0044   throw new ClassDifferException(this, o);
0045  
0046  if(strict)
0047   return this.hardEquals(o);
0048  
0049  return this.id.equals(o.getId());
0050 }
0051 
0052 /**
0053  * HardEquals method.
0054  * 
0055  * It makes a full comparation of all attributes on the class.
0056  * @param o: object to compare against
0057  * @return true if equals; false otherwise
0058  */
0059 public abstract boolean hardEquals(Historify o);
0060 
0061 /**
0062  * GetId method.
0063  * 
0064  * Getter for "id" attribute
0065  * @return id
0066  */
0067 public Object getId() {
0068  return this.id;
0069 }
0070}

Con esta clase, podemos asegurar que todos las clases que hereden "Historify" tendrán los mecanismos que necesitamos.

Ahora, vamos a pasar a implementar el DELTA.


Clase DELTA

Para esta clase, vamos a usar lo que se conoce como tipo genérico. Es decir, sabemos que vamos a estar trabajando con una subclase de "Historify" (ya que esta clase es abstracta y no se puede instanciar), así que no podemos usar una clase en concreto... esto se soluciona, efectivamente, con un tipo genérico.


Get Raw
0001import java.util.ArrayList;
0003/**
0004 * Delta Class
0005 * 
0006 * Helper class that is suitable to do delta comparation between two data array
0007 * 
0008 * The data array must have same Class, either it would not be able to compare
0009 * Also, the Class must extend the Historify Class, as it have comparation
0010 * methods declared that are being used on this class:
0011 * 
0012 *  - equals: this is a final boolean method, so there is no need to implement
0013 *      it on subclasses. It does a compare, but only having in mind the
0014 *      ID's.
0015 *  
0016 *  - hardEquals: this method must be implemented on each subclass. It may
0017 *       do a compare of all attributes in class.
0018 *  
0019 * @author DoHITB
0020 *
0021 * @param : The Class is being used.
0022 */
0023public class Delta <T extends historify>{
0024 private T[] old;
0025 private T[] cur;
0026 
0027 private ArrayList<T> ins;
0028 private ArrayList<T> upd;
0029 private ArrayList<T> del;
0030 private ArrayList<T> alpha;
0031 private ArrayList<T> aandb;
0032 private ArrayList<T> beta;
0033 
0034 /**
0035  * Class constructor
0036  * 
0037  * @param old: an array of <T> Class having the previous data
0038  * @param cur: an array of <T> Class having the current data
0039  */
0040 public Delta(T[] old, T[] cur) {
0041  this.old = old;
0042  this.cur = cur;
0043 }
0044 
0045 /**
0046  * Main method for Delta Class.
0047  * 
0048  * It distributes data, then do the Delta.
0049  * After this execution, "ins", "upd", and "del" will be fulfilled.
0050  * 
0051  * @throws ClassDifferException if "old" and "cur" have different class.
0052  */
0053 public void check() throws ClassDifferException{
0054  //restart all the data
0055  init();
0056  
0057  //first, distribute all the data.
0058  this.distribute();
0059  
0060  //then, do the delta
0061  this.doDelta();
0062 }
0063 
0064 /**
0065  * Distribute method.
0066  * 
0067  * Based on "old" and "cur", it distributes which data are on both sides,
0068  * and which one is only on one of the data arrays
0069  * 
0070  * @throws ClassDifferException if "old" and "cur" have different class.
0071  */
0072 private void distribute() throws ClassDifferException{
0073  //first, check old vs cur
0074  this.compare(this.old, this.cur, truethis.aandb, this.alpha, true);
0075 
0076  //then, check for cur vs old
0077  this.compare(this.cur, this.old, falsenullthis.beta, true);
0078 }
0079 
0080 @SuppressWarnings("unchecked")
0081 /**
0082  * doDelta method.
0083  * 
0084  * Based on the results of "distribute", it distributes which data are on 
0085  * both sides, and which one is only on one of the data arrays
0086  * 
0087  * @throws ClassDifferException if "old" and "cur" have different class.
0088  */
0089 private void doDelta() throws ClassDifferException{
0090  T[] alphaA = (T[]) new Historify[this.alpha.size()];
0091  T[] betaA  = (T[]) new Historify[this.beta.size()];
0092  
0093  alphaA = this.alpha.toArray(alphaA);
0094  betaA  = this.beta.toArray(betaA);
0095  
0096  //finally, check alpha vs beta, and beta vs alpha
0097  this.compare(alphaA, betaA, truethis.upd, this.del, false);
0098  
0099  this.compare(betaA, alphaA, falsenullthis.ins, false);
0100 }
0101 
0102 /**
0103  * Compare method.
0104  * 
0105  * It checks if given array exists on the given array. If "storeEquals" is
0106  * true, it will store all data existing on the given array. If "strict" is
0107  * true, it will do a full comparation of the object against the data array
0108  * 
0109  * @param a: array to be checked object by object
0110  * @param b: array to be checked against
0111  * @param storeEquals: if true, positive results will be stored
0112  * @param equal: ArrayList on which positive results will be stored
0113  * @param differ: ArrayList on which negative results will be stored
0114  * @param strict: if true, a full comparation will be done.
0115  * @throws ClassDifferException if "old" and "cur" have different class.
0116  */
0117 private void compare(T[] a, T[] b, boolean storeEquals, 
0118        ArrayList<T> equal, ArrayList<T> differ,
0119        boolean strict) throws ClassDifferException{
0120  for(T obj : a) 
0121   if(this.equals(obj, b, strict)) 
0122    //the Object exist both in old and cur
0123    this.storeEquals(storeEquals, equal, obj);
0124   else 
0125    //the Object only exist in old
0126    differ.add(obj);
0127 }
0128 
0129 /**
0130  * Equals method.
0131  * 
0132  * It checks if given object exists on the given array. If "strict" is true
0133  * it will do a full compare; else it will only compare both ID's.
0134  * 
0135  * @param o: object to be checked against array
0136  * @param a: array to be checked against
0137  * @param strict: if true, it will do a full comparation
0138  * @return true if found, else otherwise
0139  * @throws ClassDifferException if "old" and "cur" have different class.
0140  */
0141 private boolean equals(T o, T[] a, boolean strict) 
0142                               throws ClassDifferException{
0143  for(T obj : a)
0144   if(obj.equals(o, strict))
0145    return true;
0146 
0147  return false;
0148 }
0149 
0150 /**
0151  * StoreEquals method.
0152  * 
0153  * It checks if positive results may be stored, then store if necessary
0154  * 
0155  * @param done: if true, results will be stored
0156  * @param target: ArrayList to be increased
0157  * @param item: Object to be stored
0158  */
0159 private void storeEquals(boolean done, ArrayList<T> target, T item) {
0160  if(done)
0161   target.add(item);
0162 }
0163 
0164 /**
0165  * Init method.
0166  * 
0167  * It initializes all the ArrayLists
0168  */
0169 private void init() {
0170  this.aandb = new ArrayList<T>();
0171  this.alpha = new ArrayList<T>();
0172  this.beta  = new ArrayList<T>();
0173  this.del   = new ArrayList<T>();
0174  this.ins   = new ArrayList<T>();
0175  this.upd   = new ArrayList<T>();
0176 }
0177 
0178 /**
0179  * getInserted method.
0180  * 
0181  * Getter method ins attribute
0182  * @return ArrayList<T> ins
0183  */
0184 public ArrayList<T> getInserted(){
0185  return this.ins;
0186 }
0187 
0188 /**
0189  * getUpdated method.
0190  * 
0191  * Getter method upd attribute
0192  * @return ArrayList<T> upd
0193  */
0194 public ArrayList<T> getUpdated(){
0195  return this.upd;
0196 }
0197 
0198 /**
0199  * getDeleted method.
0200  * 
0201  * Getter method del attribute
0202  * @return ArrayList<T> del
0203  */
0204 public ArrayList<T> getDeleted(){
0205  return this.del;
0206 }
0207}

En este caso, el tipo genérico es <T extends Historify>, que nos permite referirnos a "T" dentro del código como un tipo definido de clase, asegurando que será una subclase de "Historify".

Y con esto, tendríamos una implementación de un sistema DELTA en java!

Espero que os sea útil, o que al menos os sirva para ver nuevos conceptos!! :D

Os dejo el enlace al proyecto en GitHub, por si queréis verlo mejor.


Hasta la próxima!

No hay comentarios:

Publicar un comentario