Hello. We are using Hibernate to manage repository of midPoint, an open source identity management system. There are approximately 70 entities.
Recently we have come across an issue with merging entities that have composite identifiers containing associations.
(Note that this is actually a modified version of my 4 days old question from stack overflow that unfortunately was left unanswered. I do apologize for cross-posting, as well as for the long post but I have tried to fight the issue for a couple of days and I am completely stuck. I would appreciate your opinions on the most correct way of resolving it. As a new user, I am not allowed to include more than 2 links in this post. So I had to remove all links to code (pointing to the source code on github) and majority of all other links.)
The issue can be demonstrated on a really trivial example. It is almost directly derived from the Hibernate 5.2 documentation (2.6.5. Composite identifiers with associations); except for OneToMany side of the Parent-Child association.
Let us have a parent and a child entity:
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
}
The scenario is:
- Create a parent-child pair and persist it into the database.
- Create a detached instance of the parent object that contains a new (transient) child instead of the original one.
- Merge the detached instance of the parent object.
The code is like this (simplified):
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();
}
And it fails because Hibernate tries to insert null values into the child table.
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
What is wrong here and how to resolve? Here are my thoughts and attempts.
-
Is this a bug in Hibernate? I have tried to diagnose/debug it for a some time and found the following
It looks like that DefaultMergeEventListener.copyValues method (called from line 223) does not copy anything from the transient child entity into a newly created instance (because it omits the identifiers). But I am too little experienced in Hibernate to tell if that’s OK or not.
From the documentation I know that using multiple @Id’s is deprecated…
The restriction that a composite identifier has to be represented by a “primary key class” is only JPA specific. Hibernate does allow composite identifiers to be defined without a “primary key class”, although that modeling technique is deprecated and therefore omitted from this discussion. (from the docs, section 2.6.2)
…but I think that means “it is not recommended but it should work”. I think so also because just the next example - on which my code is based - uses this technique. Overall, I consider it to be a good idea, because it eliminates much clutter from the client source code.
-
Reading the above, I replaced plain identifiers with @IdClass and/or @EmbeddedId. While this resolved the issue for simple parent-child case (simpleWithIdClass in the github project), it did not help for more complex scenario (testWithIdClass in the github project), more directly related to our midPoint usage.
The problem with more complex scenario is the Unable to find column with logical name: owner_owner_oid in org.hibernate.mapping.Table(AssignmentExtension) and its related supertables and secondary tables exception I get. When trying to resolve it using additional fields with @Column(…, insertable=false, updateable=false) annotations (inspired by earlier work of my colleague on midPoint) I have finally made it function, at the cost of making the client code totally incomprehensible and, to be honest, really ugly. Moreover, I don’t know if now it works by accident or “by design”. Please see testWithHacks in the github project.
-
Having thought that the problem lies in missing identifier values I tried to tell Hibernate about the identifier values by using GeneratedValue annotations (inspired by “How to combine the Hibernate assigned generator with a sequence or an identity column” blog post), but it didn’t help - see simpleWithIdentifierGenerators in the github project).
-
As a last resort I have tried to work around the code in DefaultMergeEventListener by persuading it that the transient entity has, in fact, an identifier. I used a custom persister to do that. It helps (even in our application), but I am afraid of the ugliness of the “solution”, in particular of messing with Hibernate internals that I do not understand. See simpleWithCustomPersister package.
Approaches that I considered but not implemented:
-
Introducing artificial (generated) simple identifiers for ExtBoolean (and related) tables. I’d like to avoid that because of the necessity to change DB schema, in particular by introducing another DB column (performance and storage effects).
-
I have recently found the following recommendation in a thread that seems a bit similar to this one:
If you bump into such issue, it means you were doing the merge like this:
- you try to clear the collection
- then you re-add back the entries sent from the client
That’s not a good approach. You should do the merging from the incoming collection and the one in the database like this:
- add the new entries
- update the remaining ones
- delete the ones that are no longer found in the incoming collection
(link)
To be honest I am not sure if this applies to my case. Because of the way we work with our internal objects and their Hibernate representation it would be very complicated to do this. Moreover, shouldn’t Hibernate be able merge arbitrary detached object structure?
Please, what is the cleanest and most recommended way to resolve this issue?
Thank you very much in advance.