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.

  1. Eager fetching of collections
  2. 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:

  1. Create your own toString method, Lombok will then backoff generating a toString
    @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);      
     }
    }
    
  2. 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.

  1. Always write your own equals and hashCode 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.
  2. Be cautious in what to include in the generated toString. You might want to include a default toString in a base entity class and disable generating a toString using @ToString(onlyExplicitlyIncluded=true).
  3. 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.