ONJava.com -- The Independent Source for Enterprise Java
oreilly.comSafari Books Online.Conferences.

advertisement

AddThis Social Bookmark Button

Don't Let Hibernate Steal Your Identity
Pages: 1, 2, 3

You may be tempted to implement an equals() method that uses the id only if the id is set. After all, if two objects haven't been saved yet, we can assume they're different objects since they will be assigned different primary keys when they're saved to the database.



public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || !(o instanceof Person))
        return false;

    Person other = (Person)o;

    // unsaved objects are never equal
    if (id == null || other.getId() == null)
        return false;

    return id.equals(other.getId());
}

There is a hidden problem here. The Java Collections framework needs equals() and hashCode() to be based on immutable fields for the lifetime of the Collection. In other words, you can't change the value of equals() or hashCode() while the object is in a Collection. For example, this program:

Person p = new Person();
Set set = new HashSet();
set.add(p);
System.out.println(set.contains(p));
p.setId(new Long(5));
System.out.println(set.contains(p));

Prints: true false

The second call to set.contains(p) returns false because the Set can no longer find p. The Set has literally lost our object! That's because we changed the value for hashCode() while the object was inside the set.

This is a problem when you want to create domain objects that hold other domain objects in Sets, Maps, or Lists. To do so you must provide an implementation of equals() and hashCode() for each of your objects that is valid both before and after saving the objects and does not change while the objects are in memory. The Hibernate Reference Documentation (v. 3) provides this suggestion:

"Never use the database identifier to implement equality; use a business key, a combination of unique, usually immutable, attributes. The database identifier will change if a transient object is made persistent. If the transient instance (usually together with detached instances) is held in a Set, changing the hashcode breaks the contract of the Set. Attributes for business keys don't have to be as stable as database primary keys, you only have to guarantee stability as long as the objects are in the same Set." (Hibernate Reference Documentation v. 3.1.1).
"We recommend implementing equals() and hashCode() using Business key equality. Business key equality means that the equals() method compares only the properties that form the business key, a key that would identify our instance in the real world (a natural candidate key)" (Hibernate Reference Documentation v. 3.1.1).

In other words, use a natural key for equals() and hashCode(), and use a Hibernate-generated surrogate key for the object's id. This works as long as you have a relatively immutable natural key for each of your objects. However, you may not have such a key for every object type, and you may be tempted to use fields that don't change often, but could change. This is consistent with the idea that business keys don't have to be as stable as database primary keys. It is "good enough" if they don't change for the lifespan of the collection the object is in. This is a dangerous proposition, as it means your application probably won't break, but it could break if someone updates the right field under the right circumstances. There really should be a better solution, and there is.

Don't let Hibernate manage your ids.

All of the problems discussed so far derive from trying to create and maintain separate definitions of identity for objects and database rows. These problems all go away if we unify all forms of identity. That is, instead of having a database-centric ID, or an object-centric ID, we should create one universal entity-specific ID that represents the data entity and is created when the data is first entered. This universal ID can identify one unique data entity regardless of whether it is stored in a database, as an object in memory, or in any other format or medium. By using entity IDs that are assigned when the data entity is first created, we can safely return to our original definition of equals() and hashCode() that simply uses the id:

public class Person {
    // assign an id as soon as possible
    private String id = IdGenerator.createId();
    private Integer version;

    public String getId() {
        return id;
    }
    public void setId(String id) {
        this.id = id;
    }

    public Integer getVersion() {
        return version;
    }
    public void setVersion(Integer version) {
        this.version = version;
    }

    // Person-specific fields and behavior here

    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || !(o instanceof Person))
            return false;

        Person other = (Person)o;

        if (id == null) return false;
        return id.equals(other.getId());
    }

    public int hashCode() {
        if (id != null) {
            return id.hashCode();
        } else {
            return super.hashCode();
        }
    }
}

This example uses the object id as the definition of equals() and to derive hashCode(). This is much simpler. However, to make this work we need two things. First, we need a way to ensure every object has an id even before it is saved. This example assigns the id a value as soon as the id variable is declared. Second, we need a way to determine if this is a newly created object, or a previously saved object. In our original example, Hibernate checked whether the id field was null to determine if the object was new. Obviously this won't work anymore since our object id is never null. We can easily solve this by configuring Hibernate to check whether the version field, rather than the id field, is null. The version field is a much more appropriate indicator of whether your object has been previously saved.

Here is the Hibernate mapping document for our improved Person class.

<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping SYSTEM
"http://hibernate.sourceforge.net/
hibernate-mapping-3.0.dtd">

<hibernate-mapping package="my.package">

  <class name="Person" table="PERSON">

    <id name="id" column="ID">
      <generator class="assigned" />
    </id>

    <version name="version" column="VERSION"
        unsaved-value="null" />

    <!-- Map Person-specific properties here. -->

  </class>

</hibernate-mapping>

Note that the generator tag under the id has the attribute class="assigned". This tells Hibernate that we're assigning the id in our code, rather than letting it assign the id from the database. Hibernate will simply expect the id to be there even for new, unsaved objects. We've also added a new attribute to the version tag: unsaved-value="null". This tells Hibernate to look for a null version (rather than a null id) as an indicator that the object is new. We could just as easily tell Hibernate to look for a negative value as an unsaved indicator, which is useful if you prefer to use an int for your version field instead of an Integer.

Pages: 1, 2, 3

Next Pagearrow