The JPA Hashcode()/Equals() Dilemma

Spring Data JPA save() method not following the hashcode/equals contract

According to this answer, https://stackoverflow.com/a/11881628/5695673, you probably have 2 records because your fields employeeId are differents for your entities, so for spring-data the two entities are differents.

To test if your equals()/hashCode() works as expected, you can try to put your entities in a collection which check object equality (i.e. a Set) and try to save all the set in one time.

Example:

Set<Employee> employees = new HashSet<Employee>();
employees.add( new Employee("111"));
employees.add( new Employee("111"));

System.out.println(employees.size()); //1
employeeRepo.save(employees); // 1 record

More information here: https://developer.jboss.org/wiki/EqualsandHashCode?_sscc=t

Should I write equals() and hashCode() methods in JPA entities?

Not necessarily. There are three options:

  • don't override - thus you will be working with instances. This is fine in cases when you are working with the collections with only entities that are attached to the session (and hence guaranteed to be the same instance). This is (for me) the preferred way in many cases, because it requires less code and less consideration when overriding

  • override hashCode() and equals() with a business key. That may be a subset of properties that identify the entity. For example, for a User a good business key might be the username or the email. This is considered good practice.

  • override hashCode() and equals() using the ID field only. This is fine in some cases, especially if you have a manually-assigned identifier (like an UUID). It is also fine if your entity will never go into a collection. But for transient entities (with no identifier) that go into collections, it causes problems, so careful with this option. As seanizer noted - you should avoid it. Generally, always, unless you are really aware of what you are doing (and perhaps documenting it)

See this article for more details. Also note that equals()and hashCode() are tied and should be implemented both with exactly the same fields.

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

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:

  • The JPA hashCode() / equals() dilemma

  • Should the id field of a JPA entity be considered in equals and hashCode?

implementation of equals and hashCode on recursive objects (JPA @ManyToMany)

In my opinion, the best practice would be to compare two entities by comparing their unique IDs. A user does not become a different user by entering a new group. much less, by changing one of the groups she belongs to.

domain specific comparisons could be various, and should be implemented in seperate comparators.

Using auto generated id of Hibernate entity object in the equals and hashcode methods

You should read Equals and HashCode on the Hibernate Community Wiki.

The main reason for not using the database identifier in equals, and by implication, hashCode is for dealing with stored, but not persisted, entities. Prior to persistence all you instances are going to be equal, unless you take care to handle that case explicitly.

If you know that you're not going to be in this scenario, and you make sure it's well documented, you may well be OK. You can always change the implementations later.

Different hashCode() in different persistence context?

If we retrieve the same entity in the same session then the
hashCode() is same.

It is expected as the first level cache of Hibernate (the Session here) keeps the entities loaded inside the transaction in cache for the transaction lifespan.

The entity is not retrieved a second time, it is just retrieved from the cache.

Each session retrieves the same entity from the table. When i print
their hashCode() they are different.

As loaded entities are not shared between sessions, it means that you didn't override hashCode() for the entity.

So to guarantee the same hashCode() and also their equality (equals()), override equals()/hashCode() if it makes sense.



Related Topics



Leave a reply



Submit