2012-08-24 16 views
11

Ho bisogno di fare il versioning su (semplici) grafici di oggetti Java memorizzati in un database orientato ai documenti (MongoDB). Per i database relazionali e Hibernate, ho scoperto Envers e sono molto sorpreso delle possibilità. C'è qualcosa di simile che può essere usato con i documenti Spring Data?Versioning Java MongoDB

Ho trovato this post delineando i pensieri che avevo (e altro ...) sull'archiviazione delle versioni dell'oggetto e la mia implementazione corrente funziona in modo simile in quanto memorizza le copie degli oggetti in una raccolta cronologica separata con un timestamp, ma io vorrebbe migliorare questo per risparmiare spazio di archiviazione. Pertanto, penso di aver bisogno di implementare sia un'operazione "diff" sugli alberi degli oggetti sia un'operazione di "fusione" per la ricostruzione di vecchi oggetti. Ci sono delle librerie là fuori che aiutano con questo?

Modifica: Qualsiasi esperienza con MongoDB e versioni molto apprezzate! Probabilmente vedo che non ci sarà una soluzione Spring Data.

+0

non pieno controllo delle versioni, ma abbiamo implementato un piccolo sistema di controllo - che ha cambiato la registrazione che vecchi valori a quelli nuovi. Stiamo usando il metodo '' prePersist() '' di Morphia (che funzionerà solo per i salvataggi di entità completa, non per gli aggiornamenti specifici). Può fornire alcuni esempi di codice, ma non è niente di sofisticato ... – xeraa

+0

Grazie per il tuo commento! Sarei molto interessato ad ulteriori dettagli che dimostrino la tua soluzione. Solo il monitoraggio dei salvataggi completi è ok: questo è anche il nostro caso d'uso principale. Un punto molto interessante è il modo in cui si confronta la vecchia con la nuova entità, identificando le proprietà modificate. Ho dato un'occhiata ai quadri di comparazione dei grafici qui, ma non ho trovato una soluzione facile e veloce. –

risposta

7

Stiamo utilizzando un'entità di base (dove impostiamo l'ID, la creazione + le ultime date di modifica, ...). Basandosi su questo stiamo utilizzando un metodo di persistenza generica, che sembra qualcosa di simile:

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

il metodo Delta si presenta così (Cercherò di rendere questo più generico possibile):

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; 
} 

nostra entità delta sembra che (senza Getters + setter, toString, hashCode, e uguale):

@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; 
    } 

Spero che questo ti aiuta per iniziare :-)

+0

Grazie mille per il campione. Ho anche trovato un post su java object diffs (http://stackoverflow.com/questions/8001400/is-there-a-java-library-that-can-diff-two-objects) che menziona questa libreria: https: // github.com/SQiShER/java-object-diff - forse posso "ravvivare" la tua soluzione con questo algoritmo di diff. Vorrei lasciare questa domanda aperta ancora per qualche tempo, forse ci sono altre idee. –

+0

Progetto interessante, in attesa di una soluzione. Un introito sarebbe comunque apprezzato nel frattempo ;-) – xeraa

12

Questo è il modo in cui ho finito per implementare il controllo delle versioni per le entità MongoDB. Grazie alla community di StackOverflow per l'aiuto!

  • Un registro delle modifiche viene mantenuto per ciascuna entità in una raccolta cronologica separata.
  • Per evitare il salvataggio di molti dati, la raccolta della cronologia non memorizza istanze complete, ma solo la prima versione e le differenze tra le versioni. (Si potrebbe anche omettere la prima versione e ricostruire le versioni "all'indietro" dalla versione corrente nella raccolta principale dell'entità.)
  • Java Object Diff viene utilizzato per generare le differenze di oggetto.
  • Per poter lavorare correttamente con le raccolte, è necessario implementare il metodo equals delle entità in modo che esegua il test per la chiave primaria del database e non per le proprietà secondarie. (Altrimenti, JavaObjectDiff non riconoscerà le modifiche alle proprietà negli elementi della raccolta.)

Ecco le entità che utilizzo per il controllo delle versioni (getter/setter, ecc.rimosso):

// 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; 
} 

Ecco l'operazione 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())); 
} 

Questo è come sembra in 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: Ecco il codice visitatore:

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); 
    } 
} 

}