Saturday, March 24, 2007

Adventures in hibernate #1

Recently, in one of the projects I am involved, I had to cope with the following situation: Two database tables (TableA and TableB) were mapped to two java classes (ClassA and ClassB) by use of the hibernate orm framework. Those tables had a one-to-many relation from TableA to TableB by use of a single foreign key. In java ClassA had a Set<ClassB> property and ClassB had a ClassA property (a bi-directional one-to-many).

public class ClassA {
...
   Set<ClassB> b;
...
}

public class ClassB {
...
   ClassA a;
...
}

The corresponding mapping in ClassA.hbm.xml

<set name="b">
   <one-to-many class="ClassB">
   <key>
      <column name="a_id">
   </key>
</set>

and in ClassB.hbm.xml

<many-to-one name="a" class="ClassA" column="a_id">

As it is shown, TableB has a column a_id which is the foreign key to TableA. By use of this foreign key hibernate can correctly map both sides of the relation.

A few months after the system had been in production, due to a functionality extension required by the client, a new type of data was introduced that could be modeled as a subclass of ClassB.

public class SubclassOfB extends ClassB {
}


Hibernate provides many ways to map class hierarchies to tables, however the single table per hierarchy method was chosen not only for its efficiency but mainly because it was a requirement that the two types of data (ClassB and SubclassOfB) should be accommodated in the same table (TableB). What further complicated the situation was the requirement that instances of ClassA had to define two sets: one Set<ClassB> and one Set<SubclassOfB>. That was simply because the existing functionality should not be disturbed. Now ClassA had an additional Set in xml mapping

<set name="sb"
    <key>
        <column name="a_id"/>
    </key>
    <one-to-many class="SubclassOfB"/>
</set>

Note that the foreign key column is the same. So we have two one-to-many relations from ClassA to ClassB using the same foreign key column. Big mistake though it didn't look like a problem at first. However, in certain cases, hibernate had a very strange behavior (e.g. trying to get the set of SubclassOfB instances from a certain instance of ClassA was actually returning instances of ClassB belonging to another instance of ClassA thus resulting in WrongClassException errors).

The solution to the above was to use a different foreign key column for the second relation. So the two sets were mapped:

<set name="b">
   <one-to-many class="ClassB">
   <key>
      <column name="a_id">
   </key>
</set>

<set name="sb"
    <key>
        <column name="a_id_new"/>
    </key>
    <one-to-many class="SubclassOfB"/>
</set>

Instances of ClassB had a many-to-one property to the containing instance of ClassA. This property was as well inherited to SubclassOfB and was mapped to the common foreign key. Now that we had two foreign keys, this property was correctly mapped to the first foreign key for ClassB, but for instances of SubclassOfB the foreign key column contained null values (because the relation from ClassA to SubclassOfB was mapped through the other foreign key). So we had two options: First one was to override this property in SubclassOfB and mapped the new one in a new foreign key column, but this was unacceptable because it would destroy the abstraction (we needed that SubclassOfB and ClassB use the same property to go back to ClassA). So we chose to map the existing property in ClassB (that was also inherited by SubclassOfB) to a third foreign key:

<many-to-one name="a" column="a_id_inverse" class="ClassA"/>

So we now have:
-- a foreign key that relates one instance of ClassA to many instances of ClassB
-- a second foreign key that relates one instance of ClassA to many instances of SubclassOfB
-- a third foreign key that models the other direction from ClassB and SubclassOfB back to ClassA

This is certainly a problem for the database administrator but it is the only way to map correctly a double one-to-many bidirectional relation.

2 comments:

Anonymous said...

We ran into a similar problem using JPA over Hibernate.
The solution in our case was to use the @Where annotation (org.hibernate.annotations.Where)
I am assuming this can be configured in xml as well. The column 'type' was the discriminator
for single table inheritence of Class B and its subclasses.

@OneToMany(mappedBy = "A")
@Where(clause = "type='B'")
public List<B> getB() { ... }

@OneToMany(mappedBy = "A")
@Where(clause = "type='SubB'")
public List<SubB> getSubB() { ... }

chstath said...

Thanks for the comment. I 'll try your approach too. It may simplify things a little.