Hibernate with Business Keys
A common practice with Hibernate is to give each entity an id field to store its primary key.
This field is generally managed by Hibernate.
When the entity has not been saved yet, the field is null, which is also how Hibernate detects the object has not been saved yet.
Once it is saved, Hibernate sets the value of the id to the generated value.
Since the value of the id field changes, it is not suited to base your equals and hashCode methods on.
Which is a bit counter-intuitive, because it corresponds to the primary key in the database.
The solution is to use Business Keys.
Data Model
Suppose you have the following data model:
With the following Java code:
import jakarta.persistence.Column;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.MappedSuperclass;
@MappedSuperclass
public abstract class BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id", updatable = false, nullable = false)
private Long id;
public Long getId() {
return id;
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null) return false;
if (obj == null || !(obj instanceof BaseEntity)) return false;
BaseEntity other = (BaseEntity) obj;
if (other.getId() == null) return false;
return other.getId().equals(getId());
}
@Override
public int hashCode() {
return getId() != null ? getId().hashCode() : 0;
}
}
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.OneToMany;
@Entity
public class Company extends BaseEntity{
@Column(name = "name", length = 100, nullable = false)
public String name;
@OneToMany(mappedBy = "company", cascade = CascadeType.ALL, orphanRemoval = true)
public Set<Employee> employees;
}
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.ForeignKey;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
@Entity
public class Employee extends BaseEntity {
@Column(name = "first_name", length = 100, nullable = false)
public String firstName;
@Column(name = "last_name", length = 100, nullable = false)
public String lastName;
@Column(name = "email", length = 100, nullable = false)
public String email;
@ManyToOne
@JoinColumn(foreignKey = @ForeignKey(name = "fk_employee_company"))
public Company company;
}
The Problem
This code can give weird problems.
Suppose you have a company object of type Company, and you add a new Employee object to company.employees.
Since the new object, employee, is new employee.id == null.
Next you save the company and the new Employee object.
Its id now gets a value from Hibernate.
However, employee was already added to the set company.employees with a different id.
But hashCode and equals now yield different values.
As a result Java/Hibernate may not be able to find the corresponding object in the set anymore.
This is why it is impossible to create a consistent hashCode and equals method based upon id.
The Solution
Fortunately there is a solution that does give consistent behavior both in your code and the database, by using Business Keys.
Refactor BaseEntity like this:
import jakarta.persistence.Column;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.MappedSuperclass;
@MappedSuperclass
public abstract class BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id", updatable = false, nullable = false)
private Long id;
// Immutable ID, set upon 'new', independent from DB
@Column(name = "business_key", nullable = false, unique = true, updatable = false)
private String businessKey = UUID.randomUUID().toString();
public Long getId() {
return id;
}
public String getBusinessKey() {
return businessKey;
}
@Override
public final boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || !(obj instanceof BaseEntity)) return false;
BaseEntity other = (BaseEntity) obj;
return Objects.equals(this.getBusinessKey(), other.getBusinessKey());
}
@Override
public final int hashCode() {
return Objects.hashCode(getBusinessKey());
}
}
With this code the value of the Business Key is always the same both for unsaved and saved entities, and its value in the code is always consistent with that in the database.
Therefore, it is recommended to always use business keys to avoid inconsistent (difficult-to-debug) behavior.