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

Дилемма 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 ]

1 голос
/ 10 апреля 2015

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

  1. Нет дополнительных полей в БД
  2. Нет дополнительного кодирования в дочерних объектах, один подход для всех
  3. Нет проблем с производительностью (как с UUID), идентификатор БДпоколение
  4. Нет проблем с хэш-картами (не нужно помнить об использовании равных и т. д.)
  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 голос
/ 01 апреля 2018

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

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

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

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

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

  @Id @GeneratedValue
  private K id;

  @Transient
  private final String kind;

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

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

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

  public K getId() {
    return id;
  }

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

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

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

Пример теста:

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

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

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

Недостатки:

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

Примечания:

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

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

0 голосов
/ 03 октября 2016

На практике кажется, что вариант 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, что является очень распространенной ситуацией.

Если у вас нет бизнес-ключа и у вас НАСТОЯЩАЯ потребность манипулировать новой сущностью в хэш-структуре, переопределите hashCode на постоянную, как было рекомендовано Владом Михалчеа.

0 голосов
/ 15 октября 2014

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

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

0 голосов
/ 07 февраля 2014

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

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

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

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

Scala Code:

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()
}
0 голосов
/ 05 февраля 2014

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

Но я хотел добавить еще больше гибкости, то есть ленивое создание UUID ТОЛЬКО при обращении к hashCode () / 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;
    }
0 голосов
/ 15 июня 2012

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

например:

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

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

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

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

0 голосов
/ 17 ноября 2018

IMO, у вас есть 3 варианта реализации equals / hashCode

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

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

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

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

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

@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
}

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

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

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

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

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

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

Если вам нужно выставить идентификаторы во внешнюю систему, используйте предложенный мной подход 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
}

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

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

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

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

0 голосов
/ 31 августа 2013

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

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

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

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

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

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