When using a framework like Lombok it is very tempting and easy to use its @Data
annotation.
All together now: A shortcut for
@ToString
,@EqualsAndHashCode
,@Getter
on all fields,@Setter
on all non-final fields, and@RequiredArgsConstructor
! (Source: Lombok Documentation)
TL;DR You probably don’t want to use @Data
annotation with @Entity
classes but rather only use the @Getter
and @Setter
methods and implement your own equals
, hashCode
and toString
methods.
When placing the “convenient” @Data
annotation on an @Entity
annotated class that might look like a nice idea.
Give the following 2 entities, Book
and Author
lets illustrate what the challenges are.
@Data
@Entity
public class Book {
@Id
@GeneratedValue
private Long id;
private String isbn;
private String title;
@ManyToMany(cascade = { CascadeType.ALL })
@JoinTable(
name = "Book_Author",
joinColumns = { @JoinColumn(name = "author_id") },
inverseJoinColumns = { @JoinColumn(name = "book_id") }
)
private List<Author> authors;
}
@Data
@Entity
public class Author {
@Id
@GeneratedValue
private Long id;
private String name;
@ManyToMany(mappedBy="authors")
private List<Book> books;
}
As mentioned in the Lombok documentation it will generate all getters/setters, toString
, equals
and hashCode
. The main problem lies in the generation of the equals
and hashCode
implementation. However there also lies a problem in the generated toString
method.
The generated equals
and hashCode
methods
When using a technology like JPA the object identity shouldn’t change between state transitions or property updates. If the book title changes, due to a typo, it still is the same book entity. However not if you use the generated equals
and hashCode
methods as the equality (checked with equals
) and the hashcode change.
Solution
Use a base class that implements the hashCode
and equals
method, which are safe for entities. Something like Vlad Mihalcea proposes in his blog
@MappedSuperclass
public abstract class BaseEntity {
@Id
@GeneratedValue
private Long id;
@Override
public boolean equals(Object o) {
if (o == null) return false;
if (this == o) return true;
if (o instanceof BaseEntity that) {
return this.id != null && Objects.equals(this.id, that.id);
}
return false;
}
@Override
public int hashCode() {
return getClass().hashCode();
}
}
Now when Book
extends the BaseEntity
, Lombok needs to know if it needs to generate an equals
and hashCode
method. As it should backoff adding @EqualsAndHashCode(onlyExplicitlyIncluded = true)
to the Book
and Author
entity will (kind of) disable the equals
and hashCode
generation.
The generated toString
method
There might be an issue when using the default generated toString
method. Not with simple entities but when entities that reference other entities or within a collection.
The default toString
generated by Lombok includes ALL the fields in the entity. This leads to 2 possible issues when using the toString
method.
- Eager fetching of collections
- Stackover flow when trying to print using
toString
due to bi-directional dependency.
The authors
field is part of the toString
and will lead to a query, to retrieve the authors. Thus eagerly fetching the collection.
The Author
has a collection of Book
entities, which will be retrieved as well and printed to the toString
. Each Book
will have its toString
called, leading to calling toString
on each Author
again, and so on, and on, and on, and on until you get a StackOverflowError
.
Solution
There are 2 possible solutions:
- Create your own
toString
method, Lombok will then backoff generating atoString
@Data @Entity public class Book { @Id @GeneratedValue private Long id; private String isbn; private String title; @ManyToMany(cascade = { CascadeType.ALL }) @JoinTable( name = "Book_Author", joinColumns = { @JoinColumn(name = "author_id") }, inverseJoinColumns = { @JoinColumn(name = "book_id") } ) private List<Author> authors; public String toString() { return String.format("Book [id=%d, isbn=%s, title=%s]", this.id, this.isbn, this.title); } }
- Add the
@ToString
annotation and exclude all the lazy and bi-directional fields using@ToString.Exclude
.
@Data
@ToString
@Entity
public class Book {
@Id
@GeneratedValue
private Long id;
private String isbn;
private String title;
@ManyToMany(cascade = { CascadeType.ALL })
@JoinTable(
name = "Book_Author",
joinColumns = { @JoinColumn(name = "author_id") },
inverseJoinColumns = { @JoinColumn(name = "book_id") }
)
@ToString.Exclude
private List<Author> authors;
}
Conclusion
You can use the @Data
annotation from Lombok with @Entity
objects, however there are somethings you need to take into account.
- Always write your own
equals
andhashCode
methods that properly works for JPA. You might want to include these in a base entity you extend and disable generating those methods by adding@EqualsAndHashCode(onlyExplicitlyIncluded = true)
to your entities. - Be cautious in what to include in the generated
toString
. You might want to include a defaulttoString
in a base entity class and disable generating atoString
using@ToString(onlyExplicitlyIncluded=true)
. - Because of 1 and 2 you are probably better of just using the
@Getter
/@Setter
annotations instead of the@Data
annotation. As that is what generally is needed/wanted.