Дилемма JPA hashCode () / equals ()

311

Здесь были некоторые дискуссии о сущностях JPA и о том, какую hashCode()/ equals()реализацию следует использовать для классов сущностей JPA. Большинство (если не все) из них зависят от Hibernate, но я бы хотел обсудить их JPA-реализацию-нейтрально (кстати, я использую EclipseLink).

Все возможные реализации имеют свои преимущества и недостатки в отношении:

  • hashCode()/ equals()соответствие контракта (неизменность) для List/ Setопераций
  • Могут ли быть обнаружены идентичные объекты (например, из разных сеансов, динамические прокси из лениво загруженных структур данных)
  • Правильно ли ведут себя сущности в отдельном (или непостоянном) состоянии

Насколько я вижу, есть три варианта :

  1. Не переопределяйте их; полагаться Object.equals()иObject.hashCode()
    • hashCode()/ equals()работа
    • не может идентифицировать идентичные объекты, проблемы с динамическими прокси
    • нет проблем с отдельными объектами
  2. Переопределите их, основываясь на первичном ключе
    • hashCode()/ equals()сломаны
    • правильная идентификация (для всех управляемых объектов)
    • проблемы с отдельными объектами
  3. Переопределите их, основываясь на Business-Id (поля не первичного ключа; как насчет внешних ключей?)
    • hashCode()/ equals()сломаны
    • правильная идентификация (для всех управляемых объектов)
    • нет проблем с отдельными объектами

Мои вопросы:

  1. Я пропустил опцию и / или за / за точку?
  2. Какой вариант вы выбрали и почему?



ОБНОВЛЕНИЕ 1:

К « hashCode()/ equals()сломаны», я имею в виду , что последовательные hashCode()вызовы может возвращать различные значения, что (при правильной реализации) не нарушаюсь в смысле Objectдокументации API, но вызывает проблемы при попытке получить измененные сущности из Map, Setили других основанный на хэше Collection. Следовательно, реализации JPA (по крайней мере, EclipseLink) в некоторых случаях не будут работать правильно.

ОБНОВЛЕНИЕ 2:

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

MRalwasser
источник
4
Я не понимаю, что вы подразумеваете под "hashCode () / equals () broken"
nanda
4
Тогда они не будут «сломаны» в этом смысле, так как в вариантах 2 и 3 вы будете реализовывать и equals (), и hashCode (), используя одну и ту же стратегию.
Matt B
11
Это не относится к варианту 3. hashCode () и equals () должны использовать одни и те же критерии, поэтому, если одно из ваших полей изменится, то метод hashcode () вернет другое значение для того же экземпляра, что и предыдущий, но так будет равно (). Вы удалили вторую часть предложения из javadoc hashcode (): всякий раз, когда он вызывается для одного и того же объекта более одного раза во время выполнения приложения Java, метод hashCode должен последовательно возвращать одно и то же целое число при условии отсутствия информации Используется в равных сравнений на объекте модифицируется .
Matt B
1
На самом деле эта часть предложения означает обратное - вызов hashcode()одного и того же экземпляра объекта должен возвращать одно и то же значение, если только не equals()изменятся поля, используемые в реализации. Другими словами, если у вас есть три поля в вашем классе, и ваш equals()метод использует только два из них для определения равенства экземпляров, то вы можете ожидать, что hashcode()возвращаемое значение изменится, если вы измените одно из значений этого поля - что имеет смысл, если учесть что этот экземпляр объекта больше не "равен" значению, которое представлял старый экземпляр.
Matt B
2
«проблемы при попытке извлечь измененную сущность из карты, набора или других коллекций на основе хеша» ... это должны быть «проблемы при попытке извлечь измененную сущность из коллекции HashMap, HashSet или других коллекций на основе хеша»
nanda

Ответы:

122

Прочитайте эту очень хорошую статью на эту тему: не позволяйте Hibernate украсть вашу личность .

Вывод статьи звучит так:

Идентификация объекта обманчиво трудно реализовать правильно, когда объекты сохраняются в базе данных. Однако проблемы полностью связаны с тем, что объекты могут существовать без идентификатора до их сохранения. Мы можем решить эти проблемы, взяв на себя ответственность за присвоение идентификаторов объектов за пределами структур объектно-реляционного отображения, таких как Hibernate. Вместо этого идентификаторы объекта могут быть назначены, как только объект будет создан. Это делает идентификацию объекта простой и безошибочной, а также уменьшает объем кода, необходимого в модели предметной области.

Stijn Geukens
источник
21
Нет, это не хорошая статья. Это чертовски замечательная статья на эту тему, и ее должен прочитать каждый программист JPA! +1!
Том Андерсон
2
Да, я использую то же решение. Не позволяя БД генерировать идентификатор, есть и другие преимущества, такие как возможность создавать объект и уже создавать другие объекты, которые ссылаются на него, прежде чем сохранить его. Это может устранить задержки и несколько циклов запросов / ответов в клиент-серверных приложениях. Если вам нужно вдохновение для такого решения, посмотрите мои проекты: suid.js и suid-server-java . В основном suid.jsвыбирает блоки ID, из suid-server-javaкоторых вы можете затем получить и использовать клиентскую часть.
Стейн де Витт
2
Это просто безумие. Я новичок в спящем режиме под капотом, писал модульные тесты и обнаружил, что не могу удалить объект из набора после его изменения, пришел к выводу, что это из-за изменения хэш-кода, но не смог понять, как решать. Статья просто великолепна!
XMight
Это отличная статья. Тем не менее, для людей, которые видят ссылку в первый раз, я бы предположил, что это может быть излишним для большинства приложений. Другие 3 варианта, перечисленные на этой странице, должны более или менее решить проблему несколькими способами.
HopeKing
1
Использует ли Hibernate / JPA метод equals и hashcode объекта, чтобы проверить, существует ли запись в базе данных?
Тушар
64

Я всегда переопределяю equals / hashcode и реализую его на основе бизнес-идентификатора. Кажется, самое разумное решение для меня. Смотрите следующую ссылку .

Чтобы подвести итог всего этого, вот список того, что будет работать или не работать с различными способами обработки equals / hashCode: введите описание изображения здесь

РЕДАКТИРОВАТЬ :

Чтобы объяснить, почему это работает для меня:

  1. Я обычно не использую хеш-коллекцию (HashMap / HashSet) в своем приложении JPA. Если я должен, я предпочитаю создать решение UniqueList.
  2. Я думаю, что изменение бизнес-идентификатора во время выполнения не является лучшей практикой для любого приложения базы данных. В редких случаях, когда нет другого решения, я бы сделал особую обработку, например, удалил элемент и вернул его в коллекцию на основе хеширования.
  3. Для моей модели я устанавливаю бизнес-идентификатор в конструкторе и не предоставляю для него установщики. Я позволил реализации JPA изменить поле вместо свойства.
  4. Решение UUID кажется излишним. Зачем UUID, если у вас есть естественный бизнес-идентификатор? Я бы все-таки установил уникальность бизнес-идентификатора в базе данных. Зачем тогда иметь ТРИ индекса для каждой таблицы в базе данных?
нанда
источник
1
Но в этой таблице отсутствует пятая строка «работает со списком / наборами» (если вы думаете об удалении сущности, являющейся частью набора, из сопоставления OneToMany), на которую в последних двух опциях будет дан ответ «Нет», поскольку ее hashCode ( ) изменения, которые нарушают его договор.
MRalwasser
Смотрите комментарий к вопросу. Похоже, вы неправильно поняли контракт равных / хэш-
кодов
1
@MRalwasser: Я думаю, что вы имеете в виду правильные вещи, но нарушается не сам контракт equals / hashCode (). Но изменяемый equals / hashCode создает проблемы с контрактом Set .
Крис Лерчер
3
@MRalwasser: хэш-код может измениться только в случае изменения бизнес-идентификатора, и дело в том, что бизнес-идентификатор не меняется. Таким образом, хэш-код не изменяется, и это прекрасно работает с хэшированными коллекциями.
Том Андерсон
1
Что делать, если у вас нет натурального бизнес-ключа? Например, в случае двумерной точки Point (X, Y) в приложении для рисования графиков? Как бы вы сохранили эту точку как сущность?
Джегедус
35

У нас обычно есть два идентификатора в наших организациях:

  1. Предназначен только для уровня сохраняемости (чтобы поставщик сохраняемости и база данных могли выяснить отношения между объектами).
  2. Для наших потребностей приложений ( equals()и hashCode()в частности)

Взглянуть:

@Entity
public class User {

    @Id
    private int id;  // Persistence ID
    private UUID uuid; // Business ID

    // assuming all fields are subject to change
    // If we forbid users change their email or screenName we can use these
    // fields for business ID instead, but generally that's not the case
    private String screenName;
    private String email;

    // I don't put UUID generation in constructor for performance reasons. 
    // I call setUuid() when I create a new entity
    public User() {
    }

    // This method is only called when a brand new entity is added to 
    // persistence context - I add it as a safety net only but it might work 
    // for you. In some cases (say, when I add this entity to some set before 
    // calling em.persist()) setting a UUID might be too late. If I get a log 
    // output it means that I forgot to call setUuid() somewhere.
    @PrePersist
    public void ensureUuid() {
        if (getUuid() == null) {
            log.warn(format("User's UUID wasn't set on time. " 
                + "uuid: %s, name: %s, email: %s",
                getUuid(), getScreenName(), getEmail()));
            setUuid(UUID.randomUUID());
        }
    }

    // equals() and hashCode() rely on non-changing data only. Thus we 
    // guarantee that no matter how field values are changed we won't 
    // lose our entity in hash-based Sets.
    @Override
    public int hashCode() {
        return getUuid().hashCode();
    }

    // Note that I don't use direct field access inside my entity classes and
    // call getters instead. That's because Persistence provider (PP) might
    // want to load entity data lazily. And I don't use 
    //    this.getClass() == other.getClass() 
    // for the same reason. In order to support laziness PP might need to wrap
    // my entity object in some kind of proxy, i.e. subclassing it.
    @Override
    public boolean equals(final Object obj) {
        if (this == obj)
            return true;
        if (!(obj instanceof User))
            return false;
        return getUuid().equals(((User) obj).getUuid());
    }

    // Getters and setters follow
}

РЕДАКТИРОВАТЬ: уточнить мою точку зрения относительно вызовов setUuid()метода. Вот типичный сценарий:

User user = new User();
// user.setUuid(UUID.randomUUID()); // I should have called it here
user.setName("Master Yoda");
user.setEmail("yoda@jedicouncil.org");

jediSet.add(user); // here's bug - we forgot to set UUID and 
                   //we won't find Yoda in Jedi set

em.persist(user); // ensureUuid() was called and printed the log for me.

jediCouncilSet.add(user); // Ok, we got a UUID now

Когда я запускаю свои тесты и вижу вывод журнала, я решаю проблему:

User user = new User();
user.setUuid(UUID.randomUUID());

В качестве альтернативы можно предоставить отдельный конструктор:

@Entity
public class User {

    @Id
    private int id;  // Persistence ID
    private UUID uuid; // Business ID

    ... // fields

    // Constructor for Persistence provider to use
    public User() {
    }

    // Constructor I use when creating new entities
    public User(UUID uuid) {
        setUuid(uuid);
    }

    ... // rest of the entity.
}

Итак, мой пример будет выглядеть так:

User user = new User(UUID.randomUUID());
...
jediSet.add(user); // no bug this time

em.persist(user); // and no log output

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

Андрей Андрей Листочкин
источник
2
Я считаю, что это правильное и хорошее решение. Это также может иметь небольшое преимущество в производительности, поскольку целые числа обычно работают лучше в индексах базы данных, чем uuids. Но кроме этого, вы могли бы, вероятно, удалить текущее свойство целочисленного идентификатора и заменить его (назначенным приложением) uuid?
Крис Лерчер
4
Чем это отличается от использования методов по умолчанию hashCode/ equalsдля равенства JVM и idдля постоянства равенства? Это не имеет смысла для меня вообще.
Бехранг Саидзаде
2
Это работает в тех случаях, когда у вас есть несколько объектов сущностей, указывающих на одну и ту же строку в базе данных. Object«s equals()вернется falseв этом случае. equals()Возврат на основе UUID true.
Андрей Андрей Листочкин
4
-1 - я не вижу никакой причины иметь два идентификатора, и поэтому два вида идентичности. Это кажется совершенно бессмысленным и потенциально вредным для меня.
Том Андерсон
1
Извините за критику вашего решения без указания на то, что я предпочел бы. Короче говоря, я бы дал объектам одно поле идентификатора, я бы реализовал equals и hashCode на его основе, и я бы генерировал его значение при создании объекта, а не при сохранении в базе данных. Таким образом, все формы объекта работают одинаково: непостоянные, постоянные и обособленные. Прокси-серверы Hibernate (или аналогичные) также должны работать правильно, и я думаю, что даже не нужно обрабатывать их для обработки вызовов equals и hashCode.
Том Андерсон
31

Я лично уже использовал все эти три государственности в разных проектах. И я должен сказать, что вариант 1, на мой взгляд, является наиболее практичным в реальной жизни приложения. По моему опыту нарушение соответствия hashCode () / equals () приводит ко многим сумасшедшим ошибкам, так как вы будете каждый раз попадать в ситуации, когда результат равенства изменяется после добавления объекта в коллекцию.

Но есть и другие варианты (также со своими плюсами и минусами):


a) hashCode / equals на основе набора неизменяемых , не нулевых , назначенных конструкторов , полей

(+) все три критерия гарантированы

(-) значения полей должны быть доступны для создания нового экземпляра

(-) усложняет обработку, если вы должны изменить один из


b) hashCode / equals на основе первичного ключа, который назначается приложением (в конструкторе) вместо JPA

(+) все три критерия гарантированы

(-) вы не можете воспользоваться простыми надежными состояниями генерации идентификаторов, такими как последовательности БД

(-) сложный, если новые объекты создаются в распределенной среде (клиент / сервер) или кластере серверов приложений


c) hashCode / equals на основе UUID, назначенного конструктором объекта

(+) все три критерия гарантированы

(-) издержки генерации UUID

(-) может быть небольшой риск того, что используется дважды один и тот же UUID, в зависимости от используемого алгоритма (может обнаруживаться по уникальному индексу в БД)

lweller
источник
Я фанат Варианта 1 и Подхода C также. Ничего не делайте, пока вам не понадобится это более гибкий подход.
Адам Гент
2
+1 за вариант (б). ИМХО, если у сущности есть естественный бизнес-идентификатор, то это также должен быть ее первичный ключ базы данных. Это простой, понятный, хороший дизайн базы данных. Если у него нет такого идентификатора, нужен суррогатный ключ. Если вы установите это при создании объекта, то все остальное просто. Когда люди не используют естественный ключ и не генерируют суррогатный ключ рано, они попадают в беду. Что касается сложности в реализации - да, есть некоторые. Но на самом деле не так много, и это можно сделать очень общим способом, который решает его один раз для всех сущностей.
Том Андерсон
Я также предпочитаю вариант 1, но то, как написать модульный тест для утверждения полного равенства, является большой проблемой, потому что мы должны реализовать метод equals для Collection.
OOD Waterball
Просто не делай этого. Смотрите « Не
связывайтесь
29

Если вы хотите использовать equals()/hashCode()для своих Наборов, в том смысле, что одна и та же сущность может быть там только один раз, тогда есть только одна опция: Вариант 2. Это потому, что первичный ключ для сущности по определению никогда не меняется (если кто-то действительно обновляет это уже не одно и то же лицо)

Вы должны понимать это буквально: поскольку вы equals()/hashCode()основаны на первичном ключе, вы не должны использовать эти методы, пока первичный ключ не установлен. Таким образом, вы не должны помещать объекты в набор, пока им не назначен первичный ключ. (Да, UUID и подобные концепции могут помочь в раннем назначении первичных ключей.)

Теперь теоретически также возможно добиться этого с помощью Варианта 3, хотя так называемые «бизнес-ключи» имеют неприятный недостаток, который они могут изменить: «Все, что вам нужно сделать, это удалить уже вставленные сущности из набора ( s) и вставьте их заново. Это верно, но это также означает, что в распределенной системе вам нужно будет убедиться, что это делается абсолютно везде, куда были вставлены данные (и вы должны быть уверены, что обновление выполнено , прежде чем что-то произойдет). Вам понадобится сложный механизм обновления, особенно если некоторые удаленные системы в настоящее время недоступны ...

Вариант 1 можно использовать только в том случае, если все объекты в ваших наборах относятся к одному сеансу Hibernate. В документации Hibernate об этом очень ясно говорится в главе 13.1.3. Учитывая идентичность объекта :

В рамках сеанса приложение может безопасно использовать == для сравнения объектов.

Однако приложение, использующее == вне сеанса, может привести к неожиданным результатам. Это может произойти даже в некоторых неожиданных местах. Например, если вы поместите два отдельных экземпляра в один и тот же набор, оба могут иметь одинаковый идентификатор базы данных (т. Е. Они представляют одну и ту же строку). Однако идентичность JVM по определению не гарантируется для экземпляров в отключенном состоянии. Разработчик должен переопределить методы equals () и hashCode () в постоянных классах и реализовать собственное понятие равенства объектов.

Он продолжает утверждать в пользу варианта 3:

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

Это правда, если вы

  • невозможно назначить идентификатор рано (например, с помощью UUID)
  • и все же вы абсолютно хотите поместить свои объекты в наборы, пока они находятся в переходном состоянии.

В противном случае вы можете выбрать вариант 2.

Затем упоминается необходимость относительной стабильности:

Атрибуты бизнес-ключей не должны быть такими же стабильными, как первичные ключи базы данных; Вы должны только гарантировать стабильность, пока объекты находятся в одном наборе.

Это верно. Практическая проблема, которую я вижу в этом: если вы не можете гарантировать абсолютную стабильность, как вы сможете гарантировать стабильность «до тех пор, пока объекты находятся в одном наборе». Я могу представить некоторые особые случаи (например, использовать наборы только для разговора, а затем выбросить его), но я бы поставил под сомнение общую практичность этого.


Укороченная версия:

  • Вариант 1 может использоваться только с объектами в течение одного сеанса.
  • Если вы можете, используйте Вариант 2. (Назначьте PK как можно раньше, потому что вы не можете использовать объекты в наборах, пока не назначен PK.)
  • Если вы можете гарантировать относительную стабильность, вы можете использовать вариант 3. Но будьте осторожны с этим.
Крис Лерчер
источник
Ваше предположение, что первичный ключ никогда не изменяется, неверно. Например, Hibernate выделяет первичный ключ только при сохранении сеанса. Таким образом, если вы используете первичный ключ в качестве hashCode, результат hashCode () перед первым сохранением объекта и после первого сохранения будет другим. Хуже того, перед сохранением сеанса два вновь созданных объекта будут иметь одинаковый хэш-код и могут перезаписывать друг друга при добавлении в коллекции. Вы можете столкнуться с необходимостью немедленно принудительно сохранить / сбросить при создании объекта, чтобы использовать этот подход.
Уильям Биллингсли
2
@William: первичный ключ сущности не меняется. Свойство id сопоставленного объекта может измениться. Как вы объяснили, это происходит, особенно когда временный объект становится постоянным . Пожалуйста, внимательно прочитайте часть моего ответа, где я сказал о методах equals / hashCode: «Вы не должны использовать эти методы, пока не установлен первичный ключ».
Крис Лерчер
Полностью согласен. С опцией 2 вы также можете выделить из класса супер-класса equals / hashcode и использовать его всеми вашими сущностями.
Тео
+1 Я новичок в JPA, но некоторые из комментариев и ответов здесь подразумевают, что люди не понимают значение термина «первичный ключ».
Raedwald
16
  1. Если у вас есть бизнес-ключ , вы должны использовать его для equals/ hashCode.
  2. Если у вас нет бизнес-ключа, вы не должны оставлять его с реализациями по умолчанию Objectequals и hashCode, потому что он не работает после вас mergeи сущности.
  3. Вы можете использовать идентификатор объекта, как предлагается в этом посте . Единственный улов в том, что вам нужно использовать hashCodeреализацию, которая всегда возвращает одно и то же значение, например так:

    @Entity
    public class Book implements Identifiable<Long> {
    
        @Id
        @GeneratedValue
        private Long id;
    
        private String title;
    
        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (!(o instanceof Book)) return false;
            Book book = (Book) o;
            return getId() != null && Objects.equals(getId(), book.getId());
        }
    
        @Override
        public int hashCode() {
            return 31;
        }
    
        //Getters and setters omitted for brevity
    }
Влад Михалча
источник
Что лучше: (1) onjava.com/pub/a/onjava/2006/09/13/… или (2) vladmihalcea.com/… ? Решение (2) проще, чем (1). Так почему я должен использовать (1). Являются ли эффекты обоих одинаковыми? Оба гарантируют одно и то же решение?
nimo23
И с вашим решением: «значение hashCode не меняется» между одинаковыми экземплярами. Это имеет такое же поведение, как если бы это был «тот же» uuid (из решения (1)), который сравнивается. Я прав?
nimo23
1
И сохранить UUID в базе данных и увеличить площадь записи и в пуле буферов? Я думаю, что это может привести к большему количеству проблем с производительностью в долгосрочной перспективе, чем уникальный хэш-код. Что касается другого решения, вы можете проверить его, чтобы убедиться, что оно обеспечивает согласованность во всех переходах состояния сущности. Вы можете найти тест, который проверяет это на GitHub .
Влад Михалча
1
Если у вас есть неизменный бизнес-ключ, его может использовать hashCode, и он выиграет от нескольких сегментов, поэтому его стоит использовать, если он у вас есть. В противном случае просто используйте идентификатор объекта, как описано в моей статье.
Влад Михалча
1
Я рад, что тебе понравилось. У меня есть еще сотни статей о JPA и Hibernate.
Влад Михальча
10

Хотя использование бизнес-ключа (вариант 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.

jbyler
источник
5

Я согласен с ответом Андрея. Мы делаем то же самое в нашем приложении, но вместо того, чтобы хранить UUID как VARCHAR / CHAR, мы разбиваем его на два длинных значения. Смотрите UUID.getLeastSignificantBits () и UUID.getMostSignificantBits ().

Еще одна вещь, которую следует учитывать, это то, что вызовы UUID.randomUUID () довольно медленные, поэтому вы можете захотеть лениво генерировать UUID только при необходимости, например, во время сохранения или при вызове equals () / hashCode ().

@MappedSuperclass
public abstract class AbstractJpaEntity extends AbstractMutable implements Identifiable, Modifiable {

    private static final long   serialVersionUID    = 1L;

    @Version
    @Column(name = "version", nullable = false)
    private int                 version             = 0;

    @Column(name = "uuid_least_sig_bits")
    private long                uuidLeastSigBits    = 0;

    @Column(name = "uuid_most_sig_bits")
    private long                uuidMostSigBits     = 0;

    private transient int       hashCode            = 0;

    public AbstractJpaEntity() {
        //
    }

    public abstract Integer getId();

    public abstract void setId(final Integer id);

    public boolean isPersisted() {
        return getId() != null;
    }

    public int getVersion() {
        return version;
    }

    //calling UUID.randomUUID() is pretty expensive, 
    //so this is to lazily initialize uuid bits.
    private void initUUID() {
        final UUID uuid = UUID.randomUUID();
        uuidLeastSigBits = uuid.getLeastSignificantBits();
        uuidMostSigBits = uuid.getMostSignificantBits();
    }

    public long getUuidLeastSigBits() {
        //its safe to assume uuidMostSigBits of a valid UUID is never zero
        if (uuidMostSigBits == 0) {
            initUUID();
        }
        return uuidLeastSigBits;
    }

    public long getUuidMostSigBits() {
        //its safe to assume uuidMostSigBits of a valid UUID is never zero
        if (uuidMostSigBits == 0) {
            initUUID();
        }
        return uuidMostSigBits;
    }

    public UUID getUuid() {
        return new UUID(getUuidMostSigBits(), getUuidLeastSigBits());
    }

    @Override
    public int hashCode() {
        if (hashCode == 0) {
            hashCode = (int) (getUuidMostSigBits() >> 32 ^ getUuidMostSigBits() ^ getUuidLeastSigBits() >> 32 ^ getUuidLeastSigBits());
        }
        return hashCode;
    }

    @Override
    public boolean equals(final Object obj) {
        if (obj == null) {
            return false;
        }
        if (!(obj instanceof AbstractJpaEntity)) {
            return false;
        }
        //UUID guarantees a pretty good uniqueness factor across distributed systems, so we can safely
        //dismiss getClass().equals(obj.getClass()) here since the chance of two different objects (even 
        //if they have different types) having the same UUID is astronomical
        final AbstractJpaEntity entity = (AbstractJpaEntity) obj;
        return getUuidMostSigBits() == entity.getUuidMostSigBits() && getUuidLeastSigBits() == entity.getUuidLeastSigBits();
    }

    @PrePersist
    public void prePersist() {
        // make sure the uuid is set before persisting
        getUuidLeastSigBits();
    }

}
Нарисовался
источник
Ну, на самом деле, если вы переопределяете equals () / hashCode (), тогда вам все равно нужно сгенерировать UUID для каждой сущности (я предполагаю, что вы хотите сохранить каждую сущность, созданную в вашем коде). Вы делаете это только один раз - прежде чем сохранить его в базе данных в первый раз. После этого UUID просто загружается провайдером постоянства. Поэтому я не вижу смысла делать это лениво.
Андрей Андрей Листочкин
Я проголосовал за ваш ответ, потому что мне действительно нравятся ваши другие идеи: хранить UUID в виде пары чисел в базе данных, а не приводить к определенному типу внутри метода equals () - этот вариант действительно хорош! Я определенно буду использовать эти два трюка в будущем.
Андрей Андрей Листочкин
1
Спасибо за голосование. Причина ленивой инициализации UUID заключалась в том, что в нашем приложении мы создали множество сущностей, которые никогда не помещаются в HashMap или сохраняются. Таким образом, мы увидели снижение производительности в 100 раз при создании объекта (100 000 из них). Таким образом, мы только инициируем UUID, если это необходимо. Я просто хотел бы, чтобы в MySql была хорошая поддержка 128-битных чисел, чтобы мы могли просто использовать UUID для id и не заботиться об auto_increment.
Дрю
О, я вижу. В моем случае мы даже не объявляем поле UUID, если соответствующая сущность не будет помещена в коллекции. Недостаток в том, что иногда мы должны добавить его, потому что позже оказывается, что нам действительно нужно поместить их в коллекции. Это иногда случается во время разработки, но, к счастью, никогда не случалось с нами после первоначального развертывания для клиента, так что это не имело большого значения. Если это произойдет после того, как система заработает, нам потребуется миграция БД. Ленивые UUID очень помогают в таких ситуациях.
Андрей Андрей Листочкин
Возможно, вам также следует попробовать более быстрый генератор UUID, который Адам предложил в своем ответе, если производительность является критической проблемой в вашей ситуации.
Андрей Андрей Листочкин
3

Как уже указывали другие люди, умнее меня, существует множество стратегий. Похоже, что дело в том, что большинство применяемых шаблонов проектирования пытаются взломать свой путь к успеху. Они ограничивают доступ к конструктору, если не мешают вызовам конструктора полностью с помощью специализированных конструкторов и фабричных методов. Действительно, это всегда приятно с четким API. Но если единственная причина состоит в том, чтобы сделать равные и хэш-коды совместимыми с приложением, то мне интересно, соответствуют ли эти стратегии KISS (Keep It Simple Stupid).

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

Тем не менее, предостережение: если ваши сущности хранятся в разных таблицах и ваш провайдер использует стратегию автоматической генерации для первичного ключа, вы получите дублированные первичные ключи для разных типов сущностей. В этом случае также сравнивайте типы времени выполнения с вызовом Object # getClass (), что, конечно, сделает невозможным, чтобы два разных типа считались равными. Это подходит мне просто отлично по большей части.

Мартин Андерссон
источник
Даже если в БД отсутствуют последовательности (например, Mysql), их можно смоделировать (например, таблица hibernate_sequence). Таким образом, вы всегда можете получить уникальный идентификатор для разных таблиц. +++ Но тебе это не нужно. Звонить Object#getClass() плохо из-за прокси-серверов H. Вызов Hibernate.getClass(o)помогает, но проблема равенства сущностей разных видов остается. Есть решение, использующее canEqual , немного сложное, но пригодное для использования. Договорились, что обычно это не нужно. +++ Ввод eq / hc на нулевой идентификатор нарушает договор, но это очень прагматично.
Maaartinus
2

Здесь, очевидно, уже есть очень информативные ответы, но я расскажу вам, что мы делаем.

Мы ничего не делаем (т.е. не переопределяем).

Если нам нужен equals / hashcode для работы с коллекциями, мы используем UUID. Вы просто создаете UUID в конструкторе. Мы используем http://wiki.fasterxml.com/JugHome для UUID. UUID немного дороже с точки зрения процессора, но дешевле по сравнению с сериализацией и доступом к БД.

Адам Гент
источник
1

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

Однако в следующий раз я могу попробовать вариант 2 - с использованием идентификатора, сгенерированного базой данных.

Хэш-код и equals вызовут IllegalStateException, если идентификатор не установлен.

Это предотвратит неожиданные ошибки, связанные с несохраненными объектами.

Что люди думают об этом подходе?

Нил Стивенс
источник
1

Подход бизнес-ключей нам не подходит. Мы используем ID , сгенерированный БД , временный переходный процесс tempId и переопределение equal () / hashcode () для решения дилеммы Все сущности являются потомками сущностей. Плюсы:

  1. Нет дополнительных полей в БД
  2. Никакого дополнительного кодирования в потомках, один подход для всех
  3. Нет проблем с производительностью (как с UUID), генерация идентификатора БД
  4. Нет проблем с Hashmaps (не нужно помнить об использовании равных и т. Д.)
  5. Хеш-код нового объекта не изменился во времени даже после сохранения

Минусы:

  1. Могут быть проблемы с сериализацией и десериализацией несохраняемых объектов
  2. Хеш-код сохраненного объекта может измениться после перезагрузки из БД
  3. Не сохранившиеся объекты считаются всегда разными (может, это правильно?)
  4. Что еще?

Посмотрите на наш код:

@MappedSuperclass
abstract public class Entity implements Serializable {

    @Id
    @GeneratedValue
    @Column(nullable = false, updatable = false)
    protected Long id;

    @Transient
    private Long tempId;

    public void setId(Long id) {
        this.id = id;
    }

    public Long getId() {
        return id;
    }

    private void setTempId(Long tempId) {
        this.tempId = tempId;
    }

    // Fix Id on first call from equal() or hashCode()
    private Long getTempId() {
        if (tempId == null)
            // if we have id already, use it, else use 0
            setTempId(getId() == null ? 0 : getId());
        return tempId;
    }

    @Override
    public boolean equals(Object obj) {
        if (super.equals(obj))
            return true;
        // take proxied object into account
        if (obj == null || !Hibernate.getClass(obj).equals(this.getClass()))
            return false;
        Entity o = (Entity) obj;
        return getTempId() != 0 && o.getTempId() != 0 && getTempId().equals(o.getTempId());
    }

    // hash doesn't change in time
    @Override
    public int hashCode() {
        return getTempId() == 0 ? super.hashCode() : getTempId().hashCode();
    }
}
Демел
источник
1

Пожалуйста, рассмотрите следующий подход, основанный на предопределенном идентификаторе типа и идентификаторе.

Конкретные предположения для JPA:

  • объекты одного и того же типа и одинакового ненулевого идентификатора считаются равными
  • непостоянные сущности (при условии отсутствия идентификатора) никогда не равны другим сущностям

Абстрактная сущность:

@MappedSuperclass
public abstract class AbstractPersistable<K extends Serializable> {

  @Id @GeneratedValue
  private K id;

  @Transient
  private final String kind;

  public AbstractPersistable(final String kind) {
    this.kind = requireNonNull(kind, "Entity kind cannot be null");
  }

  @Override
  public final boolean equals(final Object obj) {
    if (this == obj) return true;
    if (!(obj instanceof AbstractPersistable)) return false;
    final AbstractPersistable<?> that = (AbstractPersistable<?>) obj;
    return null != this.id
        && Objects.equals(this.id, that.id)
        && Objects.equals(this.kind, that.kind);
  }

  @Override
  public final int hashCode() {
    return Objects.hash(kind, id);
  }

  public K getId() {
    return id;
  }

  protected void setId(final K id) {
    this.id = id;
  }
}

Пример конкретного объекта:

static class Foo extends AbstractPersistable<Long> {
  public Foo() {
    super("Foo");
  }
}

Тестовый пример:

@Test
public void test_EqualsAndHashcode_GivenSubclass() {
  // Check contract
  EqualsVerifier.forClass(Foo.class)
    .suppress(Warning.NONFINAL_FIELDS, Warning.TRANSIENT_FIELDS)
    .withOnlyTheseFields("id", "kind")
    .withNonnullFields("id", "kind")
    .verify();
  // Ensure new objects are not equal
  assertNotEquals(new Foo(), new Foo());
}

Основные преимущества здесь:

  • простота
  • гарантирует, что подклассы обеспечивают идентичность типа
  • прогнозируемое поведение с прокси-классами

Недостатки:

  • Требуется, чтобы каждый объект звонил super()

Ноты:

  • Необходимо внимание при использовании наследования. Например, равенство экземпляров class Aи class B extends Aможет зависеть от конкретных деталей приложения.
  • В идеале используйте бизнес-ключ в качестве идентификатора

Ждем ваших комментариев.

Окс
источник
0

Это общая проблема в каждой ИТ-системе, которая использует Java и JPA. Болевая точка выходит за рамки реализации equals () и hashCode (), она влияет на то, как организация ссылается на сущность и как ее клиенты ссылаются на одну и ту же сущность. Я видел достаточно боли, когда у меня не было бизнес-ключа, и я написал свой блог, чтобы выразить свою точку зрения.

Вкратце: используйте короткий, понятный человеку последовательный идентификатор со значимыми префиксами в качестве бизнес-ключа, который генерируется без какой-либо зависимости от какого-либо хранилища, кроме ОЗУ. Снежинка Твиттера - очень хороший пример.

Кристофер Ян
источник
0

ИМО у вас есть 3 варианта реализации equals / hashCode

  • Используйте идентификатор, сгенерированный приложением, т.е. UUID
  • Реализуйте его на основе бизнес-ключа
  • Реализуйте его на основе первичного ключа

Использование идентификатора, сгенерированного приложением, является самым простым подходом, но имеет несколько недостатков

  • Соединения медленнее при использовании его в качестве PK, потому что 128-битный просто больше, чем 32 или 64-битный
  • «Отладка сложнее», потому что проверить собственными глазами, какие данные верны, довольно сложно

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

Чтобы преодолеть проблему объединения, можно использовать UUID в качестве естественного ключа и значение последовательности в качестве первичного ключа, но тогда вы все равно можете столкнуться с проблемами реализации equals / hashCode в составных дочерних объектах, которые имеют встроенные идентификаторы, поскольку вы захотите присоединиться на основе объединения. на первичном ключе. Использование естественного ключа в дочерних сущностях id и первичного ключа для обращения к родителю является хорошим компромиссом.

@Entity class Parent {
  @Id @GeneratedValue Long id;
  @NaturalId UUID uuid;
  @OneToMany(mappedBy = "parent") Set<Child> children;
  // equals/hashCode based on uuid
}

@Entity class Child {
  @EmbeddedId ChildId id;
  @ManyToOne Parent parent;

  @Embeddable class ChildId {
    UUID parentUuid;
    UUID childUuid;
    // equals/hashCode based on parentUuid and childUuid
  }
  // equals/hashCode based on id
}

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

Реализуйте его на основе бизнес-ключа, если вы ожидаете, что от пользователя это хорошая идея, но также есть несколько недостатков

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

  • Объединения медленнее, потому что объединение на основе текста переменной длины просто медленное. Некоторые СУБД могут даже иметь проблемы при создании индекса, если ключ превышает определенную длину.
  • По моему опыту, бизнес-ключи имеют тенденцию меняться, что потребует каскадного обновления объектов, ссылающихся на него. Это невозможно, если внешние системы ссылаются на него

ИМО, вы не должны внедрять или работать исключительно с бизнес-ключом. Это хорошее дополнение, т.е. пользователи могут быстро выполнять поиск по этому бизнес-ключу, но система не должна полагаться на него для работы.

Реализация его на основе первичного ключа имеет свои проблемы, но, возможно, это не такая уж большая проблема

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

Очевидный способ обойти это - просто не добавлять объект в коллекции, основанные на хэше, до назначения идентификатора. Я понимаю, что это не всегда возможно, потому что вы могли бы хотеть дедупликации прежде, чем назначить идентификатор уже. Чтобы по-прежнему иметь возможность использовать коллекции на основе хеша, вам просто нужно перестроить коллекции после назначения идентификатора.

Вы могли бы сделать что-то вроде этого:

@Entity class Parent {
  @Id @GeneratedValue Long id;
  @OneToMany(mappedBy = "parent") Set<Child> children;
  // equals/hashCode based on id
}

@Entity class Child {
  @EmbeddedId ChildId id;
  @ManyToOne Parent parent;

  @PrePersist void postPersist() {
    parent.children.remove(this);
  }
  @PostPersist void postPersist() {
    parent.children.add(this);
  }

  @Embeddable class ChildId {
    Long parentId;
    @GeneratedValue Long childId;
    // equals/hashCode based on parentId and childId
  }
  // equals/hashCode based on id
}

Я сам не проверял точный подход, поэтому не уверен, как работает изменение коллекций в до и после персистентных событиях, но идея такова:

  • Временно удалить объект из коллекций на основе хеша
  • Сохраняй это
  • Повторно добавьте объект в коллекции на основе хеша

Другой способ решения этой проблемы - просто перестроить все ваши модели, основанные на хэше, после обновления / сохранения.

В конце концов, решать вам. Я лично использую подход, основанный на последовательностях, большую часть времени и использую UUID только тогда, когда мне нужно предоставить идентификатор внешним системам.

Кристиан Бейков
источник
0

С новым стилем instanceofиз Java 14, вы можете реализовать equalsв одной строке.

@Override
public boolean equals(Object obj) {
    return this == obj || id != null && obj instanceof User otherUser && id.equals(otherUser.id);
}

@Override
public int hashCode() {
    return 31;
}
Артем
источник
-1

Если UUID является ответом для многих людей, почему бы нам просто не использовать фабричные методы из бизнес-уровня для создания сущностей и назначения первичного ключа во время создания?

например:

@ManagedBean
public class MyCarFacade {
  public Car createCar(){
    Car car = new Car();
    em.persist(car);
    return car;
  }
}

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

Мы также можем объявить конструкторы Car защищенными, а затем использовать отражение в нашем бизнес-методе для доступа к ним. Таким образом, разработчики не будут стремиться создать экземпляр Car с новым, но с помощью заводского метода.

Как насчет этого?

illEatYourPuppies
источник
Подход, который отлично работает, если вы хотите снизить производительность при создании guid при поиске в базе данных.
Майкл Уайлс
1
Как насчет юнит-тестирования автомобиля? В этом случае вам нужно подключение к базе данных для тестирования? Кроме того, ваши доменные объекты не должны зависеть от персистентности.
Джегедус
-1

Я пытался ответить на этот вопрос сам и никогда не был полностью доволен найденными решениями, пока не прочитал этот пост и особенно DREW. Мне понравилось, как он ленивый создал UUID и оптимально сохранил его.

Но я хотел добавить еще больше гибкости, то есть ленивое создание UUID ТОЛЬКО при обращении к hashCode () / equals () до первого сохранения сущности с преимуществами каждого решения:

  • equals () означает «объект относится к одной и той же логической сущности»
  • используйте как можно больше идентификатора базы данных, потому что зачем мне делать работу дважды (проблема производительности)
  • предотвратить проблему при доступе к hashCode () / equals () для еще не сохраненного объекта и сохранить то же поведение после того, как он действительно сохранится

Я бы очень признателен за отзыв о моем смешанном решении ниже

public class MyEntity { 

    @Id()
    @Column(name = "ID", length = 20, nullable = false, unique = true)
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id = null;

    @Transient private UUID uuid = null;

    @Column(name = "UUID_MOST", nullable = true, unique = false, updatable = false)
    private Long uuidMostSignificantBits = null;
    @Column(name = "UUID_LEAST", nullable = true, unique = false, updatable = false)
    private Long uuidLeastSignificantBits = null;

    @Override
    public final int hashCode() {
        return this.getUuid().hashCode();
    }

    @Override
    public final boolean equals(Object toBeCompared) {
        if(this == toBeCompared) {
            return true;
        }
        if(toBeCompared == null) {
            return false;
        }
        if(!this.getClass().isInstance(toBeCompared)) {
            return false;
        }
        return this.getUuid().equals(((MyEntity)toBeCompared).getUuid());
    }

    public final UUID getUuid() {
        // UUID already accessed on this physical object
        if(this.uuid != null) {
            return this.uuid;
        }
        // UUID one day generated on this entity before it was persisted
        if(this.uuidMostSignificantBits != null) {
            this.uuid = new UUID(this.uuidMostSignificantBits, this.uuidLeastSignificantBits);
        // UUID never generated on this entity before it was persisted
        } else if(this.getId() != null) {
            this.uuid = new UUID(this.getId(), this.getId());
        // UUID never accessed on this not yet persisted entity
        } else {
            this.setUuid(UUID.randomUUID());
        }
        return this.uuid; 
    }

    private void setUuid(UUID uuid) {
        if(uuid == null) {
            return;
        }
        // For the one hypothetical case where generated UUID could colude with UUID build from IDs
        if(uuid.getMostSignificantBits() == uuid.getLeastSignificantBits()) {
            throw new Exception("UUID: " + this.getUuid() + " format is only for internal use");
        }
        this.uuidMostSignificantBits = uuid.getMostSignificantBits();
        this.uuidLeastSignificantBits = uuid.getLeastSignificantBits();
        this.uuid = uuid;
    }
user2083808
источник
что вы подразумеваете под "UUID, который был сгенерирован для этой сущности за день до того, как я был сохранен"? Не могли бы вы привести пример для этого случая?
Джегедус
Вы могли бы использовать назначенный тип поколения? зачем нужен тип генерации личности? у него есть какое-то преимущество перед назначенным?
Джегедус
что произойдет, если вы 1) создадите новый MyEntity, 2) поместите его в список, 3) затем сохраните его в базу данных, затем 4) загрузите этот объект обратно из БД и 5) попытаетесь проверить, есть ли загруженный экземпляр в списке , Я думаю, что это не будет, хотя это должно быть.
Джегедус
Спасибо за ваши первые комментарии, которые показали мне, что я не так ясно, как следовало бы. Во-первых, «UUID, сгенерированный для этой сущности за один день до того, как я был сохранен», был опечаткой ... «до того, как это было сохранено», вместо этого нужно было прочитать. Что касается других замечаний, я скоро отредактирую свой пост, чтобы попытаться лучше объяснить свое решение.
user2083808
-1

На практике кажется, что вариант 2 (первичный ключ) используется чаще всего. Естественный и НЕМЕРТОВЫЙ бизнес-ключ - это редко, создание и поддержка синтетических ключей слишком сложны для решения ситуаций, которых, вероятно, никогда не было. Взгляните на реализацию Spring-data-jpa AbstractPersistable (единственное: для использования реализации HibernateHibernate.getClass ).

public boolean equals(Object obj) {
    if (null == obj) {
        return false;
    }
    if (this == obj) {
        return true;
    }
    if (!getClass().equals(ClassUtils.getUserClass(obj))) {
        return false;
    }
    AbstractPersistable<?> that = (AbstractPersistable<?>) obj;
    return null == this.getId() ? false : this.getId().equals(that.getId());
}

@Override
public int hashCode() {
    int hashCode = 17;
    hashCode += null == getId() ? 0 : getId().hashCode() * 31;
    return hashCode;
}

Просто в курсе манипулирования новыми объектами в HashSet / HashMap. Напротив, Вариант 1 (остается Objectреализация) нарушается сразу после merge, это очень распространенная ситуация.

Если у вас нет бизнес-ключа и у вас есть REAL, вам нужно манипулировать новым объектом в хэш-структуре, переопределите hashCodeего как константу, как показано ниже.

Григорий Кислин
источник
-2

Ниже приведено простое (и проверенное) решение для Scala.

  • Обратите внимание, что это решение не вписывается ни в одну из 3 категорий, приведенных в вопросе.

  • Все мои сущности являются подклассами UUIDEntity, поэтому я следую принципу «не повторяй себя» (СУХОЙ).

  • При необходимости генерацию UUID можно сделать более точной (используя больше псевдослучайных чисел).

Скала код:

import javax.persistence._
import scala.util.Random

@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
abstract class UUIDEntity {
  @Id  @GeneratedValue(strategy = GenerationType.TABLE)
  var id:java.lang.Long=null
  var uuid:java.lang.Long=Random.nextLong()
  override def equals(o:Any):Boolean= 
    o match{
      case o : UUIDEntity => o.uuid==uuid
      case _ => false
    }
  override def hashCode() = uuid.hashCode()
}
jhegedus
источник