Как должны быть реализованы равенства и хэш-код при использовании JPA и Hibernate

Как в Hibernate должны быть реализованы равенства классов модели и хэш-код? Каковы общие подводные камни? Подходит ли реализация по умолчанию для большинства случаев? Есть ли смысл использовать бизнес-ключи?

Мне кажется, что довольно сложно заставить его работать в любой ситуации, когда учитываются ленивая выборка, генерация идентификатора, прокси и т. Д.


person egaga    schedule 28.10.2009    source источник
comment
См. Также stackoverflow.com/a/39827962/548473 (реализация spring-data-jpa)   -  person Grigory Kislin    schedule 03.10.2016


Ответы (8)


В Hibernate есть красивое и длинное описание того, когда и как переопределять equals() / hashCode() в документация

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

  1. Основание equals() / hashCode() на бизнес-ключе - например, уникальная комбинация атрибутов, которая не изменится в течение жизни объекта (или, по крайней мере, сеанса).
  2. Если это невозможно, используйте equals() / hashCode() первичный ключ, ЕСЛИ он установлен, и идентификатор объекта / System.identityHashCode() в противном случае. Важная часть здесь заключается в том, что вам необходимо перезагрузить свой набор после того, как в него был добавлен и сохранен новый объект; в противном случае вы можете столкнуться со странным поведением (что в конечном итоге приведет к ошибкам и / или повреждению данных), потому что ваша сущность может быть размещена в корзине, не соответствующей ее текущему hashCode().
person ChssPly76    schedule 28.10.2009
comment
Когда вы говорите перезагрузить @ ChssPly76, вы имеете в виду refresh()? Каким образом ваша сущность, которая подчиняется Set контракту, оказывается в неправильном ведре (при условии, что у вас достаточно хорошая реализация хэш-кода). - person non sequitor; 28.10.2009
comment
Обновить коллекцию или перезагрузить всю сущность (владельца), да. Что касается неправильного ведра: а) вы добавляете новую сущность для установки, ее идентификатор еще не установлен, поэтому вы используете identityHashCode, который помещает вашу сущность в корзину №1. б) ваша сущность (в наборе) сохраняется, теперь у нее есть идентификатор, и поэтому вы используете hashCode () на основе этого идентификатора. Он отличается от приведенного выше, и поместил бы вашу сущность в корзину №2. Теперь, предполагая, что у вас есть ссылка на этот объект в другом месте, попробуйте позвонить Set.contains(entity), и вы вернетесь false. То же самое и с get () / put () / etc ... - person ChssPly76; 28.10.2009
comment
Имеет смысл, но я никогда не использовал identityHashCode, хотя я вижу, что он используется в источнике Hibernate, как в их ResultTransformers - person non sequitor; 29.10.2009
comment
При использовании Hibernate вы также можете столкнуться с этой проблемой, о которой я до сих пор не знаю » т нашел решение. - person Giovanni Botta; 18.07.2013
comment
@ ChssPly76 Из-за бизнес-правил, которые определяют, равны ли два объекта, мне нужно будет основывать свои методы equals / hashcode на свойствах, которые могут измениться в течение жизни объекта. Это действительно большое дело? Если да, то как мне это обойти? - person ubiquibacon; 21.11.2013
comment
Я не видел никаких доказательств того, что при обратном присоединении объекта к сеансу требуется equals / hashCode. Я думаю, что документ устарел. Если только у кого-то нет доказательств обратного .. - person Stanislav Bashkyrtsev; 11.11.2015

Не думаю, что принятый ответ верен.

Чтобы ответить на исходный вопрос:

Подходит ли реализация по умолчанию для большинства случаев?

Ответ - да, в большинстве случаев это так.

Вам нужно только переопределить equals() и hashcode(), если объект будет использоваться в Set (что очень часто) И объект будет отсоединен от сеансов гибернации и впоследствии повторно присоединен к ним (что является необычное использование спящего режима).

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

person Phil    schedule 29.10.2012
comment
Это согласуется с моим наблюдением, пора узнать почему. - person Vlastimil Ovčáčík; 11.02.2016
comment
Вам нужно только переопределить equals () и hashcode (), если сущность будет использоваться в Set, вполне достаточно, если некоторые поля идентифицируют объект, и поэтому вы не хотите полагаться на Object.equals () для идентификации объектов. - person davidxxx; 12.11.2017

Лучшая реализация equals и hashCode - это использование уникального бизнес-ключа или естественного идентификатора, например:

@Entity
public class Company {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
 
    @Column(unique = true, updatable = false)
    private String name;
 
    @Override
    public int hashCode() {
        HashCodeBuilder hcb = new HashCodeBuilder();
        hcb.append(name);
        return hcb.toHashCode();
    }
 
    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (!(obj instanceof Company)) {
            return false;
        }
        Company that = (Company) obj;
        EqualsBuilder eb = new EqualsBuilder();
        eb.append(name, that.name);
        return eb.isEquals();
    }
}

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

Другой вариант - перейти на использование идентификаторов UUID, назначаемых логикой приложения. Таким образом, вы можете использовать UUID для _4 _ / _ 5_, потому что идентификатор назначается до того, как объект будет сброшен.

Вы даже можете использовать идентификатор объекта для equals и hashCode, но это требует, чтобы вы всегда возвращали одно и то же значение [hashCode, чтобы убедиться, что значение hashCode объекта согласовано при всех переходах состояния объекта, например:

@Entity(name = "Post")
@Table(name = "post")
public class Post implements Identifiable<Long> {
 
    @Id
    @GeneratedValue
    private Long id;
 
    private String title;
 
    public Post() {}
 
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
 
        if (!(o instanceof Post))
            return false;
 
        Post other = (Post) o;
 
        return id != null &&
               id.equals(other.getId());
    }
 
    @Override
    public int hashCode() {
        return getClass().hashCode();
    }
  
    //Getters and setters omitted for brevity
}
person Vlad Mihalcea    schedule 27.06.2014
comment
+1 за подход uuid. Поместите это в BaseEntity и никогда больше не думайте об этой проблеме. Это занимает немного места на стороне db, но эту цену вам лучше заплатить за комфорт :) - person Martin Frey; 25.08.2016

Когда сущность загружается посредством отложенной загрузки, это не экземпляр базового типа, а динамически сгенерированный подтип, созданный javassist, поэтому проверка того же типа класса завершится неудачно, поэтому не используйте:

if (getClass() != that.getClass()) return false;

вместо этого используйте:

if (!(otherObject instanceof Unit)) return false;

что также является хорошей практикой, как описано в Реализация равенства в практиках Java .

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

person stivlo    schedule 10.01.2013
comment
Это работает, если вы сравниваете объекты конкретных классов, что в моей ситуации не сработало. Я сравнивал объекты суперклассов, и в этом случае у меня сработал этот код: obj1.getClass (). IsInstance (obj2) - person Tad; 07.08.2015

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

person Carlos    schedule 28.10.2009
comment
Я думаю, что мы использовали идентификацию объекта в том случае, если id не был сгенерирован. - person Kathy Van Stone; 28.10.2009
comment
проблема здесь в том, что если вы сохраните объект, ваш хэш-код изменится. Это может иметь большие пагубные последствия, если объект уже является частью структуры данных на основе хешей. Итак, если вы все же закончите использовать идентификатор объекта, вам лучше продолжать использовать идентификатор объекта, пока объект не будет полностью освобожден (или удалите объект из любых структур на основе хэша, сохраните его, а затем добавьте его обратно). Лично я думаю, что было бы лучше не использовать id и основывать хэш на неизменяемых свойствах объекта. - person Kevin Day; 29.10.2009

В документации Hibernate 5.2 говорится, что вы, возможно, вообще не захотите реализовывать hashCode и equals - в зависимости от вашей ситуации.

https://docs.jboss.org/hibernate/orm/5.2/userguide/html_single/Hibernate_User_Guide.html#mapping-model-pojo-equalshashcode.

Как правило, два объекта, загруженные из одного сеанса, будут равны, если они равны в базе данных (без реализации hashCode и equals).

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

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

person Nina    schedule 20.10.2016

Здесь есть очень хорошая статья: https://docs.jboss.org/hibernate/stable/core.old/reference/en/html/persistent-classes-equalshashcode.html.

Цитируя важную строчку из статьи:

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

Проще говоря

public class Cat {

...
public boolean equals(Object other) {
    //Basic test / class cast
    return this.catId==other.catId;
}

public int hashCode() {
    int result;

    return 3*this.catId; //any primenumber 
}

}
person Ravi Shekhar    schedule 02.01.2016

Если вам довелось переопределить equals, убедитесь, что вы выполнили его контракты: -

  • СИММЕТРИЯ
  • ОТРАЖАТЕЛЬНЫЙ
  • ПЕРЕХОДНЫЙ
  • ПОСЛЕДОВАТЕЛЬНЫЙ
  • NON NULL

И переопределить hashCode, поскольку его контракт зависит от реализации equals.

Джошуа Блох (разработчик фреймворка Collection) настоятельно призывал следовать этим правилам.

  • пункт 9. Всегда переопределять hashCode, когда вы переопределяете равно

Несоблюдение этих договоров чревато серьезными непредвиденными последствиями. Например, List#contains(Object o) может вернуть неверное значение boolean, поскольку общий договор не выполнен.

person Awan Biru    schedule 02.03.2016