30401

Cascading updates with business key equality: Hibernate best practices?

Question:

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:

<ul><li>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.</li> <li>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.</li> </ul>

Is there a better way? Am I making completely the wrong assumptions? I'm hoping that I'm just missing something.

Thanks.

Answer1:

Load the container before you add the children (tis a good idea anyway), that way you will be able to take advantage of the 'set' behaviour. When you add the Item to container it will not be added.

Oh, you'll also need to make sure you override equals and hashcode in Item to ensure things work the way you expect.

So...

Query query = session.createSQLQuery("FROM Container " + "WHERE name = ? AND owner = ?"); query.setString(0, "Things"); query.setString(1, "Me"); Container c = (BigInteger)query.uniqueResult(); c.addItem(new Item("String", "Blue")); c.addItem(new Item("Wax", "Red")); Transaction t = session.beginTransaction(); session.saveOrUpdate(c); t.commit();

Answer2:

The two recommendations (surrogate primary key, business primary key) conflict, because they are recommended for different contexts. Object-oriented programming seems to do better with surrogate primary keys. Hibernate expects you to be doing object-oriented programming, since Hibernate's purpose is to permit you to do object-oriented programming while transparently persisting your objects to a database.

Recommend

  • JQ Aggregations and Crosstabs
  • Mybatis Annotations in Complex Applications
  • XMonad: Is there a way to bind a simultaneously triggered keychord?
  • GWT and web.xml
  • Show fields for a Lucene/Elasticsearch index
  • Stale Java classes when using ColdFusion 10's custom Java loader
  • Replacing points of color by a uniform colored surface
  • The binary operator LessThan is not defined for the types 'System.Nullable`1[System.DateTime]&#
  • Maintaining unique objects for names with concurrent delete
  • How to run .ear file in JBoss 6?
  • Camel REST Bean Chaining
  • Type mismatch: cannot convert from ListFragment to Fragment
  • ASM ClassReader failed to parse class file - probably due to a new Java class file version that isn&
  • ASP.NET MVC - Detect Time Spent on Page
  • replacing while loop with list comprehension
  • Returning this from a constructor function in JS
  • CSS Grid, position absolute an element in a css grid item: IMPOSSIBLE
  • Search function not doing anything
  • Guava how to copy all files from one directory to another
  • C#: Import/Export Settings into/from a File
  • Syntax error on tokens, AnnotationName expected instead - error on query
  • JSR-330 support in Picocontainer : @Inject … @Named(\"xxx)
  • there is no graph with tensorboard
  • how do i write assembly code from c#?
  • DIV instruction jumping to random location?
  • How can I display the parent menu item's description using Wordpress walkers?
  • Groovy: Unexpected token “:”
  • Replace value with Factor in r data.table
  • How to access EntityManager inside Entity class in EJB3
  • Repeat a vertical line on every page in Report Builder / SSRS
  • How reduce the height of an mschart by breaking up the y-axis
  • Can Jackson SerializationFeature be overridden per field or class?
  • Why doesn't :active or :focus work on text links in webkit? (safari & chrome)
  • How to apply VCL Styles to DLL-based forms in Inno Setup?
  • what is the difference between the asp.net mvc application and asp.net web application
  • Calling of Constructors in a Java
  • Rails 2: use form_for to build a form covering multiple objects of the same class
  • NSLayoutConstraint that would pin a view to the bottom edge of a superview
  • Android Google Maps API OnLocationChanged only called once
  • How to push additional view controllers onto NavigationController but keep the TabBar?