« Struts Multiboxes | Main | Warped Passages »

Sorting Collections in Hibernate with Comparator

This one had me stumped for a while, until I was enlightened by others and am now feeling comfortable enough to write up some notes for others - including the future me who will clearly forget all this.

The challenge I faced was how to order a collection in moderately complex cases - e.g. not just based on fields in that table, but using values in associated tables. Turning to the bible (Hibernate in Action) there is a writeup on page 215 as to how to do this, but it is meant for people who are more Java literate than I.

The book says that you will need to write a class that implements java.util.Comparator and use the name of that class in the sort attribute. For example:
           cascade="all-delete-orphan"
      name="ChildInCampWeekSet"
      table="ChildInCampWeek"
      sort="foo.ChildInCampWeekComparator"  
      inverse="true"
    >
...
    

But the naive reader will miss two important items, which were nicely pointed out in this post at the Hibernate Forums. First, the class must be an inner class and second, the class must be static.

So, I changed my CampWeek.hbm file to have:
           cascade="all-delete-orphan"
      name="ChildInCampWeekSet"
      table="ChildInCampWeek"
      sort="camp.ChildInCampWeek$ChildInCampWeekComparator"  
      inverse="true"
    >
      
      
    

And then, I implemented the following inner class (e.g. a class within another class) in my CampWeek.java file:
  public static class CampWeekComparator implements Serializable, Comparator {
    public CampWeekComparator() {
   }
    public int compare(Object campWeek1, Object campWeek2) {
      int result = 0;
      Date startDate2 = ((CampWeek)campWeek2).getWeek().getStartDate();
      Date thisStartDate = ((CampWeek)campWeek1).getWeek().getStartDate();
      if (thisStartDate.after(startDate2)) {
        result = 1;
      } else if (thisStartDate.before(startDate2)) {
        result = -1;
      } else {
        // Use names to break the tie
        String campName2 = ((CampWeek)campWeek2).getCamp().getName();
        String thisCampName = ((CampWeek)campWeek1).getCamp().getName();
        result = thisCampName.compareTo(campName2);
      }
      return result;
      
    }
  }

This generally solved my issue - but recently I discovered some additional mistakes I had made which merit some mention.
1. Do you need to use SortedSets/TreeSets when dealing with the set?
2. Does the parent class need to implement Comparable?
3. What method is used to test for equality when you add new items into the set?

Covering each one of these in order....
1. Do you need to use SortedSets/TreeSets when dealing with the set?
As with most questions, the answer is "it depends". For example, when adding CampComments to a camp, I get the already created camp out of the database and it is hydrated with an underlying sorted set. All I needed to do was to add my new CampComment to the set and it all worked, e.g.:
camp.addToCampCommentSet(campComment);
without ever having to worry about the type of Set. However, when I create a camp and need to set up a sorted Set of CampWeeks, I need to explicity create a new TreeSet and add the new CampWeek objects to that set. If I save the newly created Camp and then rehydrate it from Hiberate, then, the Set seems to magically know that it is sorted and I can use the simple camp.addToCampWeekSet() approach. But it seemed wrong to save the Camp and then reload it only to avoid the use of a TreeSet. When creating the Camp, what I needed to do was:
Camp camp = new Camp();
// Set other properties
// Create empty ordered Set for the CampWeeks
camp.setCampWeekSet(new TreeSet());
...
CampWeek campWeek = new CampWeek();
...
camp.addToCampWeekSet(campWeek);
// Save the new camp and its CampWeeks


2. Does the parent class need to implement Comparable?
This is directly related to the above question. When I add a new CampWeek into the CampWeekSet, the system needs to determine if it is unique. To do this, there must be comparesTo() method on the CampWeek class (e.g. the outer class). So, in this case, I had to implement comparesTo() and add Serializable and Comparable to the class CampWeek. For the CampComments I didn't have to do this, since I didn't have to explicitly construct a TreeSet for them.

3. What method is used to test for equality when you add new items into the set?
My lack of understanding here led to a great deal of pain. I would try to insert two ChildInCampWeek objects into a CampWeek's Set (representing two children attending a particular week of a camp) and the Java set would only show one record. Yet the database would have two records inserted. But when I tried to get them out of the database through Hibernate, only one would show up. Even though thie hashcode and equals methods for ChildInCampWeek returned false - e.g. the two objects were not the same. Well, it turns out that when you are inserting objects into a Collection that is sorted in Hibernate, the Comparator class's compare() method is what is used, not equals/hashcode. And I had a bug in my compare() method that was easily fixed once I realized this was happening.

But now everything is working just wonderfully.....

Update (5/22/2006)...
It may not have been clear, but the discussion above applied to Hibernate 2.1. In my recent upgrade to Hibernate 3, I was suprised to discover a difference in behavior. In particular, it appeared that my Comparator, specified in the "sort=" property in the .hbm file, was not being called.

I posted this to the Hibernate forum, but no answers.

However, in some reading on the subject, I learned that the "compareTo" method for the main class (ChildInCampWeek) is the method that is used when "natural" is the sorting property (compareTo also seems to be used when I set the sort property to a class name, but that must be a bug).

In any case, since I only need one sort order, it was simpler for me to remove my inner class entirely, and simply use the compareTo method and the "natural" sort order.

I wish I knew why the inner class approach no longer works with Hibernate3, but that is still a mystery.

TrackBack

TrackBack URL for this entry:
http://landisfamily.dnsalias.org:90/cgi-bin/mt/mt-tb.cgi/27

Post a comment

(If you haven't left a comment here before, you may need to be approved by the site owner before your comment will appear. Until then, it won't appear on the entry. Thanks for waiting.)