Здесь были некоторые дискуссии о сущностях JPA и о том, какую hashCode()
/ equals()
реализацию следует использовать для классов сущностей JPA. Большинство (если не все) из них зависят от Hibernate, но я бы хотел обсудить их JPA-реализацию-нейтрально (кстати, я использую EclipseLink).
Все возможные реализации имеют свои преимущества и недостатки в отношении:
hashCode()
/equals()
соответствие контракта (неизменность) дляList
/Set
операций- Могут ли быть обнаружены идентичные объекты (например, из разных сеансов, динамические прокси из лениво загруженных структур данных)
- Правильно ли ведут себя сущности в отдельном (или непостоянном) состоянии
Насколько я вижу, есть три варианта :
- Не переопределяйте их; полагаться
Object.equals()
иObject.hashCode()
hashCode()
/equals()
работа- не может идентифицировать идентичные объекты, проблемы с динамическими прокси
- нет проблем с отдельными объектами
- Переопределите их, основываясь на первичном ключе
hashCode()
/equals()
сломаны- правильная идентификация (для всех управляемых объектов)
- проблемы с отдельными объектами
- Переопределите их, основываясь на Business-Id (поля не первичного ключа; как насчет внешних ключей?)
hashCode()
/equals()
сломаны- правильная идентификация (для всех управляемых объектов)
- нет проблем с отдельными объектами
Мои вопросы:
- Я пропустил опцию и / или за / за точку?
- Какой вариант вы выбрали и почему?
ОБНОВЛЕНИЕ 1:
К « hashCode()
/ equals()
сломаны», я имею в виду , что последовательные hashCode()
вызовы может возвращать различные значения, что (при правильной реализации) не нарушаюсь в смысле Object
документации API, но вызывает проблемы при попытке получить измененные сущности из Map
, Set
или других основанный на хэше Collection
. Следовательно, реализации JPA (по крайней мере, EclipseLink) в некоторых случаях не будут работать правильно.
ОБНОВЛЕНИЕ 2:
Спасибо за ваши ответы - большинство из них имеют замечательное качество.
К сожалению, я все еще не уверен, какой подход будет лучшим для реального приложения или как определить лучший подход для моего приложения. Поэтому я оставлю вопрос открытым и надеюсь на дальнейшие обсуждения и / или мнения.
hashcode()
одного и того же экземпляра объекта должен возвращать одно и то же значение, если только неequals()
изменятся поля, используемые в реализации. Другими словами, если у вас есть три поля в вашем классе, и вашequals()
метод использует только два из них для определения равенства экземпляров, то вы можете ожидать, чтоhashcode()
возвращаемое значение изменится, если вы измените одно из значений этого поля - что имеет смысл, если учесть что этот экземпляр объекта больше не "равен" значению, которое представлял старый экземпляр.Ответы:
Прочитайте эту очень хорошую статью на эту тему: не позволяйте Hibernate украсть вашу личность .
Вывод статьи звучит так:
источник
suid.js
выбирает блоки ID, изsuid-server-java
которых вы можете затем получить и использовать клиентскую часть.Я всегда переопределяю equals / hashcode и реализую его на основе бизнес-идентификатора. Кажется, самое разумное решение для меня. Смотрите следующую ссылку .
РЕДАКТИРОВАТЬ :
Чтобы объяснить, почему это работает для меня:
источник
У нас обычно есть два идентификатора в наших организациях:
equals()
иhashCode()
в частности)Взглянуть:
РЕДАКТИРОВАТЬ: уточнить мою точку зрения относительно вызовов
setUuid()
метода. Вот типичный сценарий:Когда я запускаю свои тесты и вижу вывод журнала, я решаю проблему:
В качестве альтернативы можно предоставить отдельный конструктор:
Итак, мой пример будет выглядеть так:
Я использую конструктор по умолчанию и сеттер, но вы можете найти подход с двумя конструкторами более подходящий для вас.
источник
hashCode
/equals
для равенства JVM иid
для постоянства равенства? Это не имеет смысла для меня вообще.Object
«sequals()
вернетсяfalse
в этом случае.equals()
Возврат на основе UUIDtrue
.Я лично уже использовал все эти три государственности в разных проектах. И я должен сказать, что вариант 1, на мой взгляд, является наиболее практичным в реальной жизни приложения. По моему опыту нарушение соответствия hashCode () / equals () приводит ко многим сумасшедшим ошибкам, так как вы будете каждый раз попадать в ситуации, когда результат равенства изменяется после добавления объекта в коллекцию.
Но есть и другие варианты (также со своими плюсами и минусами):
a) hashCode / equals на основе набора неизменяемых , не нулевых , назначенных конструкторов , полей
(+) все три критерия гарантированы
(-) значения полей должны быть доступны для создания нового экземпляра
(-) усложняет обработку, если вы должны изменить один из
b) hashCode / equals на основе первичного ключа, который назначается приложением (в конструкторе) вместо JPA
(+) все три критерия гарантированы
(-) вы не можете воспользоваться простыми надежными состояниями генерации идентификаторов, такими как последовательности БД
(-) сложный, если новые объекты создаются в распределенной среде (клиент / сервер) или кластере серверов приложений
c) hashCode / equals на основе UUID, назначенного конструктором объекта
(+) все три критерия гарантированы
(-) издержки генерации UUID
(-) может быть небольшой риск того, что используется дважды один и тот же UUID, в зависимости от используемого алгоритма (может обнаруживаться по уникальному индексу в БД)
источник
Collection
.Если вы хотите использовать
equals()/hashCode()
для своих Наборов, в том смысле, что одна и та же сущность может быть там только один раз, тогда есть только одна опция: Вариант 2. Это потому, что первичный ключ для сущности по определению никогда не меняется (если кто-то действительно обновляет это уже не одно и то же лицо)Вы должны понимать это буквально: поскольку вы
equals()/hashCode()
основаны на первичном ключе, вы не должны использовать эти методы, пока первичный ключ не установлен. Таким образом, вы не должны помещать объекты в набор, пока им не назначен первичный ключ. (Да, UUID и подобные концепции могут помочь в раннем назначении первичных ключей.)Теперь теоретически также возможно добиться этого с помощью Варианта 3, хотя так называемые «бизнес-ключи» имеют неприятный недостаток, который они могут изменить: «Все, что вам нужно сделать, это удалить уже вставленные сущности из набора ( s) и вставьте их заново. Это верно, но это также означает, что в распределенной системе вам нужно будет убедиться, что это делается абсолютно везде, куда были вставлены данные (и вы должны быть уверены, что обновление выполнено , прежде чем что-то произойдет). Вам понадобится сложный механизм обновления, особенно если некоторые удаленные системы в настоящее время недоступны ...
Вариант 1 можно использовать только в том случае, если все объекты в ваших наборах относятся к одному сеансу Hibernate. В документации Hibernate об этом очень ясно говорится в главе 13.1.3. Учитывая идентичность объекта :
Он продолжает утверждать в пользу варианта 3:
Это правда, если вы
В противном случае вы можете выбрать вариант 2.
Затем упоминается необходимость относительной стабильности:
Это верно. Практическая проблема, которую я вижу в этом: если вы не можете гарантировать абсолютную стабильность, как вы сможете гарантировать стабильность «до тех пор, пока объекты находятся в одном наборе». Я могу представить некоторые особые случаи (например, использовать наборы только для разговора, а затем выбросить его), но я бы поставил под сомнение общую практичность этого.
Укороченная версия:
источник
equals
/hashCode
.Object
equals и hashCode, потому что он не работает после васmerge
и сущности.Вы можете использовать идентификатор объекта, как предлагается в этом посте . Единственный улов в том, что вам нужно использовать
hashCode
реализацию, которая всегда возвращает одно и то же значение, например так:источник
Хотя использование бизнес-ключа (вариант 3) является наиболее часто рекомендуемым подходом ( вики-сообщество Hibernate , «Java Persistence with Hibernate», стр. 398), и это то, что мы в основном используем, есть ошибка Hibernate, которая нарушает эту задачу для eager-fetched комплекты: HHH-3799 . В этом случае Hibernate может добавить объект в набор до инициализации его полей. Я не уверен, почему эта ошибка не получила большего внимания, так как это действительно делает рекомендуемый подход бизнес-ключом проблематичным.
Я думаю, что суть дела в том, что equals и hashCode должны основываться на неизменяемом состоянии (ссылка Odersky et al. ), А сущность Hibernate с управляемым Hibernate первичным ключом не имеет такого неизменного состояния. Первичный ключ изменяется в Hibernate, когда временный объект становится постоянным. Бизнес-ключ также изменяется в Hibernate, когда он гидратирует объект в процессе инициализации.
Это оставляет только вариант 1, наследующий реализации java.lang.Object, основанный на идентичности объекта, или использующий первичный ключ, управляемый приложением, как это было предложено Джеймсом Брюнджемом в «Не позволяйте Hibernate украсть вашу личность» (уже упоминается в ответе Стейна Гойкенса ) и Лэнсом Арлаусом в «Генерации объектов: лучший подход к интеграции в спящий режим» .
Самая большая проблема с вариантом 1 заключается в том, что отдельные экземпляры нельзя сравнивать с постоянными экземплярами с помощью .equals (). Но это нормально; Контракт equals и hashCode оставляют на усмотрение разработчика решать, что означает равенство для каждого класса. Так что пусть equals и hashCode наследуются от Object. Если вам нужно сравнить отдельный экземпляр с постоянным экземпляром, вы можете явно создать новый метод для этой цели, возможно,
boolean sameEntity
илиboolean dbEquivalent
илиboolean businessEquals
.источник
Я согласен с ответом Андрея. Мы делаем то же самое в нашем приложении, но вместо того, чтобы хранить UUID как VARCHAR / CHAR, мы разбиваем его на два длинных значения. Смотрите UUID.getLeastSignificantBits () и UUID.getMostSignificantBits ().
Еще одна вещь, которую следует учитывать, это то, что вызовы UUID.randomUUID () довольно медленные, поэтому вы можете захотеть лениво генерировать UUID только при необходимости, например, во время сохранения или при вызове equals () / hashCode ().
источник
Как уже указывали другие люди, умнее меня, существует множество стратегий. Похоже, что дело в том, что большинство применяемых шаблонов проектирования пытаются взломать свой путь к успеху. Они ограничивают доступ к конструктору, если не мешают вызовам конструктора полностью с помощью специализированных конструкторов и фабричных методов. Действительно, это всегда приятно с четким API. Но если единственная причина состоит в том, чтобы сделать равные и хэш-коды совместимыми с приложением, то мне интересно, соответствуют ли эти стратегии KISS (Keep It Simple Stupid).
Для меня я люблю переопределять equals и hashcode посредством проверки идентификатора. В этих методах я требую, чтобы идентификатор не был нулевым, и хорошо документирую это поведение. Таким образом, это станет контрактом разработчиков на сохранение нового объекта перед его хранением в другом месте. Приложение, которое не соблюдает этот контракт, не сможет в течение минуты (надеюсь).
Тем не менее, предостережение: если ваши сущности хранятся в разных таблицах и ваш провайдер использует стратегию автоматической генерации для первичного ключа, вы получите дублированные первичные ключи для разных типов сущностей. В этом случае также сравнивайте типы времени выполнения с вызовом Object # getClass (), что, конечно, сделает невозможным, чтобы два разных типа считались равными. Это подходит мне просто отлично по большей части.
источник
Object#getClass()
плохо из-за прокси-серверов H. ВызовHibernate.getClass(o)
помогает, но проблема равенства сущностей разных видов остается. Есть решение, использующее canEqual , немного сложное, но пригодное для использования. Договорились, что обычно это не нужно. +++ Ввод eq / hc на нулевой идентификатор нарушает договор, но это очень прагматично.Здесь, очевидно, уже есть очень информативные ответы, но я расскажу вам, что мы делаем.
Мы ничего не делаем (т.е. не переопределяем).
Если нам нужен equals / hashcode для работы с коллекциями, мы используем UUID. Вы просто создаете UUID в конструкторе. Мы используем http://wiki.fasterxml.com/JugHome для UUID. UUID немного дороже с точки зрения процессора, но дешевле по сравнению с сериализацией и доступом к БД.
источник
Я всегда использовал вариант 1 в прошлом, потому что я знал об этих обсуждениях и думал, что лучше ничего не делать, пока я не узнаю, что нужно делать. Эти системы все еще успешно работают.
Однако в следующий раз я могу попробовать вариант 2 - с использованием идентификатора, сгенерированного базой данных.
Хэш-код и equals вызовут IllegalStateException, если идентификатор не установлен.
Это предотвратит неожиданные ошибки, связанные с несохраненными объектами.
Что люди думают об этом подходе?
источник
Подход бизнес-ключей нам не подходит. Мы используем ID , сгенерированный БД , временный переходный процесс tempId и переопределение equal () / hashcode () для решения дилеммы Все сущности являются потомками сущностей. Плюсы:
Минусы:
Посмотрите на наш код:
источник
Пожалуйста, рассмотрите следующий подход, основанный на предопределенном идентификаторе типа и идентификаторе.
Конкретные предположения для JPA:
Абстрактная сущность:
Пример конкретного объекта:
Тестовый пример:
Основные преимущества здесь:
Недостатки:
super()
Ноты:
class A
иclass B extends A
может зависеть от конкретных деталей приложения.Ждем ваших комментариев.
источник
Это общая проблема в каждой ИТ-системе, которая использует Java и JPA. Болевая точка выходит за рамки реализации equals () и hashCode (), она влияет на то, как организация ссылается на сущность и как ее клиенты ссылаются на одну и ту же сущность. Я видел достаточно боли, когда у меня не было бизнес-ключа, и я написал свой блог, чтобы выразить свою точку зрения.
Вкратце: используйте короткий, понятный человеку последовательный идентификатор со значимыми префиксами в качестве бизнес-ключа, который генерируется без какой-либо зависимости от какого-либо хранилища, кроме ОЗУ. Снежинка Твиттера - очень хороший пример.
источник
ИМО у вас есть 3 варианта реализации equals / hashCode
Использование идентификатора, сгенерированного приложением, является самым простым подходом, но имеет несколько недостатков
Если вы можете работать с этими недостатками , просто используйте этот подход.
Чтобы преодолеть проблему объединения, можно использовать UUID в качестве естественного ключа и значение последовательности в качестве первичного ключа, но тогда вы все равно можете столкнуться с проблемами реализации equals / hashCode в составных дочерних объектах, которые имеют встроенные идентификаторы, поскольку вы захотите присоединиться на основе объединения. на первичном ключе. Использование естественного ключа в дочерних сущностях id и первичного ключа для обращения к родителю является хорошим компромиссом.
IMO, это самый чистый подход, поскольку он позволит избежать всех недостатков и в то же время предоставит вам значение (UUID), которым вы можете поделиться с внешними системами, не подвергая системным внутренним компонентам.
Реализуйте его на основе бизнес-ключа, если вы ожидаете, что от пользователя это хорошая идея, но также есть несколько недостатков
В большинстве случаев этот бизнес-ключ будет представлять собой некий код , предоставляемый пользователем, и реже составной из нескольких атрибутов.
ИМО, вы не должны внедрять или работать исключительно с бизнес-ключом. Это хорошее дополнение, т.е. пользователи могут быстро выполнять поиск по этому бизнес-ключу, но система не должна полагаться на него для работы.
Реализация его на основе первичного ключа имеет свои проблемы, но, возможно, это не такая уж большая проблема
Если вам нужно выставить идентификаторы во внешнюю систему, используйте предложенный мной подход UUID. Если вы этого не сделаете, вы все равно можете использовать подход UUID, но вам не нужно. Проблема использования идентификатора, созданного СУБД в equals / hashCode, связана с тем фактом, что объект мог быть добавлен в коллекции на основе хеша до назначения идентификатора.
Очевидный способ обойти это - просто не добавлять объект в коллекции, основанные на хэше, до назначения идентификатора. Я понимаю, что это не всегда возможно, потому что вы могли бы хотеть дедупликации прежде, чем назначить идентификатор уже. Чтобы по-прежнему иметь возможность использовать коллекции на основе хеша, вам просто нужно перестроить коллекции после назначения идентификатора.
Вы могли бы сделать что-то вроде этого:
Я сам не проверял точный подход, поэтому не уверен, как работает изменение коллекций в до и после персистентных событиях, но идея такова:
Другой способ решения этой проблемы - просто перестроить все ваши модели, основанные на хэше, после обновления / сохранения.
В конце концов, решать вам. Я лично использую подход, основанный на последовательностях, большую часть времени и использую UUID только тогда, когда мне нужно предоставить идентификатор внешним системам.
источник
С новым стилем
instanceof
из Java 14, вы можете реализоватьequals
в одной строке.источник
Если UUID является ответом для многих людей, почему бы нам просто не использовать фабричные методы из бизнес-уровня для создания сущностей и назначения первичного ключа во время создания?
например:
таким образом мы получили бы первичный ключ по умолчанию для сущности от поставщика постоянства, и наши функции hashCode () и equals () могли бы полагаться на это.
Мы также можем объявить конструкторы Car защищенными, а затем использовать отражение в нашем бизнес-методе для доступа к ним. Таким образом, разработчики не будут стремиться создать экземпляр Car с новым, но с помощью заводского метода.
Как насчет этого?
источник
Я пытался ответить на этот вопрос сам и никогда не был полностью доволен найденными решениями, пока не прочитал этот пост и особенно DREW. Мне понравилось, как он ленивый создал UUID и оптимально сохранил его.
Но я хотел добавить еще больше гибкости, то есть ленивое создание UUID ТОЛЬКО при обращении к hashCode () / equals () до первого сохранения сущности с преимуществами каждого решения:
Я бы очень признателен за отзыв о моем смешанном решении ниже
источник
На практике кажется, что вариант 2 (первичный ключ) используется чаще всего. Естественный и НЕМЕРТОВЫЙ бизнес-ключ - это редко, создание и поддержка синтетических ключей слишком сложны для решения ситуаций, которых, вероятно, никогда не было. Взгляните на реализацию Spring-data-jpa AbstractPersistable (единственное: для использования реализации Hibernate
Hibernate.getClass
).Просто в курсе манипулирования новыми объектами в HashSet / HashMap. Напротив, Вариант 1 (остается
Object
реализация) нарушается сразу послеmerge
, это очень распространенная ситуация.Если у вас нет бизнес-ключа и у вас есть REAL, вам нужно манипулировать новым объектом в хэш-структуре, переопределите
hashCode
его как константу, как показано ниже.источник
Ниже приведено простое (и проверенное) решение для Scala.
Обратите внимание, что это решение не вписывается ни в одну из 3 категорий, приведенных в вопросе.
Все мои сущности являются подклассами UUIDEntity, поэтому я следую принципу «не повторяй себя» (СУХОЙ).
При необходимости генерацию UUID можно сделать более точной (используя больше псевдослучайных чисел).
Скала код:
источник