torsdag 8 november 2007

Domain objects, equals and hash code

A good place to start a project that is going to use Hibernate is with the domain objects. The domain objects are the objects containing your actual data. So if your are developing software for a library these would be the classes named Book and Author an so on.

These are just Plain Old Java Objects (POJOs) right? Well yes sure but it's a little bit more complicated than what it sounds like. The Hibernate books say that you should have constructors, getters and setters. That's not problematic of course. They also say that I should implement my own equals and hashcode methods. Those are a little bit more trixy but lets take the problems more or less in the order I stumbled on them.

My fist attempt was using Eclipse's generated equals and hashcode methods. Just one little problem. The code Eclipse generates for you will not work with Hibernate. At least not in the way I use Hibernate. There are two main reasons for this: When using lazy loading (which I have a whole lot of other troubles with getting to work) all calls to need to be changed inte other.getFoo() since foo is loaded lazily on a getFoo call and is null otherwise. But the much bigger problem is that HashSet's won't work in the expected way.

Remember that when an object is put into a HashSet the hashCode method is used in order to figure out which hashbucket to place the object in. The problem is that later on when I change something on my object, of course that also changes the objects hashcode and this results in the contains-method returning false even when given a reference to an object which actually is in the set. It will simply look in the wrong hash bucket.

It took me a very long time to realize this since the problem only turns up if the object has been altered after it has been put in the hashset and I also more or less also needed to run the contains method in order to stumble upon the problem. So when I figured it all out I was already long into the project. I found the explanation on a hibernate page.
I think the page has changed a bit since I stumbled upon it but it is full of wisdom. It is also full of more or less working ideas on how to work around this problem. What I finally did in my project was to make sure the hashcode was always the same by actually persisting the objects original hashcode in the database. Although this isn't a very nice solution it works and the hashcode becomes like a persisted field there only to speed up searches for example when a domain object is in a hashset.

However in a new ORM using project I have started, which is using iBatis instead of Hibernate, I use another approach instead which I will describe here and that is the one I would truly recommend.

What I base this approach on is that there are actually three different sorts of equality when dealing with objects mapped from a database.
  • The trivial object identity that is == meaning that two references are to the actual same object.
  • The equality meaning that two object have all properties equal to each other. This corresponds to the equals method generated by Eclipse.
  • The these-objects-refer-to-the-same database row equality. This is not the same thing as equality since you can have two different objects loaded from the same database row in memory at the same time and you can change one and leave the other.
The third equality type is only a problem in Hibernate if you have objects that is not connected to an active session. (By the way this sessions are near the root of almost all my Hibernate problems but more about that some other time).

So let's make methods for these equality types. Well the first one hardly needs a method but the second one does. I choose to call it hasValuesEqualTo because what it does is not copatible with what we want our equals method to do (which must work well with the hashcode). So, yes the equals method will correspond to the same row in the database equality. That makes sence since you don't want multiple references to the same database row in your hashset. In order to do this I used java's unique id generator and implemented equals and hashcode as final methods in a base class. I save the id as a string and equals simple runs equals on this string and the hashcode method simply returns the hashcode for the id. Then there is the hasValuesEqualTo left which is like a common equals method. Currently the code for my baseobject can be found here. So far this solution works for me.