Hibernate Facts: Equals and HashCode
Every Java object inherits the equals and hashCode methods, yet they are useful only for Value objects, being of no use for stateless behavior oriented objects.
While comparing references using the “==” operator is straight forward, for object equality things are a little bit more complicated.
Since you are responsible for telling what equality means for a particular object type, it’s mandatory that your equals and hashCode implementations follow all the rules specified by the java.lang.Object JavaDoc (equals() and hashCode()).
It’s also important to know how your application (and its employed frameworks) make use of these two methods.
Fortunately Hibernate doesn’t require them for checking if the Entities have changed, having a dedicated dirty checking mechanisms for this purpose.
After browsing Hiberante documentation I stumbled on these two links: Equals and HashCode and Hiberante 4.3 docs pointing out the contexts where two methods are required:
- when adding entities to Set collections
- when reattaching entities to a new persistence context
These requirements arise from the Object.equals “consistent” constraint, leading us to the following principle:
An entity must be equal to itself across all JPA object states:
- transient
- attached
- detached
- removed (as long as the object is marked to be removed and it still living on the Heap)
Therefore we can conclude that:
- We can’t use an auto-incrementing database id for comparing objects, since the transient and the attached object versions won’t be equal to each other.
- We can’t rely on the default Object equals/hashCode implementations, since two entities loaded in two different persistence contexts will end up as two different Java objects, therefore breaking the all-states equality rule.
- So if Hibernate uses the equality to uniquely identify an Object, for its whole lifetime, we need to find the right combination of properties satisfying this requirement.
Those entity fields having the property of being unique in the whole entity object space are generally called a business key.
The business key is also independent of any persistence technology employed in our project architecture, as opposed to a synthetic database auto incremented id.
So, the business key must be set from the very moment we are creating the Entity and then never change it.
Lets take several examples of Entities in relation to their dependencies and choose the appropriate business key.
- Root Entity use case (an entity without any parent dependency)
This is how the equals/hashCode are implemented:
@Entity public class Company { @Id @GeneratedValue(strategy = GenerationType.AUTO) private Long id; @Column(unique = true, updatable = false) private String name; @Override public int hashCode() { HashCodeBuilder hcb = new HashCodeBuilder(); hcb.append(name); return hcb.toHashCode(); } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (!(obj instanceof Company)) { return false; } Company that = (Company) obj; EqualsBuilder eb = new EqualsBuilder(); eb.append(name, that.name); return eb.isEquals(); } }
The name field represents the Company business key, and therefore it’s declared unique and non-updatable. So two Company objects are equal if they have the same name, ignoring any other fields it may contain.
- Children entities with an EAGER fetched parent
@Entity public class Product { @Id @GeneratedValue(strategy = GenerationType.AUTO) private Long id; @Column(updatable = false) private String code; @ManyToOne(fetch = FetchType.EAGER) @JoinColumn(name = "company_id", nullable = false, updatable = false) private Company company; @OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL, mappedBy = "product", orphanRemoval = true) @OrderBy("index") private Set images = new LinkedHashSet(); @Override public int hashCode() { HashCodeBuilder hcb = new HashCodeBuilder(); hcb.append(name); hcb.append(company); return hcb.toHashCode(); } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (!(obj instanceof Product)) { return false; } Product that = (Product) obj; EqualsBuilder eb = new EqualsBuilder(); eb.append(name, that.name); eb.append(company, that.company); return eb.isEquals(); } }
In this example we are always fetching the Company for a Product, and since the Product code is not unique among Companies we can include the parent entity in our business-key. The parent reference is marked as non up-datable, to prevent breaking the equals/hashCode contract (moving a Product from one Company to another won’t make sense anyway). But this model breaks if the Parent has a Set of Children entities, and you call something like:
public void removeChild(Child child) { children.remove(child); child.setParent(null); }
This will break the equals/hashCode contract since the parent was set to null, and the child object won’t be found in the children collection, if that were a Set. So be careful when using bidirectional associations having Child entities using this type of equals/hashCode.
- Children entities with a LAZY fetched parent
@Entity public class Image { @Id @GeneratedValue(strategy = GenerationType.AUTO) private Long id; @Column(updatable = false) private String name; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "product_id", nullable = false, updatable = false) private Product product; @Override public int hashCode() { HashCodeBuilder hcb = new HashCodeBuilder(); hcb.append(name); hcb.append(product); return hcb.toHashCode(); } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (!(obj instanceof Image)) { return false; } Image that = (Image) obj; EqualsBuilder eb = new EqualsBuilder(); eb.append(name, that.name); eb.append(product, that.product); return eb.isEquals(); } }
If the Images are fetched without the Product and the Persistence Context is closed, and we load the Images in a Set, we will get a LazyInitializationException like in the following code example:
List images = transactionTemplate.execute(new TransactionCallback<List>() { @Override public List doInTransaction(TransactionStatus transactionStatus) { return entityManager.createQuery( "select i from Image i ", Image.class) .getResultList(); } }); try { assertTrue(new HashSet(images).contains(frontImage)); fail("Should have thrown LazyInitializationException!"); } catch (LazyInitializationException expected) { }
Therefore I wouldn’t recommend this use case since it’s error prone and to properly use the equals and hashCode we always need the LAZY associations to be initialized anyway.
- Children entities ignoring the parent
In this use case we simply drop the parent reference from our business key. As long as we always use the Child through the Parent children collection we are safe. If we load children from multiple parents and the business key is not unique among those, then we shouldn’t add those to a Set collection, since the Set may drop Child objects having the same business key from different Parents.
Conclusion
Choosing the right business key for an Entity is not a trivial job, since it reflects on your Entity usage inside and outside of Hibernate scope. Using a combination of fields that’s unique among Entities is probably the best choice for implementing equals and hashCode methods.
Using EqualsBuilder and HashCodeBuilder helps us writing concise equals and hashCode implementations, and it seems to work with Hibernate Proxies too.
In the class Product “code” should be added to the HashCodeBuilder instead of “name”. The class does not have a property “name”.
Thanks, I updated my original post but I can;t do anything about the JCG article, since the original authors don’t have the access rights to edit posts.