2012-08-24 13 views
11

Potrzebuję przeprowadzić wersjonowanie na (prostych) wykresach obiektów Java przechowywanych w zorientowanej na dokumenty bazie danych (MongoDB). W przypadku relacyjnych baz danych i Hibernate odkryłem Envers i jestem bardzo zaskoczony możliwościami. Czy jest coś podobnego, co można wykorzystać w Spring Data Documents?Java MongoDB Object Versioning

Znalazłem this post przedstawiający myśli, które miałem (i więcej ...) na temat przechowywania wersji obiektów, a moja obecna implementacja działa podobnie, ponieważ przechowuje kopie obiektów w osobnej kolekcji historii z sygnaturą czasową, ale ja chciałby to poprawić, aby zaoszczędzić miejsce. Dlatego myślę, że muszę zaimplementować zarówno operację "diff" na drzewach obiektów oraz operację "scalania" dla rekonstrukcji starych obiektów. Czy są tam jakieś biblioteki, które pomagają w tym?

Edit: Wszelkie doświadczenia z MongoDB i wersjonowania bardzo mile widziane! Najprawdopodobniej nie będzie rozwiązania Spring Data.

+0

Nie pełnym wersjami, ale zostały wdrożone maleńką systemu kontroli - logowanie który zmienił którym stare wartości do nowych. Używamy metody 'prePersist()' firmy Morphia (która będzie działać tylko dla zapisów pełnej jednostki, a nie dla konkretnych aktualizacji). Może dostarczyć kilka próbek kodu, ale to nic wyszukanego ... – xeraa

+0

Dzięki za komentarz! Byłbym bardzo zainteresowany szczegółami, demonstrującymi twoje rozwiązanie. Tylko śledzenie zapisów pełnego obiektu jest definitywnie w porządku: to również jest nasz główny przypadek użycia. Bardzo interesującym punktem jest sposób porównywania starego z nowym obiektem, identyfikujący zmienione właściwości. Spojrzałem tutaj na schematy porównania wykresów, ale nie znalazłem szybkiego i łatwego rozwiązania. –

Odpowiedz

7

Używamy elementu podstawowego (gdzie ustawiamy identyfikator, datę utworzenia + ostatniej zmiany, ...). Opierając się na tym używamy metoda rodzajowa trwałości, który wygląda mniej więcej tak:

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

Metoda delta wygląda następująco (postaram się zrobić to jako rodzajowy, jak to możliwe):

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

Nasza jednostka delta wygląda, że ​​(bez pobierające + ustawiaczy, toString, hashcode, i równa):

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

Nadzieja to pomaga podręczny :-)

+0

Dziękuję bardzo za próbkę. Znalazłem również post o jav obiektowych diffach (http://stackoverflow.com/questions/8001400/is-there-a-java-library-that-can-diff-two-objects), które wspominają tę bibliotekę: https: // github.com/SQiShER/java-object-diff - może uda mi się "urozmaicić" twoje rozwiązanie za pomocą tego algorytmu diff. Chciałbym pozostawić to pytanie otwarte jeszcze przez jakiś czas, może są inne pomysły. –

+0

Interesujący projekt, czekamy na Twoje rozwiązanie. W międzyczasie zostanie docenione upominek ;-) – xeraa

12

Tak właśnie zakończyłem implementację wersji dla podmiotów MongoDB. Dzięki społeczności StackOverflow za pomoc!

  • Dziennik zmian jest przechowywany dla każdej jednostki w osobnym zbiorze historii.
  • Aby uniknąć zapisywania dużej ilości danych, kolekcja historii nie przechowuje kompletnych wystąpień, ale tylko pierwszą wersję i różnice między wersjami. (Można nawet pominąć pierwszą wersję i zrekonstruować wersje "wstecz" z bieżącej wersji w głównym zbiorze jednostki.)
  • służy do generowania różnic obiektowych.
  • Aby móc poprawnie pracować z kolekcjami, należy wdrożyć metodę encji equals, aby testował klucz podstawowy bazy danych, a nie właściwości podrzędne. (W przeciwnym razie JavaObjectDiff nie rozpozna zmian właściwości w elementach kolekcji).

Oto podmioty używane do wersjonowania (pobierające/ustawiające itp.usunięte):

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

Oto saveChangeHistory operacja:

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

Jak to wygląda w 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: Oto kod Visitor:

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

}