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

Здесь были некоторые дискуссии о субъектах 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:

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

22 ответа

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

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

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

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

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

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

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

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

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

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


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

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

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

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


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

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

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

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


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

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

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

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

Если вы хотите использовать 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. Но будьте осторожны с этим.

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

  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

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

  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
    }
    

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

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

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

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

Я согласен с ответом Андрея. Мы делаем то же самое в нашем приложении, но вместо того, чтобы хранить 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 в конструкторе. Мы используем http://wiki.fasterxml.com/JugHome для UUID. UUID немного дороже с точки зрения процессора, но дешевле по сравнению с сериализацией и доступом к БД.

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

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

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

ИМО у вас есть 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, только если мне нужно предоставить идентификатор внешним системам.

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

  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 в прошлом, потому что я знал об этих обсуждениях и думал, что лучше ничего не делать, пока я не узнаю, что нужно делать. Эти системы все еще успешно работают.

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

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

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

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

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

      /**
 * @author marcos.oliveira
 */
@MappedSuperclass
public abstract class EntityBase<TId extends Serializable> implements Serializable{
    /**
     *
     */
    private static final long serialVersionUID = 1L;

    @Id
    @Column(name = "id", unique = true, nullable = false)
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    protected TId id;



    public TId getId() {
        return this.id;
    }

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

    @Override
    public int hashCode() {
        return (super.hashCode() * 907) + Objects.hashCode(getId());//this.getId().hashCode();
    }

    @Override
    public String toString() {
        return super.toString() + " [Id=" + id + "]";
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null || getClass() != obj.getClass()) {
            return false;
        }
        EntityBase entity = (EntityBase) obj;
        if (entity.id == null || id == null) {
            return false;
        }
        return Objects.equals(id, entity.id);
    }
}

Ссылка: https://thorben-janssen.com/ultimate-guide-to-implementing-equals-and-hashcode-with-hibernate/

В ответ на ОП два вопроса:

Я пропустил какой-то вариант и/или плюсы/минусы?

Я думаю, вы рассмотрели все варианты/контекст.

Какой вариант вы выбрали и почему?

Я выбрал вариант 2 ОП — переопределить их на основе первичного ключа.

Ниже приведен шаблон, который я использую в Java 16 и выше, где JPA (т. е. база данных) генерирует при первом сохранении (INSERTED) в свой репозиторий.

Ключевое отличие: сохранение и согласованность в отношении двух состояний (иnot null) принадлежащий .

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

Ключевая часть этого заключается в том, что он явно избегает хеширования значения, что всегда приводит к хеш-значению 0.

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

      @Override
public boolean equals(Object o) {
  return (o == this) ||
      ((o instanceof Market that)
          //&& (this.getClass() == that.getClass())
          && ((this.marketId != null) && (that.marketId != null))
          && (this.marketId.intValue() == that.marketId.intValue()));
}

@Override
public int hashCode() {
  return
      this.marketId == null
          ? super.hashCode()
          : Objects.hash(marketId);
}

Более подробную информацию о приведенном выше шаблоне можно найти в ответе StackOverflow.

Примечание. Из-за того, как JPA/Hibernate может обрабатывать прокси, строка&& (this.getClass() == that.getClass())не работает должным образом и закомментировано.

ПРЕДУПРЕЖДЕНИЕ. Предполагается, что объект находится внутри объекта JPA до его сохранения (INSERTed) в первый раз. И что экземпляр модифицируется (т.е. мутирует) во время операции сохранения.

Любое использованиеMarketэкземпляр сущности сnullполе внутри существующего поля или в качестве ключа должно быть «повторно инициализировано» перед дальнейшим использованием.

Поведение (или ) стало неопределенным из-за изменения объекта JPA во время первоначального сохранения (INSERT). Значения, возвращаемые обоими, и будут изменены.

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

И явно регенерировать любые коллекции, чтобы правильно включить новые ненулевыеmarketIdзначение, введенное структурой JPA.

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

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

На практике кажется, что вариант 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 постоянному, как ниже советовал Влад Михалча.

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

например:

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

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

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

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

С новым стилем 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;
}

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

Ниже приведено простое (и проверенное) решение для 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()
}
Другие вопросы по тегам