Почему Lombok на @Entity — не «бесплатный сахар»
Lombok ускоряет бойлерплейт, но на JPA-сущностях слепое @Data / @EqualsAndHashCode / @ToString часто ломает сессию Hibernate, кэш и отладку. Классические симптомы: LazyInitializationException при логировании, бесконечная рекурсия в toString, дубликаты в Set, странное поведение dirty checking.
Правило простое: генерируйте только то, что осознанно нужно, и исключайте ассоциации и ленивые поля из equals/hashCode/toString.
Антипример: @Data на сущности
@Entity
@Data // = getter/setter + equals/hashCode + toString по ВСЕМ полям
public class Order {
@Id
@GeneratedValue
private Long id;
private String number;
@ManyToOne(fetch = FetchType.LAZY)
private Customer customer;
@OneToMany(mappedBy = "order", fetch = FetchType.LAZY)
private List<OrderItem> items = new ArrayList<>();
}
Что пойдёт не так:
toString()трогаетcustomerиitems→ запросы вне сессии или N+1;equals/hashCodeзависят от мутабельных коллекций и прокси;- двусторонние связи легко дают
StackOverflowErrorв toString/equals.
Пример 1: toString и LazyInitializationException
// В сервисе после закрытия транзакции:
log.info("Saved order {}", order);
// Lombok toString() лезет в items → LazyInitializationException
Исключите ассоциации из toString:
@Entity
@Getter
@Setter
@ToString(onlyExplicitlyIncluded = true)
public class Order {
@Id
@GeneratedValue
@ToString.Include
private Long id;
@ToString.Include
private String number;
@ManyToOne(fetch = FetchType.LAZY)
private Customer customer; // не в toString
@OneToMany(mappedBy = "order")
@ToString.Exclude
private List<OrderItem> items = new ArrayList<>();
}
Альтернатива: @ToString(exclude = {"customer", "items"}).
Пример 2: equals/hashCode и Set<Entity>
Если equals строится по всем полям или по id, который null до flush:
Set<OrderItem> items = new HashSet<>();
OrderItem a = new OrderItem(); // id == null
OrderItem b = new OrderItem(); // id == null
items.add(a);
items.add(b); // при equals только по id оба «равны» → потеря элемента
Практичный вариант для JPA — бизнес-ключ или стабильный UUID, назначенный в конструкторе:
@Entity
@Getter
@Setter
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
public class OrderItem {
@Id
@GeneratedValue
private Long id;
@EqualsAndHashCode.Include
@Column(nullable = false, unique = true, updatable = false)
private UUID businessId = UUID.randomUUID();
@ManyToOne(fetch = FetchType.LAZY)
@EqualsAndHashCode.Exclude
private Order order;
private BigDecimal price;
}
Не включайте в equals ленивые @ManyToOne/@OneToMany — сравнение начнёт грузить граф или сравнивать прокси непредсказуемо.
Пример 3: двусторонняя связь и рекурсия
@Entity
@Data
public class Customer {
@OneToMany(mappedBy = "customer")
private List<Order> orders;
}
@Entity
@Data
public class Order {
@ManyToOne
private Customer customer;
}
System.out.println(customer);
// customer.toString → orders → order.toString → customer → StackOverflowError
Всегда помечайте обратную сторону:
@ToString.Exclude
@EqualsAndHashCode.Exclude
@OneToMany(mappedBy = "customer")
private List<Order> orders;
Пример 4: @Builder и обязательный no-args конструктор
JPA требует конструктор без аргументов. Связка с Lombok:
@Entity
@Getter
@Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Builder
public class Product {
@Id
@GeneratedValue
private Long id;
private String sku;
private String name;
}
Без @NoArgsConstructor Hibernate не сможет инстанцировать entity. PROTECTED оставляет конструктор для ORM, но не светит его в публичном API.
Пример 5: что ставить вместо @Data
@Entity
@Table(name = "orders")
@Getter
@Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@ToString(onlyExplicitlyIncluded = true)
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@ToString.Include
@EqualsAndHashCode.Include
private Long id; // ок, если equals нужен только для уже persisted;
// иначе лучше UUID business key
@ToString.Include
private String number;
@ManyToOne(fetch = FetchType.LAZY, optional = false)
private Customer customer;
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
@ToString.Exclude
@EqualsAndHashCode.Exclude
private List<OrderItem> items = new ArrayList<>();
}
Ещё жёстче: вообще не генерировать equals/hashCode Lombok'ом и написать их вручную по бизнес-ключу — так проще ревьюить.
Чеклист перед merge entity-класса
- Нет
@Dataна@Entity/@MappedSuperclass. - Есть
@ToString.Exclude(или onlyExplicitlyIncluded) на всех ассоциациях. equals/hashCodeне тянут коллекции и LAZY-поля.- Есть no-args конструктор для JPA.
- Логи и отладка не вызывают полный граф объекта «случайно».
Вывод
Lombok полезен на entity, если включать его точечно: getters/setters, аккуратный toString, equals по стабильному ключу. Проблемы начинаются, когда в equals/hashCode/toString попадают поля, которые не должны туда попадать — ассоциации, ленивые коллекции, двусторонние ссылки. Делайте исключения явно (@ToString.Exclude, @EqualsAndHashCode.Exclude) — это дешевле, чем ловить LazyInitializationException и StackOverflow на проде.
