Как должны быть реализованы равенства и хэш-код при использовании JPA и Hibernate

103

Как в Hibernate должны быть реализованы равенства классов модели и хэш-код? Каковы общие подводные камни? Подходит ли реализация по умолчанию для большинства случаев? Есть ли смысл использовать бизнес-ключи?

Мне кажется, что довольно сложно заставить его правильно работать в любой ситуации, когда учитываются ленивая выборка, генерация идентификатора, прокси и т. Д.

эгага
источник
См. Также stackoverflow.com/a/39827962/548473 (реализация spring-data-jpa)
Григорий Кислин

Ответы:

74

В Hibernate есть хорошее и длинное описание того, когда и как переопределять equals()/ hashCode()в документации.

Суть в том, что вам нужно беспокоиться только о том, будет ли ваша сущность частью a Setили если вы собираетесь отсоединять / присоединять ее экземпляры. Последнее встречается не так часто. С первым обычно лучше всего справиться с помощью:

  1. Базируясь equals()/ hashCode()на бизнес - ключ - например , уникальная комбинация атрибутов, не собирается менять во время объекта (или, по крайней мере, сессии) жизни.
  2. Если выше невозможно, основание equals()/ hashCode()на первичный ключ , если он установлен и объект идентичности / System.identityHashCode()иначе. Важной частью здесь является то , что вам нужно перезагрузить ваш набор после нового объекта было добавлено к нему и сохраняется; в противном случае вы можете столкнуться со странным поведением (что в конечном итоге приведет к ошибкам и / или повреждению данных), потому что ваша сущность может быть размещена в корзине, не соответствующей ее текущему состоянию hashCode().
ЧссПлы76
источник
1
Когда вы говорите "перезагрузить" @ ChssPly76, вы имеете в виду выполнение refresh()? Как ваша сущность, которая подчиняется Setконтракту, оказывается в неправильном ведре (при условии, что у вас достаточно хорошая реализация хэш-кода).
non sequitor
4
Обновить коллекцию или перезагрузить всю сущность (владельца), да. Что касается неправильного ведра: а) вы добавляете новую сущность для установки, ее идентификатор еще не установлен, поэтому вы используете identityHashCode, который помещает вашу сущность в корзину №1. б) ваша сущность (в наборе) сохраняется, теперь у нее есть идентификатор, и поэтому вы используете hashCode () на основе этого идентификатора. Она отличается от выше , и был бы разместила свой объект в ведре # 2. Теперь, если у вас есть ссылка на этот объект в другом месте, попробуйте позвонить, Set.contains(entity)и вы вернетесь false. То же самое и с get () / put () / etc ...
ChssPly76
Имеет смысл, но сам никогда не использовал identityHashCode, хотя я вижу, что он используется в исходном коде Hibernate, как и в их ResultTransformers
non sequitor
1
При использовании Hibernate вы также можете столкнуться с этой проблемой , решения которой я до сих пор не нашел.
Джованни Ботта
@ ChssPly76 Из-за бизнес-правил, которые определяют, равны ли два объекта, мне нужно будет основывать свои методы equals / hashcode на свойствах, которые могут измениться в течение времени существования объекта. Это действительно большое дело? Если да, то как мне это обойти?
ubiquibacon
39

Не думаю, что принятый ответ верен.

Чтобы ответить на исходный вопрос:

Подходит ли реализация по умолчанию для большинства случаев?

Ответ - да, в большинстве случаев это так.

Вам нужно только переопределить, equals()и hashcode()если объект будет использоваться в Set(что очень часто), И объект будет отсоединен от сеансов гибернации и впоследствии повторно присоединен к ним (что является необычным использованием гибернации).

Принятый ответ означает, что методы необходимо переопределить, если выполняется какое- либо из условий.

Фил
источник
Это согласуется с моими наблюдениями, пора выяснить, почему .
Vlastimil Ovčáčík
«Вам нужно только переопределить equals () и hashcode (), если объект будет использоваться в Set» вполне достаточно, если некоторые поля идентифицируют объект, и поэтому вы не хотите полагаться на Object.equals () для идентификации объекты.
davidxxx
17

Лучшее equals/ hashCodeреализация при использовании уникального бизнес - ключа .

Бизнес-ключ должен быть согласованным для всех переходов состояния объекта (временное, присоединенное, отсоединенное, удаленное), поэтому вы не можете полагаться на идентификатор для равенства.

Другой вариант - перейти на использование идентификаторов UUID , назначенных логикой приложения. Таким образом, вы можете использовать UUID для equals/, hashCodeпотому что идентификатор назначается до того, как объект будет сброшен.

Вы даже можете использовать идентификатор объекта для equalsи hashCode, но это требует, чтобы вы всегда возвращали одно и то же hashCodeзначение, чтобы убедиться, что значение hashCode объекта согласовано при всех переходах состояния объекта. Прочтите этот пост, чтобы узнать больше по этой теме .

Влад Михалча
источник
+1 за подход uuid. Поместите это в a BaseEntityи никогда больше не думайте об этой проблеме. Это занимает немного места на стороне db, но эту цену лучше заплатить за комфорт :)
Мартин Фрей,
12

Когда сущность загружается посредством отложенной загрузки, это не экземпляр базового типа, а динамически сгенерированный подтип, созданный javassist, поэтому проверка того же типа класса завершится ошибкой, поэтому не используйте:

if (getClass() != that.getClass()) return false;

вместо этого используйте:

if (!(otherObject instanceof Unit)) return false;

что также является хорошей практикой, как описано в разделе «Реализация равенства в практиках Java» .

по той же причине прямой доступ к полям может не работать и возвращать null вместо базового значения, поэтому не используйте сравнение свойств, а используйте геттеры, поскольку они могут запускать загрузку базовых значений.

стивло
источник
1
Это работает, если вы сравниваете объекты конкретных классов, что в моей ситуации не сработало. Я сравнивал объекты суперклассов, и в этом случае у меня сработал этот код: obj1.getClass (). IsInstance (obj2)
Tad
6

Да, это сложно. В моем проекте equals и hashCode полагаются на идентификатор объекта. Проблема этого решения в том, что ни один из них не работает, если объект еще не сохранен, поскольку идентификатор генерируется базой данных. В моем случае это терпимо, поскольку почти во всех случаях объекты сохраняются сразу. Помимо этого, он отлично работает и прост в реализации.

Карлос
источник
Я думаю, что мы использовали идентификацию объекта в случае, когда идентификатор не был сгенерирован
Кэти Ван Стоун
2
проблема здесь в том, что если вы сохраните объект, ваш хэш-код изменится. Это может иметь большие пагубные последствия, если объект уже является частью структуры данных на основе хэша. Итак, если вы все-таки завершите использование идентификатора объекта, вам лучше продолжать использовать идентификатор объекта, пока объект не будет полностью освобожден (или удалите объект из любых структур на основе хеша, сохраните его, а затем добавьте его обратно). Лично я считаю, что было бы лучше не использовать id и основывать хэш на неизменяемых свойствах объекта.
Кевин Дэй,
1

В документации Hibernate 5.2 говорится, что вы, возможно, вообще не захотите реализовывать hashCode и equals - в зависимости от вашей ситуации.

https://docs.jboss.org/hibernate/orm/5.2/userguide/html_single/Hibernate_User_Guide.html#mapping-model-pojo-equalshashcode

Как правило, два объекта, загруженные из одного сеанса, будут равны, если они равны в базе данных (без реализации hashCode и equals).

Это усложняется, если вы используете два или более сеанса. В этом случае равенство двух объектов зависит от вашей реализации метода equals.

Кроме того, у вас возникнут проблемы, если ваш метод equals сравнивает идентификаторы, которые генерируются только при сохранении объекта в первый раз. Их может еще не быть, когда призовут равных.

Нина
источник
0

Здесь очень хорошая статья: https://docs.jboss.org/hibernate/stable/core.old/reference/en/html/persistent-classes-equalshashcode.html

Цитируя важную строчку из статьи:

Мы рекомендуем реализовать equals () и hashCode () с использованием равенства бизнес-ключей. Равенство бизнес-ключей означает, что метод equals () сравнивает только свойства, которые образуют бизнес-ключ, ключ, который будет идентифицировать наш экземпляр в реальном мире (естественный ключ-кандидат):

Проще говоря

public class Cat {

...
public boolean equals(Object other) {
    //Basic test / class cast
    return this.catId==other.catId;
}

public int hashCode() {
    int result;

    return 3*this.catId; //any primenumber 
}

}
Рави Шекхар
источник
0

Если вам довелось переопределить equals, убедитесь, что вы выполнили его контракты: -

  • СИММЕТРИЯ
  • ОТРАЖАТЕЛЬНЫЙ
  • ПЕРЕХОДНЫЙ
  • ПОСТОЯННЫЙ
  • NON NULL

И переопределить hashCode, поскольку его контракт зависит от equalsреализации.

Джошуа Блох (дизайнер фреймворка Collection) настоятельно призывал следовать этим правилам.

  • пункт 9: Всегда переопределять hashCode, когда вы переопределяете, равно

Несоблюдение этих договоров чревато серьезными непредвиденными последствиями. Например, List#contains(Object o)может вернуть неправильное booleanзначение, поскольку общий договор не выполнен.

Аван Биру
источник