Как переопределять equals в сущностях и зачем
Что проверяют
Спрашивают не про контракт equals из учебника, а про сущности JPA: почему реализация по всем полям для них опасна и что делать с идентификатором, который появляется только после persist. Следом обычно звучит вопрос про Lombok на сущностях.
Как отвечать
Сначала зачем. Внутри одного persistence context Hibernate гарантирует, что одна строка это один объект, и сравнения по ссылке хватает. Проблемы начинаются, когда объекты из разных сессий или detached-сущности попадают в Set, сравниваются после merge или живут в связи @OneToMany типа Set.
Контракт стандартный: рефлексивность, симметричность, транзитивность, согласованность, x.equals(null) == false, и равные объекты обязаны иметь одинаковый hashCode. Для сущности добавляется требование: результат не должен меняться при переходе между состояниями transient, managed и detached.
Рабочие варианты:
- натуральный ключ: бизнес-поле, уникальное и неизменяемое, например ИНН или артикул. В Hibernate его помечают
@NaturalId. Лучший вариант, если такой ключ есть; - по идентификатору:
equalsсравниваетidи возвращаетfalse, если онnull, аhashCodeвозвращает константу, напримерgetClass().hashCode(). Хэш-коллекции от этого медленнее, зато объект не теряется вHashSetпосле сохранения; - UUID из приложения: присваивается в конструкторе, поэтому известен до
persist, иequalsсhashCodeпо нему безопасны.
Тип проверяют через instanceof или Hibernate.getClass(), а не getClass(): ленивая загрузка подставляет прокси-подкласс, и сравнение классов даст false.
Где спотыкаются
Ставят на сущность @Data или @EqualsAndHashCode из Lombok. В сравнение попадают ленивые связи: лишние запросы, LazyInitializationException, а при двусторонних связях StackOverflowError.
Считают hashCode по сгенерированному id. До сохранения он null, после появляется, и объект, положенный в HashSet раньше, больше не находится.
Читают поля другого объекта напрямую: у неинициализированного прокси other.name равно null, обращаться нужно через геттеры.