合并失败,包含关联

时间:2018-02-02 15:09:26

标签: java hibernate

我们正在使用hibernate来管理midPoint的存储库,这是一个开源身份管理系统。大约有70个实体。

最近,我们遇到了合并具有包含关联的复合标识符的实体的问题。

这个问题可以通过一个非常简单的例子来证明。它几乎直接来自Hibernate 5.2 documentation(协会的OneToMany方除外)。

让我们有一个父实体和一个子实体:

Parent.java

@Entity
public class Parent implements Serializable {

    @Id
    private Integer id;

    @OneToMany(fetch = FetchType.LAZY, mappedBy = "parent", 
               orphanRemoval = true, cascade = CascadeType.ALL)
    private Set<Child> children = new HashSet<>();

    // getters, setters, equals, hashCode omitted for brevity
}

Child.java

@Entity
public class Child implements Serializable {

    @Id
    @ManyToOne(fetch = FetchType.LAZY, optional = false)
    private Parent parent;

    @Id
    private String value;

    // getters, setters, equals, hashCode omitted for brevity
}

场景是:

  1. 创建父子对并将其保留在数据库中。
  2. 创建包含新(临时)子对象而不是原始对象的父对象的分离实例。
  3. 合并父对象的新实例。
  4. 代码就像这样(简化):

        public static void main(String[] args) {
            create();
            update();
        }
    
        private static void create() {
            Session session = HibernateUtil.getSessionFactory().openSession();
            session.beginTransaction();
    
            Parent parent = new Parent();
            parent.setId(10);
    
            Child child = new Child();
            child.setParent(parent);
            parent.getChildren().add(child);
            child.setValue("old");
    
            session.save(parent);
            session.getTransaction().commit();
            session.close();
        }
    
        private static void update() {
            Session session = HibernateUtil.getSessionFactory().openSession();
            session.beginTransaction();
    
            Parent parent = new Parent();
            parent.setId(10);
    
            Child child = new Child();
            child.setParent(parent);
            parent.getChildren().add(child);
            child.setValue("new");
    
            session.merge(parent);
            session.getTransaction().commit();
            session.close();
        }
    

    它失败了,因为Hibernate试图将空值插入子表。

    Exception in thread "main" javax.persistence.PersistenceException: org.hibernate.exception.ConstraintViolationException: could not execute statement
        at org.hibernate.internal.ExceptionConverterImpl.convert(ExceptionConverterImpl.java:149)
        at org.hibernate.internal.ExceptionConverterImpl.convert(ExceptionConverterImpl.java:157)
        at org.hibernate.internal.ExceptionConverterImpl.convert(ExceptionConverterImpl.java:164)
        at org.hibernate.internal.SessionImpl.doFlush(SessionImpl.java:1443)
        at org.hibernate.internal.SessionImpl.managedFlush(SessionImpl.java:493)
        at org.hibernate.internal.SessionImpl.flushBeforeTransactionCompletion(SessionImpl.java:3207)
        at org.hibernate.internal.SessionImpl.beforeTransactionCompletion(SessionImpl.java:2413)
        at org.hibernate.engine.jdbc.internal.JdbcCoordinatorImpl.beforeTransactionCompletion(JdbcCoordinatorImpl.java:473)
        at org.hibernate.resource.transaction.backend.jdbc.internal.JdbcResourceLocalTransactionCoordinatorImpl.beforeCompletionCallback(JdbcResourceLocalTransactionCoordinatorImpl.java:156)
        at org.hibernate.resource.transaction.backend.jdbc.internal.JdbcResourceLocalTransactionCoordinatorImpl.access$100(JdbcResourceLocalTransactionCoordinatorImpl.java:38)
        at org.hibernate.resource.transaction.backend.jdbc.internal.JdbcResourceLocalTransactionCoordinatorImpl$TransactionDriverControlImpl.commit(JdbcResourceLocalTransactionCoordinatorImpl.java:231)
        at org.hibernate.engine.transaction.internal.TransactionImpl.commit(TransactionImpl.java:68)
        at simple.TestMergeParentChild.update(TestMergeParentChild.java:49)
        at simple.TestMergeParentChild.main(TestMergeParentChild.java:10)
    Caused by: org.hibernate.exception.ConstraintViolationException: could not execute statement
        at org.hibernate.exception.internal.SQLStateConversionDelegate.convert(SQLStateConversionDelegate.java:112)
        at org.hibernate.exception.internal.StandardSQLExceptionConverter.convert(StandardSQLExceptionConverter.java:42)
        at org.hibernate.engine.jdbc.spi.SqlExceptionHelper.convert(SqlExceptionHelper.java:111)
        at org.hibernate.engine.jdbc.spi.SqlExceptionHelper.convert(SqlExceptionHelper.java:97)
        at org.hibernate.engine.jdbc.internal.ResultSetReturnImpl.executeUpdate(ResultSetReturnImpl.java:178)
        at org.hibernate.engine.jdbc.batch.internal.NonBatchingBatch.addToBatch(NonBatchingBatch.java:45)
        at org.hibernate.persister.entity.AbstractEntityPersister.insert(AbstractEntityPersister.java:3013)
        at org.hibernate.persister.entity.AbstractEntityPersister.insert(AbstractEntityPersister.java:3513)
        at org.hibernate.action.internal.EntityInsertAction.execute(EntityInsertAction.java:89)
        at org.hibernate.engine.spi.ActionQueue.executeActions(ActionQueue.java:589)
        at org.hibernate.engine.spi.ActionQueue.executeActions(ActionQueue.java:463)
        at org.hibernate.event.internal.AbstractFlushingEventListener.performExecutions(AbstractFlushingEventListener.java:337)
        at org.hibernate.event.internal.DefaultFlushEventListener.onFlush(DefaultFlushEventListener.java:39)
        at org.hibernate.internal.SessionImpl.doFlush(SessionImpl.java:1437)
        ... 10 more
    Caused by: org.h2.jdbc.JdbcSQLException: NULL not allowed for column "VALUE"; SQL statement:
    /* insert simple.Child */ insert into Child (value, parent_id) values (?, ?) [23502-193]
        at org.h2.message.DbException.getJdbcSQLException(DbException.java:345)
        at org.h2.message.DbException.get(DbException.java:179)
        at org.h2.message.DbException.get(DbException.java:155)
        at org.h2.table.Column.validateConvertUpdateSequence(Column.java:311)
        at org.h2.table.Table.validateConvertUpdateSequence(Table.java:784)
        at org.h2.command.dml.Insert.insertRows(Insert.java:151)
        at org.h2.command.dml.Insert.update(Insert.java:114)
        at org.h2.command.CommandContainer.update(CommandContainer.java:98)
        at org.h2.command.Command.executeUpdate(Command.java:258)
        at org.h2.jdbc.JdbcPreparedStatement.executeUpdateInternal(JdbcPreparedStatement.java:160)
        at org.h2.jdbc.JdbcPreparedStatement.executeUpdate(JdbcPreparedStatement.java:146)
        at org.hibernate.engine.jdbc.internal.ResultSetReturnImpl.executeUpdate(ResultSetReturnImpl.java:175)
        ... 19 more
    

    在查看源代码时,问题似乎在这里:

    Picture from the debugger

    即。 on DefaultMergeEventListener:233其中copyValues方法不会将瞬态子实体中的任何内容复制到新创建的实例中(可能因为它省略了标识符)。

    对我来说,它看起来像是Hibernate中的一个bug。但我没有足够的经验知道。我试图解决的问题是:

    1. 我用@IdClass和/或@EmbeddedId替换了普通标识符。虽然这解决了simple parent-child case的问题,但它对more complex scenario无效,与我们的midPoint使用更直接相关。
    2. 更复杂情况的问题是我得到的Unable to find column with logical name: owner_owner_oid in org.hibernate.mapping.Table(AssignmentExtension) and its related supertables and secondary tables例外。当尝试使用额外的@Column(..., insertable=false, updateable=false)来解决它时,除了事实上代码变得越来越不全面之外,我还遇到了另一个问题。
    3. 我尝试使用@GeneratedValue annotations告诉hibernate标识符,但它没有帮助。
    4. 作为最后的手段,我试图通过说服瞬态实体实际上有一个标识符来解决DefaultMergeEventListener中的代码。我使用custom persister来做到这一点。它有助于(甚至在我们的应用中),但我害怕解决方案的丑陋;除了其他方面,它使得hibernate认为瞬态子实例是分离的,因为现在它们确实有一个标识符(由应用程序提供)。
    5. 拜托,我应该如何应对这种情况?这是一个可以在将来修复的hibernate错误吗?我应该探索@IdClass方式,试图解决那里出现的问题吗?我知道也许最自然的方式是在子表中引入人工(生成)ID,但我想避免这种情况,因为有必要更改数据库模式。

      非常感谢你。

      修改(2018-02-06)a discussion on discourse.hibernate.org我提交HHH-12276后。基于@EmbeddedId的替代解决方案尚未得到验证。

0 个答案:

没有答案