Search code examples
javamongodbdiffversioningspring-data

Java MongoDB Object Versioning


I need to do versioning on (simple) Java object graphs stored in a document-oriented database (MongoDB). For relational databases and Hibernate, I discovered Envers and am very amazed about the possibilities. Is there something similar that can be used with Spring Data Documents?

I found this post outlining the thoughts I had (and more...) about storing the object versions, and my current implementation works similar in that it stores copies of the objects in a separate history collection with a timestamp, but I would like to improve this to save storage space. Therefore, I think I need to implement both a "diff" operation on object trees and a "merge" operation for reconstructing old objects. Are there any libraries out there helping with this?

Edit: Any experiences with MongoDB and versioning highly appreciated! I see most probably there won't be a Spring Data solution.


Solution

  • We're using a base entity (where we set the Id, creation + last change dates,...). Building upon this we're using a generic persistence method, which looks something like this:

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

    The delta method looks like this (I'll try to make this as generic as possible):

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

    Our delta entity looks like that (without the getters + setters, toString, hashCode, and 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;
        }
    

    Hope this helps you getting started :-)