Robert Donaway

All | General | Java | Ruby
XML
20070410 Tuesday April 10, 2007
Mapping a universal comment table using Hibernate 3.0

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.

Of course, the 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.

Posted by rdonaway Apr 10 2007, 06:00:00 PM EDT