Управление версиями Java MongoDB
Мне нужно сделать управление версиями (простых) графов объектов Java, хранящихся в документно-ориентированной базе данных (MongoDB). Для реляционных баз данных и Hibernate я открыл Envers и очень удивлен возможностями. Есть ли что-то подобное, что можно использовать с Spring Data Documents?
Я нашел этот пост, в котором изложены мысли, которые у меня были (и больше...) о хранении версий объектов, и моя текущая реализация работает аналогично в том, что он хранит копии объектов в отдельной коллекции истории с отметкой времени, но я хотел бы улучшить это, чтобы сэкономить место для хранения. Поэтому я думаю, что мне нужно реализовать как операцию "diff" на деревьях объектов, так и операцию "слияния" для восстановления старых объектов. Есть ли какие-нибудь библиотеки, помогающие с этим?
Изменить: Любой опыт с MongoDB и версиями высоко ценится! Я думаю, что, скорее всего, решения Spring Data не будет.
3 ответа
Мы используем базовую сущность (где мы устанавливаем Id, дату создания + последнего изменения,...). Опираясь на это, мы используем универсальный метод персистентности, который выглядит примерно так:
@Override
public <E extends BaseEntity> ObjectId persist(E entity) {
delta(entity);
mongoDataStore.save(entity);
return entity.getId();
}
Дельта-метод выглядит следующим образом (я постараюсь сделать его как можно более универсальным):
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;
}
Наша дельта-сущность выглядит так (без getters + setters, toString, hashCode и equals):
@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;
}
Надеюсь, это поможет вам начать:-)
Так я реализовал управление версиями для сущностей MongoDB. Спасибо сообществу Stackru за помощь!
- Журнал изменений хранится для каждого объекта в отдельной коллекции истории.
- Чтобы избежать сохранения большого количества данных, коллекция истории хранит не полные экземпляры, а только первую версию и различия между версиями. (Вы могли бы даже опустить первую версию и восстановить версии "назад" из текущей версии в основной коллекции сущности.)
- Java Object Diff используется для генерации объектов diff.
- Чтобы правильно работать с коллекциями, необходимо реализовать
equals
метод сущностей, чтобы он проверял первичный ключ базы данных, а не подчиненные свойства. (В противном случае JavaObjectDiff не будет распознавать изменения свойств в элементах коллекции.)
Вот сущности, которые я использую для управления версиями (удаленные методы получения / установки и т. Д.):
// 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;
}
Вот операция 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()));
}
Вот как это выглядит в 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[LetterSection@116a3de]",
"state" : "ADDED",
"modified" : {
"_class" : "LetterSection",
"_id" : ObjectId("5034c7a83c75c52dddcbd556"),
"letterId" : "5034c7a83c75c52dddcbd554",
"sectionIndex" : 2,
"stringContent" : "BLABLA",
"contentMimetype" : "text/plain",
"sectionConfiguration" : "BLUBB"
}
}, {
"path" : "/sections[LetterSection@19546ee]",
"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[LetterSection@44a38a]/stringContent",
"state" : "CHANGED",
"base" : "blub5",
"modified" : "blub6"
}]
},
}],
"deleted" : false
}
РЕДАКТИРОВАТЬ: Вот код посетителя:
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);
}
}
}
Похоже, что Javers - подходящий инструмент для этой работы, см. http://javers.org/documentation/features/.
Концептуально Javers - это VCS для управления версиями объектов домена, поддерживаемая JSON и MongoDB