Last week I found a strange bug in a project I’m working on. We have a couple of domain objects which depend on each other in a multi-level master-detail relation. The domain objects are managed by Hibernate and everything was working fine until we noticed that sometimes after a complicated update the details relation of one of the master objects was missing a few entries. It took little time to identify the service method where the bug had to happen. However when I stepped through the code in the debugger everything was fine – each object contained all the dependent objects that had to be there. Too bad that the database didn’t agree with the debugger.
Be careful with one-to-many relations
So what exactly was going wrong? The domain objects in question could have been taken clear out of a textbook, there was nothing special about them. But the effect is easy to reproduce even with two trivial classes.
The problem was that the same details object was added to two master objects. All was fine as long as the objects remained in the Hibernate session: the details object was contained in the corresponding Set of each master object. Of course the database saw things differently. There only one row in the master table could be the owner of the details row. And after the domain objects were reloaded from the database the object showed the same relation and only one master object owned the details object. The other “lost” it.
A trivial example
The code for a trivial example reproducing this effect looks like this:
Master.java
public class Master {
private Long id;
private String name;
private Set details = new HashSet();
public Master() {
}
public Master(String name) {
this.name = name;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Set getDetails() {
return details;
}
public void setDetails(Set details) {
this.details = details;
}
public void add(Detail detail) {
this.details.add(detail);
}
@Override
public String toString() {
return "Master{" +
"id=" + id +
", name='" + name + '\'' +
", details=" + details +
'}';
}
}
Detail.java
public class Detail {
private Long id;
private String name;
public Detail() {
}
public Detail(String name) {
this.name = name;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "Detail{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
}
Hibernate Mapping
<class name="Master" table="MASTER">
<id name="id" type="java.lang.Long">
<generator class="identity" />
</id>
<property name="name" type="java.lang.String" />
<set name="details" cascade="all" access="field">
<key column="MASTER_ID" />
<one-to-many class="org.acm.steidinger.masterDetail.Detail" />
</set>
</class>
<class name="Detail" table="DETAIL">
<id name="id" type="java.lang.Long">
<generator class="identity" />
</id>
<property name="name" type="java.lang.String" />
</class>
Test.java
public class Test {
public static void main(String[] args) {
Configuration config = new Configuration();
config.configure("hibernate.cfg.xml");
SessionFactory sessionFactory = config.buildSessionFactory();
Session session = sessionFactory.openSession();
Transaction transaction = session.beginTransaction();
Master firstMaster = new Master("first");
Detail detail = new Detail("firstDetail");
firstMaster.add(detail);
session.save(detail);
session.save(firstMaster);
transaction.commit();
transaction.begin();
Master secondMaster = new Master("second");
secondMaster.add(detail);
session.save(secondMaster);
transaction.commit();
System.out.println("firstMaster = " + firstMaster);
System.out.println("secondMaster = " + secondMaster);
session.clear();
transaction.begin();
firstMaster = (Master) session.get(Master.class, firstMaster.getId());
secondMaster = (Master) session.get(Master.class, secondMaster.getId());
System.out.println("firstMaster = " + firstMaster);
System.out.println("secondMaster = " + secondMaster);
transaction.commit();
session.close();
}
}
The session.clear() in the test method is there to simulate the following calls being in a new session. As you will see if you run this code the output will look like this:
firstMaster = Master{id=1, name='first', details=[Detail{id=1, name='firstDetail'}]}
secondMaster = Master{id=2, name='second', details=[Detail{id=1, name='firstDetail'}]}
firstMaster = Master{id=1, name='first', details=[]}
secondMaster = Master{id=2, name='second', details=[Detail{id=1, name='firstDetail'}]}
As expected one of the Master object loses the details object when it is reloaded from the database. But how can we avoid this effect? One way is to put the responsibility on the shoulders of the user of the domain classes who will have to be very careful about not adding an object to two owners. Another way is to augment the domain classes so that the details object can never have more than one owner.
A simple solution
Putting the responsibility on the user of our domain classes rather obviously is an invitation to disaster as sooner rather than later someone will forget the restriction. The first step to a solution is hiding the internals of the details relation, i.e. prevent an outsider from modifying the contains of the details set directly. To this end we modify the getter and setter as follows:
public Set getDetails() {
return Collections.unmodifiableSet(details);
}
public void setDetails(Set details) {
this.details = new HashSet();
for(Detail detail : details) {
add(detail);
}
}
Now the master object has complete control over what happens when a details object is added. It can detect if a newly added details object is already owned by another master or not. In our case the details object did not contain any special state and could be easily replicated so the proposed implementation for the add method looks like this:
public void add(Detail detail) {
if (detail != null && detail.getId() != null) {
this.details.add(new Detail(detail));
}
else {
details.add(detail);
}
}
Now if we run this new version of our example the output looks like this:
firstMaster = Master{id=1, name='first', details=[Detail{id=1, name='firstDetail'}]}
secondMaster = Master{id=2, name='second', details=[Detail{id=2, name='firstDetail'}]}
firstMaster = Master{id=1, name='first', details=[Detail{id=1, name='firstDetail'}]}
secondMaster = Master{id=2, name='second', details=[Detail{id=2, name='firstDetail'}]}
An obvious improvement to the above code is making the relation bidirectional and checking for an existing owner using the getter for the master object. However for this trivial example a quick check for the ID also works.
The moral of the story
Do not rely on your ORM tool for maintaining the correct semantics of your object’s relations. If you have objects that can only be owned by one owner ensure that restriction yourself.
I find it quite strange that none of the books on the topic of ORM that I’ve read fail to mention this pitfall.