Как трябва да се прилагат равенства и хешкод при използване на 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. b) вашият обект (в рамките на набора) се запазва, той вече има идентификатор и следователно използвате 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, можете също да се сблъскате с този проблем, за който все още не съм t намери решение. - person Giovanni Botta; 18.07.2013
comment
@ChssPly76 Поради бизнес правилата, които определят дали два обекта са еднакви, ще трябва да базирам моите методи за равенство/хешкод на свойства, които могат да се променят в рамките на живота на обекта. Това наистина ли е голяма работа? Ако е така, как да го заобиколя? - person ubiquibacon; 21.11.2013
comment
Не съм виждал никакви доказателства, че се изисква equals/hashCode при прикачване на обекта обратно към сесия. Мисля, че документът е остарял. Освен ако някой няма доказателство за обратното. - person Stanislav Bashkyrtsev; 11.11.2015
comment
текущата документация се чете доста по-различно от оригиналната версия, започва с това, че обикновено не е необходимо да заменяте equals() и hashCode() изобщо, тъй като Hibernate гарантира еквивалентност на постоянна идентичност (ред на базата данни) и идентичност на Java в определен обхват на сесия .: - person Stefan L; 10.08.2018

Не мисля, че приетият отговор е точен.

За да отговоря на първоначалния въпрос:

Изпълнението по подразбиране достатъчно добро ли е за повечето случаи?

Отговорът е да, в повечето случаи е така.

Трябва да замените equals() и hashcode() само ако обектът ще се използва в Set (което е много често) И обектът ще бъде отделен от и впоследствие отново прикрепен към сесии на хибернация (което е необичайно използване на хибернация).

Приетият отговор показва, че методите трябва да бъдат заменени, ако някое от условие е вярно.

person Phil    schedule 29.10.2012
comment
Това е в съответствие с моето наблюдение, време е да разберете защо. - person Vlastimil Ovčáčík; 11.02.2016
comment
Трябва само да замените equals() и hashcode(), ако обектът ще бъде използван в набор, е напълно достатъчно, ако някои полета идентифицират обект и затова не искате да разчитате на 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();
    }
}

Бизнес ключът трябва да е последователен при всички преходи на състояние на обект (преходно, прикачено, отделено, премахнато), ето защо не можете да разчитате на id за равенство.

Друг вариант е да преминете към използване на UUID идентификатори, зададени от логиката на приложението. По този начин можете да използвате UUID за equals/hashCode, защото идентификаторът се присвоява преди обектът да бъде изчистен.

Можете дори да използвате идентификатора на обекта за 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 Practices .

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

person stivlo    schedule 10.01.2013
comment
Това работи, ако сравнявате обекти от конкретни класове, което не работи в моята ситуация. Сравнявах обекти на супер класове, в който случай този код работи за мен: obj1.getClass().isInstance(obj2) - person Tad; 07.08.2015

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

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

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

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, уверете се, че изпълнявате неговите договори:-

  • СИМЕТРИЯ
  • ОТРАЗИТЕЛЕН
  • ПРЕХОДНИ
  • ПОСЛЕДОВАТЕЛЕН
  • НЕ НУЛЕВ

И замени hashCode, тъй като неговият договор разчита на изпълнението на equals.

Джошуа Блок (дизайнер на рамката на колекцията) силно настоя тези правила да се спазват.

  • точка 9: Винаги заменяйте hashCode, когато заменяте равно на

Има сериозен нежелан ефект, когато не спазвате тези договори. Например List#contains(Object o) може да върне грешна стойност boolean, тъй като общият договор не е изпълнен.

person Awan Biru    schedule 02.03.2016