javahibernateequalshashcodenaturalid

Using Primiry Key (id) while overriding equals() and hashCode() methods


I am really too confused with the equals() and hashCode() methods after reading lots of documentation and articles. Mainly, there are different kind of examples and usages that makes me too confused.

So, could you clarify me about the following points?

1. If there is not any unique field in an entity (except from id field) then should we use getClass() method or only id field in the equals() method as shown below?

@Override
public boolean equals(Object o) {
   if (this == o) return true;
   if (getClass() != o.getClass()) return false;
   
   // code omitted
}

2. If there is a unique key e.g. private String isbn;, then should we use only this field? Or should we combine it with getClass() as shown below?

@Override
public boolean equals(Object o) {
   if (this == o) return true;
   if (getClass() != o.getClass()) return false;
   Book book = (Book) o;
   return isbn == book.isbn;
}

3. What about NaturalId? As far as I understood, it is used for unique fields e.g. private String isbn;. What is the purpose of its usage? Is it related to equals() and hashCode() methods?


Solution

  • getClass()

    In regard to the usage of getClass() everything is straightforward.

    Method equals() expects an argument of type Object.

    It's important to ensure that you're dialing with an instance of the same class before performing casting and comparing attributes, otherwise you can end up with a ClassCastException. And getClass() can be used for that purpose, if objects do not belong to the same class they are clearly not equal.

    Natural Id vs Surrogate Id

    When you're talking about "NaturalId" like ISBN-number of a book versus "id", I guess you refer to a natural key of a persistence entity versus surrogate key which is used in a relational database.

    There are different opinions on that point, the general recommended approach (see a link to the Hibernate user-guide and other references below) is to use natural id (a set of unique properties, also called business keys) in your application and ID which entity obtains after being persisted only in the database.

    You can encounter hashCode() and equals() that are implemented based on surrogate id, and making a defensive null-check to guard against the case when an entity is in transient state and its id is null. According to such implementations, a transient entity would not be equal to the entity in persistent state, having the same properties (apart from non-null id). Personally, I don't think this approach is correct.

    The following code-sample has been taken from the most recent official Hibernate 6.1 User-Guide

    Example 142. Natural Id equals/hashCode

    @Entity(name = "Book")
    public static class Book {
    
        @Id
        @GeneratedValue
        private Long id;
        private String title;
        private String author;
    
        @NaturalId
        private String isbn;
    
        //Getters and setters are omitted for brevity
    
        @Override
        public boolean equals(Object o) {
            if (this == o) {
                return true;
            }
            if (o == null || getClass() != o.getClass()) {
                return false;
            }
            Book book = (Book) o;
            return Objects.equals(isbn, book.isbn);
        }
    
        @Override
        public int hashCode() {
            return Objects.hash(isbn);
        }
    }
    

    The code provided above that makes use of business-keys is denoted in the guide as a final approach in contrast to implementation based on the surrogate keys, which is called a naive implementation (see Example 139 and further).

    The same reasoning for the choice ID vs Natural key has been described here:

    You have to override the equals() and hashCode() methods if you

    • intend to put instances of persistent classes in a Set (the recommended way to represent many-valued associations) and

    • intend to use reattachment of detached instances

    Hibernate guarantees equivalence of persistent identity (database row) and Java identity only inside a particular session scope. So as soon as we mix instances retrieved in different sessions, we must implement equals() and hashCode() if we wish to have meaningful semantics for Sets.

    The most obvious way is to implement equals()/hashCode() by comparing the identifier value of both objects. If the value is the same, both must be the same database row, they are therefore equal (if both are added to a Set, we will only have one element in the Set). Unfortunately, we can't use that approach with generated identifiers! Hibernate will only assign identifier values to objects that are persistent, a newly created instance will not have any identifier value! Furthermore, if an instance is unsaved and currently in a Set, saving it will assign an identifier value to the object. If equals() and hashCode() are based on the identifier value, the hash code would change, breaking the contract of the Set. See the Hibernate website for a full discussion of this problem. Note that this is not a Hibernate issue, but normal Java semantics of object identity and equality.

    We recommend implementing equals() and hashCode() using Business key equality.

    For more information, have a look at this recent (Sep 15, 2021) article by @Vlad Mihalcea on how to improve caching query results with natural keys The best way to map a @NaturalId business key with JPA and Hibernate, and these questions: