Today I spend some hours trying to fix a hibernate bug in our application. I changed the configuration just a little and it seemed that Hibernate was unable to handle this. I'd even found a bug report in Hibernate JIRA that described the same situation. I was on the brink of downloading the sources and trying to fix the problem in Hibernate... turns out there was an error in our configuration! This is to say, the model we wanted to configure could be configured in a non straight forward way. Apparently from the JIRA issue there are more people that find it difficult to come up with the correct configuration for this situation. Let me try to help them with a little example.
Say we have a Master class, that has a set of children and a Peer property. The set of Children is modeled as a one-to-many association and the Peer property as a one-to-one association. Furthermore both associations are bi-directionally mapped. This is a fairly straightforward mapping:
Now this will lead to a very simple database schema, as depicted in the image below.
Now in some cases you might want to split the Peer and Child tables into two (joined) tables. A particular situation when this might be useful is if the class is part of a hierarchy and a mixed table-per-hierarchy, table-per-class strategy is used. The database schema we are aiming for is depicted below.
Solution - First Attempt
Now how do you change the mapping? A logical idea would be that, since we have split the Peer and Child class in to two tables, we need a <join> in the configuration for these two classes. The configuration then looks like this:
This mapping seems to be very logical. The changes have been made in the PEER and CHILD tables and we have changed the mapping for these tables. There is no need to change the mapping for the MASTER table, right? Nothing has changed there, right?
WRONG! Inserting data works fine with this mapping, but selecting data gives terrible SQLExcptions. Doing a simple get on a Master object (with the children set configured with lazy="false" and fetch="join" to make the error more apparent) will lead to the following exception:
org.hibernate.exception.SQLGrammarException: could not load an entity: [com.xebia.hibernate.joinassociation.Master#0] Caused by: java.sql.SQLException: Column not found: MASTER_FK in statement [...]
Taking a closer look at the SQL that is being executed will explain the cause of the exception.
select master0_.ID as ID0_2_, peer1_.ID as ID1_0_, (1) peer1_1_.MASTER_FK as MASTER2_2_0_, (3) children2_.MASTER_FK as MASTER2_4_, children2_.ID as ID4_, children2_.ID as ID3_1_, (4) children2_1_.MASTER_FK as MASTER2_4_1_ from MASTER master0_ left outer join PEER peer1_ (2) on master0_.ID=peer1_.MASTER_FK left outer join PEER_JOIN peer1_1_ on peer1_.ID=peer1_1_.ID left outer join CHILD children2_ (5) on master0_.ID=children2_.MASTER_FK left outer join CHILD_JOIN children2_1_ on children2_.ID=children2_1_.ID where master0_.ID=?
This statement look really odd:
- In the select clause, the MASTER_FK column is correctly queried from the PEER_JOIN table, ...
- but in the join condition it selected from the PEER table.
- In the select clause, the MASTER_FK column is queried both from the CHILD table
- as the CHILD_JOIN table,
- and in the join condition it is selected from the CHILD table.
Now the MASTER_FK column is in the ..._JOIN table in both cases, so that is why the database is complaining about this query. To me this looks like an error in Hibernate: The configuration clearly stated that the MASTER_FK column is in the ..._JOIN table. When querying from the Master-side of the association, Hibernate should be able to figure out where to find this column as well.
Actually, doing bi-directional associations with join table is very well documented in the Hibernate documentation. The syntax used for this configuration is just so very counter intuitive, that it is hard to remember. The correct mapping is this:
(4) (1) (5) (2) (3)
The following changes have been made:
- The join that we had in the Peer class configuration is also added to the Master class configuration.
- The one-to-one mapping of the peer property has changed to a unique many-to-one mapping. The mapping for the peer property of the Master class is now a mirror image of the master property in the Peer class.
- One of the sides of the bi-directional join mapping must be inverse. I'll come back to that later.
- The children set is now mapped to the CHILD_JOIN table...
- ...and it has become a unique many-to-many mapping instead of a one-to-many mapping.
Looking at the table structure for the bi-directional one-to-one on a join table, it is understandable why Hibernate chooses for a symmetric configuration, because the table structure (MASTER <- PEER_JOIN -> PEER) is symmetric.
The SQL that hibernate will produce from this mapping looks like this
select master0_.ID as ID0_2_, master0_1_.ID as ID2_2_, children1_.MASTER_FK as MASTER2_4_, child2_.ID as ID4_, child2_.ID as ID4_0_, child2_1_.MASTER_FK as MASTER2_1_0_, peer3_.ID as ID3_1_, peer3_1_.MASTER_FK as MASTER1_2_1_ from MASTER master0_ inner join PEER_JOIN master0_1_ on master0_.ID=master0_1_.MASTER_FK left outer join CHILD_JOIN children1_ on master0_.ID=children1_.MASTER_FK left outer join CHILD child2_ on children1_.ID=child2_.ID left outer join CHILD_JOIN child2_1_ on child2_.ID=child2_1_.ID left outer join PEER peer3_ on master0_1_.ID=peer3_.ID left outer join PEER_JOIN peer3_1_ on peer3_.ID=peer3_1_.ID where master0_.ID=?
As you can see, all columns are now selected from the correct tables. Hibernate joins a few tables too many, but that is because it likes to address each entity and association as a separate part of the query, so the CHILD_JOIN.MASTER_FK column is once queried as part of the Master->Child association (with alias children1_) and once as property of CHILD (with alias child2_1_).
We assumed that we wouldn't have to change the Master class configuration, since we didn't change the MASTER table. We forgot that the mapping configuration also contains all associations and that these associations did change.
Hibernate then forces you to change all bi-directional one-to-... associations to bi-directional unique many-to-... associations. This configuration works fine, but it is very counter intuitive and therefor hard to remember and use.
Complication - Inverse-ness
From this configuration one complication arises. As explained above, when a join table is bi-directionally mapped, we need to make onside of the association inverse. This should always be done for any bi-directional association. Mapping the join as inverse means that the properties in the join will not be updated from that side of the association.
In the example above we have made the join in the Peer class mapping inverse. Now what if we want to add a property to the Peer class that is mapped on the PEER_JOIN table? Normally we would just add a <property ... /> node in the <join .../> node of the Peer class configuration, but since the join is inverse on this side, the property will not be saved. So we need to move the inverse-ness of this association to the other side.
Now just making the join in the Master class configuration inverse will break the cascading behavior. Until now we had configured hibernate to save the whole object tree from a master object. The inserting sequence for this was MASTER, PEER, PEER_JOIN, ... . Now after moving the inverse-ness to the other side of the join, hibernate changes the sequence to PEER, PEER_JOIN, MASTER, ... . But this won't wprk, since at the point of inserting the row in the PEER_JOIN, the value for the not nullable MASTER_FK column is not yet known. This will result in ConstraintViolationExceptions.
The only way to resolve this is by changing the application to save the object tree from the Peer object. This is really bad, because it means you have to change your application to enable Hibernate to work correctly. I can't really imagine why Hibernate is unable to find the correct sequence for saving the data cascadingly.
I hope this blog will save you some time in finding the right mapping for your bi-directional association with join table.