2012-08-24 37 views
11

我需要对存储在面向文档的数据库(MongoDB)中的(简单)Java对象图进行版本控制。对于关系数据库和Hibernate,我发现了Envers,并对可能性感到非常惊讶。是否有类似的东西可以用于Spring Data Documents?Java MongoDB对象版本控制

我发现this post勾勒出的想法我有(更多...)有关存储对象的版本,和我目前的执行工作在它存储的对象的副本在一个单独的历史收集与时间戳类似,但我想要改善这一点以节省存储空间。因此,我认为我需要在对象树上执行“差异”操作和为重建旧对象执行“合并”操作。有没有图书馆在这方面提供帮助?

编辑: 任何有关MongoDB和版本的经验都非常感谢!我认为很可能不会有Spring Data解决方案。

+0

不完整的版本的支持,但我们已经实现了一个很小的审计系统 - 记录谁改变了旧值到新的。我们使用Morphia的''prePersist()''方法(它只适用于完整的实体保存,而不是特定的更新)。可以提供一些代码示例,但它没有什么复杂的... – xeraa

+0

感谢您的评论!我会对展示您的解决方案的更多细节非常感兴趣。只有跟踪完整的实体保存才能确定:这也是我们的主要用例。一个非常有趣的观点是你比较旧实体和新实体的方式,确定已更改的属性。我在这里查看了图形比较框架,但没有找到快速简单的解决方案。 –

回答

7

我们正在使用一个基础实体(我们在这里设置标识,创建+最后更改日期,...)。基于这一点,我们使用一个通用的持久性方法,它看起来是这样的:

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

我们三角洲实体看起来像这样(不干将+ setter方法,的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; 
    } 

希望这有助于你入门:-)

+0

非常感谢您的示例。我还发现一篇关于java对象差异的文章(http://stackoverflow.com/questions/8001400/is-there-a-java-library-that-c​​an-diff-two-objects)提到这个库:https:// github.com/SQiShER/java-object-diff - 也许我可以用这个差异算法“激发”你的解决方案。我想留下这个问题更多的时间,也许还有其他的想法。 –

+0

有趣的项目,期待您的解决方案。在此期间仍然赞赏upvote ;-) – xeraa

12

这就是我最终如何实现MongoDB实体的版本控制。感谢StackOverflow社区的帮助!

  • 更改日志保存在单独的历史记录集合中的每个实体。
  • 为避免保存大量数据,历史集合不存储完整实例,但只存储第一个版本和版本之间的差异。 (您甚至可以省略第一个版本,并从实体的主要集合中的当前版本向后“重新”版本)。
  • Java Object Diff用于生成对象差异。
  • 为了能够正确使用集合,需要实现实体的equals方法,以便它测试数据库主键而不是子属性。 (否则,JavaObjectDiff将无法识别收集元素中的属性更改。)

这里是我用于版本控制的实体(getters/setters等)去掉):

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

编辑:这里是游客代码:

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

}