Tuesday April 10, 2007 Robert Donaway
I have used Hibernate on several projects that required the users be able to enter rich text comments for many of the major domain objects.
In each case there needed to be a one-to-many relationship between each entity and comments, so that different users could comment on an entity
over time. To store these in the database, my DBA's have requested a universal COMMENT table that would hold comments for any entity
in a CLOB column. Among other benefits, this allows for easy searching over the entire set of comments.
Here is the (Oracle) DDL for a situation like this, where two domain objects, Person and Vehicle, stored in the
PERSON and VEHICLE tables, store their comments in the COMMENTS table:
CREATE TABLE COMMENTS
( COMMENT_ID NUMERIC(12) NOT NULL
, CREATED_ON DATE NOT NULL
, CREATED_BY VARCHAR(50) NOT NULL
, COMMENT_TYPE CHAR(1) NOT NULL
, PERSON_ID NUMERIC(12)
, VEHICLE_ID NUMERIC(12)
, COMMENT_TEXT CLOB NOT NULL
, PRIMARY KEY (COMMENT_ID)
, FOREIGN KEY (PERSON_ID) REFERENCES PERSON(PERSON_ID)
, FOREIGN KEY (VEHICLE_ID) REFERENCES VEHICLE(VEHICLE_ID)
, CHECK (COMMENT TYPE IN ('P', 'V'))
, CHECK ((PERSON_ID IS NULL AND VEHICLE_ID IS NOT NULL) OR (PERSON_ID IS NOT NULL AND VEHICLE_ID IS NULL))
);
Note that this table has a single synthetic primary key, which is generated sequentially as records are inserted. Also, I chose to have separate
columns for the PERSON and VEHICLE keys so that I can enforce integrity with a foreign key constraint. The check constraint
ensures that exactly one of these is not null. The COMMENT_TYPE column will be 'P' if the comment is about a Person and 'V'
if the comment is about a Vehicle.
COMMENT_TYPE can be inferred from the data in the PERSON_ID
and VEHICLE_ID columns, but it will come in handy, as we will see later.
In our Java application, both the Person and Vehicle objects have a collection of Comment objects.
However, a Comment object should also have a reference back to its domain object, so that we can navigate to it easily.
One way to model this type of relationship is to create specific PersonComment and VehicleComment classes, which extend the
generic Comment class. Here is some sample code, leaving out the accessor methods and other details:
public class Person {
private Long personId;
...
private Set comments;
...
}
public class Vehicle {
private Long vehicleId;
...
private Set comments;
...
}
public abstract class Comment {
private Long commentId;
private Date createdOn;
private String createdBy;
private String commentText;
...
}
public class PersonComment extends Comment {
private Person person;
...
}
public class VehicleComment extends Comment {
private Vehicle vehicle;
...
}
So how do we map these classes using Hibernate? Since all instances of a Comment will be stored in the COMMENTS table,
we use the "Table per class hierarchy" mapping strategy. The COMMENT_TYPE column will be the discriminator that tells
Hibernate what concrete type to construct when querying for Comment objects. Here is the main part of the mapping file, Comment.hbm.xml,
that maps the PersonComment and VehicleComment classes to the COMMENTS table:
<hibernate-mapping>
<class name="Comment" table="COMMENTS">
<id name="commentId" column="COMMENT_ID"> ... </id>
<discriminator column="COMMENT_TYPE"/>
<property name="createdOn" column="CREATED_ON"/>
<property name="createdBy" column="CREATED_BY"/>
<property name="commentText" column="COMMENT_TEXT" type="clob"/>
<subclass name="PersonComment" discriminator-value="P">
<many-to-one name="person" class="Person" not-null="true">
<column name="PERSON_ID"/>
</many-to-one>
</subclass>
<subclass name="VehicleComment" discriminator-value="V">
<many-to-one name="vehicle" class="Vehicle" not-null="true">
<column name="VEHICLE_ID"/>
</many-to-one>
</subclass>
</class>
</hibernate-mapping>
Notice that there is no Java property that maps to the discriminator column. Once we specify it in the subclass tag, we can forget
about it since Hibernate takes care of populating it in the database. The collection mappings for the Person and Vehicle
classes are straightforward. Here is the one for Person:
<set name="comments" inverse="true">
<key><column="PERSON_ID"/></key>
<one-to-many class="PersonComment"/>
</set>
One very nice feature of this mapping strategy is the way queries work. If you are using HQL, the query "from Comment where ..." will
return a list of Comment objects of varying concrete types, either PersonComment or VehicleComment depending
on the value of the discriminator column in the corresponding row. When iterating through this list, you can use the instanceof keyword
to determine whether the object has a getPerson() or getVehicle() method. This will allow you to get at the details of the
entity the comment is about.
A while back I tried this mapping strategy using Hibernate 2, but it didn't give me predictable results. With Hibernate 3 it works well.

