How to implement Equals and HashCode for JPA entities

Imagine having a tool that can automatically detect JPA and Hibernate performance issues. Wouldn’t that be just awesome?

Well, Hypersistence Optimizer is that tool! And it works with Spring Boot, Spring Framework, Jakarta EE, Java EE, Quarkus, or Play Framework.

So, enjoy spending your time on the things you love rather than fixing performance issues in your production system on a Saturday night!

Introduction

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 straightforward, for object equality things are a little bit more complicated.

Requirements

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 mechanism for this purpose.

The Hibernate documentation lists the situations when these two methods are required:

  • when adding entities to Set collections
  • when reattaching entities to a new persistence context

These requirements arise from the Object.equalsconsistent” 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 in the hashCode method since the transient and the attached object versions will no longer be located in the same hashed bucket.
  • We can’t rely on the default Object equals and 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.

Business key equality

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.

Let’s 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(code);
        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(code, that.code);
        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-updatable, 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) {
    child.setParent(null);
    children.remove(child);
}

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’s 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();
    }
});

//Throws LazyInitializationException

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.

I'm running an online workshop on the 20-21 and 23-24 of November about High-Performance Java Persistence.

If you enjoyed this article, I bet you are going to love my Book and Video Courses as well.

Conclusion

If you want to use the entity identifier when implementing equals and hashCode, then check out this post for how to do it properly.

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 are 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.

Transactions and Concurrency Control eBook

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.