C'est ainsi que j'ai fini par implémenter la gestion des versions pour les entités MongoDB. Merci à la communauté StackOverflow pour son aide !
- Un journal des modifications est conservé pour chaque entité dans une collection d'historique distincte.
- Pour éviter d'enregistrer beaucoup de données, la collection d'historique ne stocke pas les instances complètes, mais uniquement la première version et les différences entre les versions. (Vous pouvez même omettre la première version et reconstruire les versions "à l'envers" à partir de la version actuelle dans la collection principale de l'entité.)
- Java Object Diff est utilisé pour générer des différences d'objets.
- Afin de pouvoir travailler correctement avec les collections, il faut implémenter le
equals
méthode des entités afin qu'elle teste la clé primaire de la base de données et non les sous-propriétés. (Sinon, JavaObjectDiff ne reconnaîtra pas les changements de propriétés dans les éléments de collection.)
Voici les entités que j'utilise pour la gestion des versions (getters/setters, etc. supprimés) :
// 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;
}
Voici l'opération 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()));
}
Voici à quoi cela ressemble dans 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 :Voici le code du visiteur :
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);
}
}
}