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

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

289 голосов
/ 17 февраля 2011

Были некоторые обсуждения здесь о сущностях 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. Я пропустил опцию и / или pro / con точку?
  2. Какой вариант вы выбрали и почему?



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

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

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

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

Ответы [ 19 ]

110 голосов
/ 20 апреля 2011

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

Вывод статьи выглядит так:

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

62 голосов
/ 17 февраля 2011

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

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

EDIT :

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

  1. Я обычно не использую хеш-коллекцию (HashMap / HashSet) в своем приложении JPA. Если я должен, я предпочитаю создать решение UniqueList.
  2. Я думаю, что изменение бизнес-идентификатора во время выполнения не рекомендуется для любого приложения базы данных. В редких случаях, когда нет другого решения, я бы сделал особую обработку, например, удалил элемент и поместил его обратно в коллекцию на основе хеширования.
  3. Для моей модели я устанавливаю бизнес-идентификатор в конструкторе и не предоставляю для него установщики. Я позволил реализации JPA изменить поле вместо свойства.
  4. UUID решение кажется излишним. Зачем UUID, если у вас есть естественный бизнес-идентификатор? Я бы все-таки установил уникальность бизнес-идентификатора в базе данных. Почему тогда THREE индексов для каждой таблицы в базе данных?
33 голосов
/ 24 февраля 2011

У нас обычно есть два идентификатора:

  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

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

28 голосов
/ 17 февраля 2011

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

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

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

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

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

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

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

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

Это верно, , если you

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

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

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

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

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


Короткая версия:

  • Вариант 1 можно использовать только с объектами в пределах одного сеанса.
  • Если вы можете, используйте Вариант 2. (Назначьте PK как можно раньше, потому что вы не можете использовать объекты в наборах до тех пор, пока PKназначен.)
  • Если вы можете гарантировать относительную стабильность, вы можете использовать Вариант 3. Но будьте осторожны с этим.
27 голосов
/ 17 февраля 2011

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

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


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

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

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

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


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

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

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

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


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

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

(-) накладные расходы на генерацию UUID

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

12 голосов
/ 09 ноября 2014
  1. Если у вас есть бизнес-ключ , то вы должны использовать его для equals / hashCode.
  2. Если у вас нет бизнес-ключа, выне следует оставлять по умолчанию Object equals и 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
    }
    
10 голосов
/ 13 сентября 2013

Хотя использование бизнес-ключа (вариант 3) является наиболее рекомендуемым подходом ( Вики-сообщество Hibernate , «Сохранение Java с Hibernate», стр. 398), и это то, что мы в основном используем, есть Ошибка в спящем режиме, которая нарушает эту ситуацию для загруженных наборов: HHH-3799 . В этом случае Hibernate может добавить объект в набор до инициализации его полей. Я не уверен, почему эта ошибка не получила большего внимания, так как это действительно делает рекомендуемый подход бизнес-ключом проблематичным.

Я думаю, что суть дела в том, что equals и hashCode должны основываться на неизменяемом состоянии (ссылка Odersky et al. ), а сущность Hibernate с управляемым Hibernate первичным ключом имеет no такое неизменное состояние. Первичный ключ изменяется в Hibernate, когда временный объект становится постоянным. Бизнес-ключ также изменяется в Hibernate, когда он гидратирует объект в процессе инициализации.

Это оставляет только вариант 1, наследующий реализации java.lang.Object, основанный на идентичности объекта, или использующий первичный ключ, управляемый приложением, как предложено Джеймсом Брундегом в «Не позволяйте Hibernate украсть вашу личность» (уже упоминавшийся в ответе Стейна Геккенса) и Лэнса Арлауса в «Генерация объектов: лучший подход к интеграции в спящий режим» .

Самая большая проблема с вариантом 1 заключается в том, что отдельные экземпляры нельзя сравнивать с постоянными экземплярами с помощью .equals (). Но это нормально; Контракт equals и hashCode оставляют на усмотрение разработчика решать, что означает равенство для каждого класса. Так что пусть equals и hashCode наследуются от Object. Если вам нужно сравнить отдельный экземпляр с постоянным экземпляром, вы можете явно создать новый метод для этой цели, например boolean sameEntity или boolean dbEquivalent или boolean businessEquals.

5 голосов
/ 27 февраля 2011

Я согласен с ответом Андрея. Мы делаем то же самое в нашем приложении, но вместо того, чтобы хранить 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();
    }

}
3 голосов
/ 05 мая 2013

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

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

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

2 голосов
/ 21 февраля 2011

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

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

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

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