Why I Can't Use Java Records as JPA Entities
Source: Dev.to
Introduction
With the introduction of Java Records (officially in Java 16), developers finally got a concise way to create immutable data‑carrier classes without writing boilerplate code such as equals(), hashCode(), and toString().
Naturally, many of us looked at our verbose JPA entities and wondered:
“Can I replace these with records to make my code cleaner?”
The short answer is no.
Why Records Are Incompatible with JPA
Immutability vs. Mutability
- Record philosophy – All fields are implicitly
final; once an instance is created its state cannot change. - JPA requirement – Entities must be mutable. JPA manages the state of an entity by loading it from the database, allowing fields to be changed, and then persisting those changes.
Key JPA mechanisms that rely on mutability:
- Dirty checking – Hibernate compares the current state of an object with its original snapshot.
- Setters – JPA uses setters (or field access) to populate entity fields when reading from the database.
Since records lack setters and have final fields, JPA cannot manage their lifecycle or update their state.
Lazy Loading and Proxies
One of JPA’s most powerful features is lazy loading, which fetches related data only when it is accessed. To achieve this, providers such as Hibernate generate proxy classes that:
- Extend the original entity class.
- Override getter methods to trigger a database call on demand.
Records are final classes, which means they cannot be subclassed. Consequently, JPA cannot create proxies for records, and lazy loading (as well as other proxy‑based features) breaks completely.
Alternatives: Keep Entities Concise with Lombok
While records are unsuitable for JPA entities, you can still reduce boilerplate by using Lombok with regular classes.
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.GeneratedValue;
import lombok.Getter;
import lombok.Setter;
import lombok.NoArgsConstructor;
import lombok.AccessLevel;
@Entity
@Getter
@Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED) // limit access to the default constructor
public class User {
@Id
@GeneratedValue
private Long id;
private String name;
private String email;
}
Note: The following record definition would cause issues with JPA because records are immutable and final.
// This will cause issues with JPA!
public record User(Long id, String name, String email) {}
Conclusion
- Use standard classes (optionally with Lombok) for JPA entities to satisfy mutability, default constructors, and non‑final class requirements.
- Use records for immutable data carriers such as DTOs, where JPA’s persistence features are not needed.