我们正在使用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
}
场景是:
代码就像这样(简化):
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
在查看源代码时,问题似乎在这里:
即。 on DefaultMergeEventListener:233其中copyValues
方法不会将瞬态子实体中的任何内容复制到新创建的实例中(可能因为它省略了标识符)。
对我来说,它看起来像是Hibernate中的一个bug。但我没有足够的经验知道。我试图解决的问题是:
@IdClass
和/或@EmbeddedId
替换了普通标识符。虽然这解决了simple parent-child case的问题,但它对more complex scenario无效,与我们的midPoint使用更直接相关。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)
来解决它时,除了事实上代码变得越来越不全面之外,我还遇到了另一个问题。DefaultMergeEventListener
中的代码。我使用custom persister来做到这一点。它有助于(甚至在我们的应用中),但我害怕解决方案的丑陋;除了其他方面,它使得hibernate认为瞬态子实例是分离的,因为现在它们确实有一个标识符(由应用程序提供)。拜托,我应该如何应对这种情况?这是一个可以在将来修复的hibernate错误吗?我应该探索@IdClass
方式,试图解决那里出现的问题吗?我知道也许最自然的方式是在子表中引入人工(生成)ID,但我想避免这种情况,因为有必要更改数据库模式。
非常感谢你。
修改(2018-02-06):a discussion on discourse.hibernate.org我提交HHH-12276后。基于@EmbeddedId
的替代解决方案尚未得到验证。