2012-08-24 12 views
11

Necesito hacer versiones en gráficos de objetos Java (simples) almacenados en una base de datos orientada a documentos (MongoDB). Para bases de datos relacionales e Hibernate, descubrí Envers y estoy muy sorprendido sobre las posibilidades. ¿Hay algo similar que pueda usarse con Spring Data Documents?Java MongoDB Object Versioning

Encontré this post delineando las ideas que tenía (y más ...) sobre el almacenamiento de las versiones de los objetos, y mi implementación actual funciona de manera similar ya que almacena copias de los objetos en una colección de historia separada con una marca de tiempo, pero Me gustaría mejorar esto para ahorrar espacio de almacenamiento. Por lo tanto, creo que necesito implementar una operación "diff" en árboles de objetos y una operación "fusionar" para reconstruir objetos viejos. ¿Hay bibliotecas por ahí ayudando con esto?

Editar: ¡Todas las experiencias con MongoDB y versiones muy apreciadas! Veo muy probablemente que no habrá una solución de Spring Data.

+0

No llena de versiones, pero hemos implementado un pequeño sistema de auditoría - tala quién cambió qué valores antiguos a los nuevos. Estamos utilizando el método '' prePersist() '' de Morphia (que solo funcionará para guardar entidades completas, no actualizaciones específicas). Puede proporcionar algunas muestras de código, pero no es nada sofisticado ... – xeraa

+0

¡Gracias por tu comentario! Me interesaría mucho más detalles para demostrar tu solución. Solo el seguimiento completo de las salvaciones de entidades está definitivamente bien: este es nuestro caso de uso principal, también. Un punto muy interesante es la forma en que se compara la antigua con la nueva entidad, identificando las propiedades modificadas. Eché un vistazo a los marcos de comparación de gráficos aquí, pero no encontré una solución rápida y fácil. –

Respuesta

7

Estamos utilizando una entidad base (donde establecemos el Id, la creación + las últimas fechas de cambio, ...). Basándose en esto estamos utilizando un método de persistencia genérica, que es como la siguiente:

@Override 
public <E extends BaseEntity> ObjectId persist(E entity) { 
    delta(entity); 
    mongoDataStore.save(entity); 
    return entity.getId(); 
} 

El método delta se parece a esto (Voy a tratar de hacer esto lo más genérico posible):

protected <E extends BaseEntity> void delta(E newEntity) { 

    // If the entity is null or has no ID, it hasn't been persisted before, 
    // so there's no delta to calculate 
    if ((newEntity == null) || (newEntity.getId() == null)) { 
     return; 
    } 

    // Get the original entity 
    @SuppressWarnings("unchecked") 
    E oldEntity = (E) mongoDataStore.get(newEntity.getClass(), newEntity.getId()); 

    // Ensure that the old entity isn't null 
    if (oldEntity == null) { 
     LOG.error("Tried to compare and persist null objects - this is not allowed"); 
     return; 
    } 

    // Get the current user and ensure it is not null 
    String email = ...; 

    // Calculate the difference 
    // We need to fetch the fields from the parent entity as well as they 
    // are not automatically fetched 
    Field[] fields = ArrayUtils.addAll(newEntity.getClass().getDeclaredFields(), 
      BaseEntity.class.getDeclaredFields()); 
    Object oldField = null; 
    Object newField = null; 
    StringBuilder delta = new StringBuilder(); 
    for (Field field : fields) { 
     field.setAccessible(true); // We need to access private fields 
     try { 
      oldField = field.get(oldEntity); 
      newField = field.get(newEntity); 
     } catch (IllegalArgumentException e) { 
      LOG.error("Bad argument given"); 
      e.printStackTrace(); 
     } catch (IllegalAccessException e) { 
      LOG.error("Could not access the argument"); 
      e.printStackTrace(); 
     } 
     if ((oldField != newField) 
       && (((oldField != null) && !oldField.equals(newField)) || ((newField != null) && !newField 
         .equals(oldField)))) { 
      delta.append(field.getName()).append(": [").append(oldField).append("] -> [") 
        .append(newField).append("] "); 
     } 
    } 

    // Persist the difference 
    if (delta.length() == 0) { 
     LOG.warn("The delta is empty - this should not happen"); 
    } else { 
     DeltaEntity deltaEntity = new DeltaEntity(oldEntity.getClass().toString(), 
       oldEntity.getId(), oldEntity.getUuid(), email, delta.toString()); 
     mongoDataStore.save(deltaEntity); 
    } 
    return; 
} 

Nuestra entidad delta parece que (sin la getters + set, toString, hashCode, y es igual a):

@Entity(value = "delta", noClassnameStored = true) 
public final class DeltaEntity extends BaseEntity { 
    private static final long serialVersionUID = -2770175650780701908L; 

    private String entityClass; // Do not call this className as Morphia will 
          // try to work some magic on this automatically 
    private ObjectId entityId; 
    private String entityUuid; 
    private String userEmail; 
    private String delta; 

    public DeltaEntity() { 
     super(); 
    } 

    public DeltaEntity(final String entityClass, final ObjectId entityId, final String entityUuid, 
      final String userEmail, final String delta) { 
     this(); 
     this.entityClass = entityClass; 
     this.entityId = entityId; 
     this.entityUuid = entityUuid; 
     this.userEmail = userEmail; 
     this.delta = delta; 
    } 

la esperanza que esto le ayuda para empezar :-)

+0

Muchas gracias por la muestra. También encontré una publicación sobre diffs de objeto java (http://stackoverflow.com/questions/8001400/is-there-a-java-library-that-can-diff-two-objects) mencionando esta biblioteca: https: // github.com/SQiShER/java-object-diff - tal vez pueda "darle más sabor" a su solución con este algoritmo diff. Me gustaría dejar esta pregunta abierta por un tiempo más, tal vez haya otras ideas. –

+0

Interesante proyecto, esperando su solución. Un voto positivo aún sería apreciado mientras tanto ;-) – xeraa

12

Así es como terminé implementando versiones para entidades MongoDB. ¡Gracias a la comunidad de StackOverflow por ayudarme!

  • Se guarda un registro de cambios para cada entidad en una colección de historial separada.
  • Para evitar guardar una gran cantidad de datos, la colección de historial no almacena instancias completas, sino solo la primera versión y las diferencias entre las versiones. (Incluso podría omitir la primera versión y reconstruir las versiones "al revés" de la versión actual en la colección principal de la entidad.)
  • Java Object Diff se utiliza para generar objetos diffs.
  • Para poder trabajar con colecciones correctamente, se necesita implementar el método equals de las entidades para que pruebe la clave principal de la base de datos y no las subpropiedades. (De lo contrario, JavaObjectDiff no reconocerá los cambios de propiedad en los elementos de colección.)

Estas son las entidades que uso para el control de versiones (getters/setters, etc.eliminado):

// This entity is stored once (1:1) per entity that is to be versioned 
// in an own collection 
public class MongoDiffHistoryEntry { 
    /* history id */ 
    private String id; 

    /* reference to original entity */ 
    private String objectId; 

    /* copy of original entity (first version) */ 
    private Object originalObject; 

    /* differences collection */ 
    private List<MongoDiffHistoryChange> differences; 

    /* delete flag */ 
    private boolean deleted; 
} 

// changeset for a single version 
public class MongoDiffHistoryChange { 
    private Date historyDate; 
    private List<MongoDiffHistoryChangeItem> items; 
} 

// a single property change 
public class MongoDiffHistoryChangeItem { 
    /* path to changed property (PropertyPath) */ 
    private String path; 

    /* change state (NEW, CHANGED, REMOVED etc.) */ 
    private Node.State state; 

    /* original value (empty for NEW) */ 
    private Object base; 

    /* new value (empty for REMOVED) */ 
    private Object modified; 
} 

Aquí es la operación saveChangeHistory:

private void saveChangeHistory(Object working, Object base) { 
    assert working != null && base != null; 
    assert working.getClass().equals(base.getClass()); 

    String baseId = ObjectUtil.getPrimaryKeyValue(base).toString(); 
    String workingId = ObjectUtil.getPrimaryKeyValue(working).toString(); 
    assert baseId != null && workingId != null && baseId.equals(workingId); 

    MongoDiffHistoryEntry entry = getObjectHistory(base.getClass(), baseId); 
    if (entry == null) { 
     //throw new RuntimeException("history not found: " + base.getClass().getName() + "#" + baseId); 
     logger.warn("history lost - create new base history record: {}#{}", base.getClass().getName(), baseId); 
     saveNewHistory(base); 
     saveHistory(working, base); 
     return; 
    } 

    final MongoDiffHistoryChange change = new MongoDiffHistoryChange(); 
    change.setHistoryDate(new Date()); 
    change.setItems(new ArrayList<MongoDiffHistoryChangeItem>()); 

    ObjectDiffer differ = ObjectDifferFactory.getInstance(); 
    Node root = differ.compare(working, base); 
    root.visit(new MongoDiffHistoryChangeVisitor(change, working, base)); 

    if (entry.getDifferences() == null) 
     entry.setDifferences(new ArrayList<MongoDiffHistoryChange>()); 
    entry.getDifferences().add(change); 

    mongoTemplate.save(entry, getHistoryCollectionName(working.getClass())); 
} 

Ésta es la forma en que se ve como en MongoDB:

{ 
    "_id" : ObjectId("5040a9e73c75ad7e3590e538"), 
    "_class" : "MongoDiffHistoryEntry", 
    "objectId" : "5034c7a83c75c52dddcbd554", 
    "originalObject" : { 
     BLABLABLA, including sections collection etc. 
    }, 
    "differences" : [{ 
     "historyDate" : ISODate("2012-08-31T12:11:19.667Z"), 
     "items" : [{ 
      "path" : "/sections[[email protected]]", 
      "state" : "ADDED", 
      "modified" : { 
      "_class" : "LetterSection", 
      "_id" : ObjectId("5034c7a83c75c52dddcbd556"), 
      "letterId" : "5034c7a83c75c52dddcbd554", 
      "sectionIndex" : 2, 
      "stringContent" : "BLABLA", 
      "contentMimetype" : "text/plain", 
      "sectionConfiguration" : "BLUBB" 
      } 
     }, { 
      "path" : "/sections[[email protected]]", 
      "state" : "REMOVED", 
      "base" : { 
      "_class" : "LetterSection", 
      "_id" : ObjectId("5034c7a83c75c52dddcbd556"), 
      "letterId" : "5034c7a83c75c52dddcbd554", 
      "sectionIndex" : 2, 
      "stringContent" : "BLABLABLA", 
      "contentMimetype" : "text/plain", 
      "sectionConfiguration" : "BLUBB" 
      } 
     }] 
    }, { 
     "historyDate" : ISODate("2012-08-31T13:15:32.574Z"), 
     "items" : [{ 
      "path" : "/sections[[email protected]]/stringContent", 
      "state" : "CHANGED", 
      "base" : "blub5", 
      "modified" : "blub6" 
     }] 
    }, 
    }], 
    "deleted" : false 
} 

EDIT: Aquí está el código de Visitantes:

public class MongoDiffHistoryChangeVisitor implements Visitor { 

private MongoDiffHistoryChange change; 
private Object working; 
private Object base; 

public MongoDiffHistoryChangeVisitor(MongoDiffHistoryChange change, Object working, Object base) { 
    this.change = change; 
    this.working = working; 
    this.base = base; 
} 

public void accept(Node node, Visit visit) { 
    if (node.isRootNode() && !node.hasChanges() || 
     node.hasChanges() && node.getChildren().isEmpty()) { 
     MongoDiffHistoryChangeItem diffItem = new MongoDiffHistoryChangeItem(); 
     diffItem.setPath(node.getPropertyPath().toString()); 
     diffItem.setState(node.getState()); 

     if (node.getState() != State.UNTOUCHED) { 
      diffItem.setBase(node.canonicalGet(base)); 
      diffItem.setModified(node.canonicalGet(working)); 
     } 

     if (change.getItems() == null) 
      change.setItems(new ArrayList<MongoDiffHistoryChangeItem>()); 
     change.getItems().add(diffItem); 
    } 
} 

}