Cascading updates with business key equality: Hibernate best practices?
- by Traphicone
I'm new to Hibernate, and while there are literally tons of examples to look at, there seems to be so much flexibility here that it's sometimes very hard to narrow all the options down the best way of doing things. I've been working on a project for a little while now, and despite reading through a lot of books, articles, and forums, I'm still left with a bit of a head scratcher. Any veteran advice would be very appreciated.
So, I have a model involving two classes with a one-to-many relationship from parent to child. Each class has a surrogate primary key and a uniquely constrained composite business key.
<class name="Container">
<id name="id" type="java.lang.Long">
<generator class="identity"/>
</id>
<properties name="containerBusinessKey" unique="true" update="false">
<property name="name" not-null="true"/>
<property name="owner" not-null="true"/>
</properties>
<set name="items" inverse="true" cascade="all-delete-orphan">
<key column="container" not-null="true"/>
<one-to-many class="Item"/>
</set>
</class>
<class name="Item">
<id name="id" type="java.lang.Long">
<generator class="identity"/>
</id>
<properties name="itemBusinessKey" unique="true" update="false">
<property name="type" not-null="true"/>
<property name="color" not-null="true"/>
</properties>
<many-to-one name="container" not-null="true" update="false"
class="Container"/>
</class>
The beans behind these mappings are as boring as you can possibly imagine--nothing fancy going on. With that in mind, consider the following code:
Container c = new Container("Things", "Me");
c.addItem(new Item("String", "Blue"));
c.addItem(new Item("Wax", "Red"));
Transaction t = session.beginTransaction();
session.saveOrUpdate(c);
t.commit();
Everything works fine the first time, and both the Container and its Items are persisted. If the above code block is executed again, however, Hibernate throws a ConstraintViolationException--duplicate values for the "name" and "owner" columns. Because the new Container instance has a null identifier, Hibernate assumes it is an unsaved transient instance. This is expected but not desired. Since the persistent and transient Container objects have the same business key values, what we really want is to issue an update.
It is easy enough to convince Hibernate that our new Container instance is the same as our old one. With a quick query we can get the identifier of the Container we'd like to update, and set our transient object's identifier to match.
Container c = new Container("Things", "Me");
c.addItem(new Item("String", "Blue"));
c.addItem(new Item("Wax", "Red"));
Query query = session.createSQLQuery("SELECT id FROM Container" +
"WHERE name = ? AND owner = ?");
query.setString(0, c.getName());
query.setString(1, c.getOwner());
BigInteger id = (BigInteger)query.uniqueResult();
if (id != null) {
c.setId(id.longValue());
}
Transaction t = session.beginTransaction();
session.saveOrUpdate(c);
t.commit();
This almost satisfies Hibernate, but because the one-to-many relationship from Container to Item cascades, the same ConstraintViolationException is also thrown for the child Item objects.
My question is: what is the best practice in this situation? It is highly recommended to use surrogate primary keys, and it is also recommended to use business key equality. When you put these two recommendations in to practice together, however, two of the greatest conveniences of Hibernate--saveOrUpdate and cascading operations--seem to be rendered almost completely useless. As I see it, I have only two options:
Manually fetch and set the identifier for each object in the mapping. This clearly works, but for even a moderately sized schema this is a lot of extra work which it seems Hibernate could easily be doing.
Write a custom interceptor to fetch and set object identifiers on each operation. This looks cleaner than the first option but is rather heavy-handed, and it seems wrong to me that you should be expected to write a plug-in which overrides Hibernate's default behavior for a mapping which follows the recommended design.
Is there a better way? Am I making completely the wrong assumptions? I'm hoping that I'm just missing something.
Thanks.