Управление версиями объектов Java MongoDB

Мне нужно выполнить управление версиями (простых) графов объектов Java, хранящихся в документно-ориентированной базе данных (MongoDB). Что касается реляционных баз данных и Hibernate, я обнаружил Envers и очень поражен возможностями. Есть ли что-то подобное, что можно использовать с Spring Data Documents?

Я нашел этот пост, в котором изложены мои мысли (и многое другое.. .) о хранении версий объектов, и моя текущая реализация работает аналогично тому, что она хранит копии объектов в отдельной коллекции истории с отметкой времени, но я хотел бы улучшить это, чтобы сэкономить место для хранения. Поэтому я думаю, что мне нужно реализовать как операцию «diff» для деревьев объектов, так и операцию «слияния» для восстановления старых объектов. Есть ли какие-нибудь библиотеки, помогающие в этом?

Редактировать: Приветствуется любой опыт работы с MongoDB и управлением версиями! Я вижу, скорее всего, решения Spring Data не будет.


person Matthias Wuttke    schedule 24.08.2012    source источник
comment
Не полное управление версиями, но мы внедрили маленькую систему аудита — регистрацию того, кто изменил какие старые значения на новые. Мы используем метод Morphia prePersist() (который будет работать только для полных сохранений сущностей, а не для конкретных обновлений). Могу предоставить несколько примеров кода, но в этом нет ничего сложного...   -  person xeraa    schedule 26.08.2012
comment
Спасибо за ваш комментарий! Я был бы очень заинтересован в некоторых подробностях, демонстрирующих ваше решение. Абсолютно нормально только отслеживание полных сохранений сущностей: это также наш основной вариант использования. Очень интересным моментом является то, как вы сравниваете старый объект с новым, определяя измененные свойства. Я просмотрел здесь фреймворки для сравнения графов, но не нашел быстрого и простого решения.   -  person Matthias Wuttke    schedule 27.08.2012


Ответы (3)


Мы используем базовую сущность (где мы устанавливаем идентификатор, дату создания + даты последнего изменения и т. д.). Опираясь на это, мы используем общий метод персистентности, который выглядит примерно так:

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

Наша дельта-сущность выглядит так (без геттеров + сеттеров, toString, hashCode и равных):

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

Надеюсь, это поможет вам начать работу :-)

person xeraa    schedule 30.08.2012
comment
Большое спасибо за образец. Я также нашел сообщение о различиях объектов Java (stackoverflow.com/questions/8001400/) с упоминанием этой библиотеки: github.com/SQiShER/java-object-diff — возможно, я смогу оживить ваше решение с помощью этого алгоритма сравнения. Я хотел бы оставить этот вопрос открытым еще некоторое время, может быть, есть другие идеи. - person Matthias Wuttke; 31.08.2012
comment
Интересный проект, жду вашего решения. Тем временем голосование все еще приветствуется ;-) - person xeraa; 31.08.2012

Вот как я реализовал управление версиями для сущностей MongoDB. Спасибо сообществу StackOverflow за помощь!

  • Журнал изменений хранится для каждой сущности в отдельной коллекции истории.
  • Чтобы избежать сохранения большого количества данных, в коллекции истории хранятся не полные экземпляры, а только первая версия и различия между версиями. (Вы даже можете опустить первую версию и реконструировать версии «назад» из текущей версии в основной коллекции объекта.)
  • Различия объектов Java используется для создания различий объектов.
  • Чтобы правильно работать с коллекциями, необходимо реализовать метод 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);
    }
}

}
person Matthias Wuttke    schedule 31.08.2012
comment
Для всех, кто просматривает это годы спустя: Visitor::accept с тех пор был переименован в Visitor::node. - person Kyrstellaine; 10.06.2021

похоже, что Javers — правильный инструмент для этой работы, см. http://javers.org/documentation/features/#javers-repository

Концептуально Javers — это VCS для управления версиями объектов домена, поддерживаемая JSON и MongoDB.

person Bartek Walacik    schedule 01.02.2015
comment
На самом деле, я должен пересмотреть свой предыдущий комментарий. Я попытался использовать Javers только для того, чтобы обнаружить, что это невозможно, поскольку он всегда создает текущие объекты из базовой версии плюс все изменения, что делает время чтения примерно в 20 раз больше, чем если бы он просто где-то хранил последнюю версию документа. . И поскольку получение последней версии документа является своего рода основным вариантом использования, на мой взгляд, это своего рода остановка шоу. - person Kira Resari; 24.06.2020