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

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

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

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

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

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

Мои вопросы:

  • Я пропустил опцию и/или про/точку подключения?
  • Какой вариант вы выбрали и почему?



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

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

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

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

Ответ 1

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

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

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

Ответ 2

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

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

ИЗМЕНИТЬ

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

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

Ответ 3

Обычно мы имеем два идентификатора в наших сущностях:

  • Используется только для уровня сохранения (так что поставщик устойчивости и база данных могут определять отношения между объектами).
  • Для наших приложений (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 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 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 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("[email protected]");

jediSet.add(user); // here 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

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

Ответ 4

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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

Ответ 5

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

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


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

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

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

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


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

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

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

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


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

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

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

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

Ответ 6

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

Ответ 7

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

Ответ 8

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

}

Ответ 9

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

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

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

Ответ 10

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

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

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

Ответ 11

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

  • Никаких дополнительных полей в БД
  • Отсутствие дополнительного кодирования в сущности потомков, один подход для всех
  • Отсутствие проблем с производительностью (например, с UUID), генерация идентификатора базы данных
  • Нет проблем с Hashmaps (не нужно иметь в виду использование равных и т.д.)
  • Hashcode новой сущности не изменяется во времени даже после сохранения

Минусы:

  • Возможно, возникли проблемы с сериализацией и десериализацией не сохраняемых объектов
  • Hashcode сохраненного объекта может измениться после перезагрузки из DB
  • Не сохраняемые объекты считаются всегда разными (возможно, это правильно?)
  • Что еще?

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

@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();
    }
}

Ответ 12

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

Конкретные предположения для 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 может зависеть от конкретных деталей приложения.
  • В идеале используйте бизнес-ключ в качестве идентификатора

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

Ответ 13

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

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

Hashcode и equals будут вызывать IllegalStateException, если идентификатор не установлен.

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

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

Ответ 14

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

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

Ответ 15

ИМО у вас есть 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
}

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

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

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

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

Ответ 16

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

например:

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

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

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

Как это?

Ответ 17

Я попытался ответить на этот вопрос сам и никогда не был полностью доволен найденными решениями, пока не прочитал этот пост и особенно 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;
    }

Ответ 18

На практике кажется, что вариант 2 (первичный ключ) наиболее часто используется. Естественный и ИМПУЛЬСНЫЙ бизнес-ключ редкость, создание и поддержка синтетических ключей слишком тяжелы для решения ситуаций, которых, вероятно, никогда не было. Посмотрите spring -data-jpa AbstractPersistable (единственное: для использования Hibernate Hibernate.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 на константу, как указано ниже Влад Михальче.

Ответ 19

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

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

  • Все мои объекты - это подклассы UUIDEntity, поэтому я следую принцип не-повторитель (DRY).

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

Scala Код:

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()
}