Что рекомендует Hibernate по equals и hashCode в entity классах
Что проверяют
Понимаете ли вы, что у сущности три разные идентичности: ссылка в памяти, идентификатор строки в базе и бизнес-ключ. Вопрос быстро показывает, работали ли вы с коллекциями сущностей или только с плоскими выборками.
Как отвечать
Главное правило: equals и hashCode должны давать одинаковый результат на всём пути объекта, от только что созданного до отсоединённого. Сгенерированный базой идентификатор этому мешает: до сохранения он null, после вставки появляется, и объект, уже лежащий в HashSet, теряется, потому что его хеш изменился.
Отсюда рекомендации. Есть настоящий бизнес-ключ, неизменный и уникальный, стройте оба метода на нём. Нет такого ключа, берите идентификатор, присвоенный самим приложением, например UUID в конструкторе. Если невозможно и это, оставьте хеш постоянным, а equals стройте на идентификаторе с проверкой на null.
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof User other)) return false;
return id != null && id.equals(other.getId());
}
@Override
public int hashCode() {
return getClass().hashCode();
}Постоянный хеш выглядит странно, но он законен: контракт требует лишь стабильности и совпадения хешей у равных объектов. Набор сущностей одного типа в памяти обычно мал, и вырождение корзины хеш-таблицы в список тут не проблема.
Где спотыкаются
Строят hashCode на сгенерированном идентификаторе. Сохранение объекта, добавленного в множество до вставки, ломает contains, и дефект всплывает далеко от места ошибки.
Сравнивают классы через getClass и получают false для ленивого прокси: Hibernate подсовывает сгенерированный подкласс. Нужен instanceof или Hibernate.getClass.
Читают поле другого объекта напрямую вместо геттера. У неинициализированного прокси поля пусты, инициализацию запускает именно вызов метода, поэтому сравнение по полю молча возвращает неверный ответ.