ORM и подводные камни реализации equals и hashCode.
Думаю каждому, кому приходилось работать в плотную с ORM (аля Hibernate, TopLink, OpenJPA, …), приходилось сталкиваться с подводными камнями при реализации классов. Тоже самое случилось и со мной, когда мне поручили искать бажину в одном из наших приложений …. окна дебага поглотили меня не на один час… Проблема заключалась в том, что при вызове утильного метода ListUtils.subtract(list1, list2) (данный метод исключает list2 из list1) на выходе получалось совсем не то что ожидалось, а получалось так потому, что лажа скрывалась в реализации метода equals. Просто не нужно в equals обрабатыать ВСЕ поля класса, ну не нужно, я Вас умоляю. В моём случае в equals обрабатывалось поле lastUpdated – о чём это говорит, думаю Вы догадываетесь. Именно по этой причине некоторые классы входящие в список list2 не исключались из списка классов list1, которые были детачены ранее. Плюс ко всему, старайтесь реализовывать метод equals таким образом, что бы он обеспечивал УНИКАЛЬНОСТЬ сущности в соответствии с теми полями, которые непосредственно обеспечивают уникальность в таблице!
Есть вопросы? – пишем комменты )) Идём дальше. Если мы используем lazy loading, мы должны знать, что во многих случаях мы работаем с объектами через динамический прокси во избежание их предварительной загрузки. Это говорит нам о том, что эти самые прокси реализованы как подклассы класса, экземпляр которого загружается, и выражение this.getClass().equals(o.getClass()) вернёт false. Рассмотрим на примере:
User saved = new User("Irina Alkaeva");
Long key = dao.save(saved);
dao.flush();
User retrieved = dao.retrieve(key);
saved.getClass().equals(retrieved.getClass());
// Will return false if User is loaded lazy
В этом случае вместо equals() спасёт instanceof User, так как именно он обеспечит правильное поведение.
Есть вопросы? – пишем комменты )) Работаем дальше с lazy loading. ORM обычно используют геттеры для лейзи лоадинга объектов, таким образом значение поля user.username будет равно null, если мы предварительно не обратимся к методу объекта user.getUsername(), поэтому принудительно советую в реализации метода equals и hashCode обращаться к полям через геттеры.
Есть вопросы? Пишем комменты )) Идём дальше. При сохранении новго объекта будет изменено его состояние. Допустим для обеспечения уникальности сущность мы используем целочисленные айдишники в качестве ключа. Когда мы создаем новый объект и затем персистим его, соответственно будет автоматически сгенерирован уникальный ключ для нашего объекта. И здесь мы должны помнить о том, что если в реализации метода hashCode задействован ключ, то значение hashCode до персистенса и после будет координально отличаться, поэтому практика не советует пихать ключи в hashCode, потому что может получиться так, что вы никогда не найдете обект, положенный ранее например в HashSet – а это лажа.



Попробую высказаться, поправьте если заблуждаюсь. По поводу equals. Любые два объекта, если им не переопределить equals являются разными, то есть уже существующий equals очень даже подходит. Кроме случаев, когда два разных объекта с логической точки зрения таки являются одинаковыми и никого не волнует что на самом деле это два разных объекта. Вот тогда и делаем override equals.
(см. выше) Судя по вашей реакции скорее всего не прав и equals реализован неправильно.
Если ваш предшественник использовал поле lastUpdated для реализации equals то он или прав или не прав
По поводу того что не надо использовать все поля для реализации equals, наверное это зависит от каждого конкретного случая. Количество полей тут ни при чем. Но конечно же, елси база была составлена правильно, то primary keys вполне описывают уникальность объекта и их наверное можно использовать для equals. Примерно так.
Согласен, но не совсем
, по порядку
>>Любые два объекта, если им не переопределить equals являются разными, то есть уже существующий equals очень даже подходит.
Согласен, но в ORM это не допустимо.
По поводу предшественника, да, но Вы знаете, я сам с этим столкнулся впервые, что и стало причиной написания даного поста
Cледует учесть, что персистентный контекст описывает совокупность объектов среди которых каждой хранимой репрезентации соответствует только один java объект. Работая в контексте достаточно как стандартного Object.equals так и сравнения ссылок (==).
Можно представить, что вместо исправления реализации equals/hashCode их вообще лучше не переопределять для @Entity.
Только для работы с детачеными объектами или комбинацией сохраненных и детаченых объектов надобно вообще реализовываеть equals и hashCode в сущностных бинах. И если уж таким заниматься, то следует реализовать так, как того требует конкретный use case.
Конечно, необходимо учитывать приведенные рекомендации относительно динамических подклассов, переопределения байткода и Lazy значениях и коллекциях.
Если объект детаченый и имеет незагруженое lazy-поле, то как тут поможет геттер в equals?
Правило что в персистентном контексте уникальность сущности можно сравнивать по ссылкам как раз было придумано для того чтобы работать с неполностью загружеными моделями.