Dylemat hashCode () / equals () JPA

311

Odbyło się tutaj kilka dyskusji na temat jednostek JPA i tego, które hashCode()/ equals()implementację należy zastosować dla klas jednostek JPA. Większość (jeśli nie wszystkie) z nich zależy od Hibernacji, ale chciałbym omówić je neutralnie z implementacją JPA (nawiasem mówiąc, używam EclipseLink).

Wszystkie możliwe wdrożenia mają swoje zalety i wady dotyczące:

  • hashCode()/ equals()zgodność kontraktu (niezmienność) dla List/ Setoperacji
  • Czy można wykryć identyczne obiekty (np. Z różnych sesji, dynamiczne proxy z leniwie załadowanych struktur danych)
  • Czy jednostki zachowują się poprawnie w stanie odłączonym (lub nietrwałym)

Jak widzę, istnieją trzy opcje :

  1. Nie zastępuj ich; polegać na Object.equals()iObject.hashCode()
    • hashCode()/ equals()praca
    • nie można zidentyfikować identycznych obiektów, problemy z dynamicznymi serwerami proxy
    • żadnych problemów z odłączonymi jednostkami
  2. Zastąp je na podstawie klucza podstawowego
    • hashCode()/ equals()są zepsute
    • poprawna tożsamość (dla wszystkich zarządzanych podmiotów)
    • problemy z odłączonymi jednostkami
  3. Zastąp je na podstawie identyfikatora firmy (pola klucza innego niż podstawowy; co z kluczami obcymi?)
    • hashCode()/ equals()są zepsute
    • poprawna tożsamość (dla wszystkich zarządzanych podmiotów)
    • żadnych problemów z odłączonymi jednostkami

Moje pytania to:

  1. Czy przegapiłem opcję i / lub punkt pro / con?
  2. Którą opcję wybrałeś i dlaczego?



AKTUALIZACJA 1:

Przez „ hashCode()/ equals()są łamane”, to znaczy, że kolejne hashCode()wywołania mogą zwracać różne wartości, co jest (gdy prawidłowo wdrożone) nie uszkodzony w sensie Objectdokumentacji API, ale co powoduje problemy, gdy próbuje odebrać zmieniony podmiot od A Map, Setlub inne oparty na haszowaniu Collection. W związku z tym implementacje JPA (przynajmniej EclipseLink) nie będą działać poprawnie w niektórych przypadkach.

AKTUALIZACJA 2:

Dziękujemy za odpowiedzi - większość z nich ma niezwykłą jakość.
Niestety nadal nie jestem pewien, które podejście będzie najlepsze dla aplikacji z prawdziwego życia, ani jak określić najlepsze podejście dla mojej aplikacji. Będę więc pozostawił pytanie otwarte i mam nadzieję na dalsze dyskusje i / lub opinie.

MRalwasser
źródło
4
Nie rozumiem co masz na myśli przez „hashcode () / równy () uszkodzony”
Nanda
4
W tym sensie nie byłyby one „zepsute”, ponieważ w opcji 2 i 3 implementowałbyś zarówno equals (), jak i hashCode () przy użyciu tej samej strategii.
matt b
11
Nie dotyczy to opcji 3. hashCode () i equals () powinny używać tych samych kryteriów, dlatego jeśli jedno z pól się zmieni, tak, metoda hashcode () zwróci inną wartość dla tego samego wystąpienia niż poprzednio, ale będzie równa się (). Opuściłeś drugą część zdania z hashcode () javadoc: Ilekroć jest wywoływana w tym samym obiekcie więcej niż jeden raz podczas wykonywania aplikacji Java, metoda hashCode musi konsekwentnie zwracać tę samą liczbę całkowitą, pod warunkiem , że nie ma informacji używane w równych porównaniach na obiekcie jest modyfikowany .
matt b
1
W rzeczywistości ta część zdania oznacza hashcode()coś przeciwnego - wywołanie tej samej instancji obiektu powinno zwrócić tę samą wartość, chyba że equals()zmienione zostaną pola użyte w implementacji. Innymi słowy, jeśli masz trzy pola w swojej klasie, a twoja equals()metoda używa tylko dwóch z nich do ustalenia równości instancji, możesz spodziewać hashcode()się zmiany wartości zwracanej, jeśli zmienisz jedną z tych wartości pola - co ma sens, jeśli weźmiesz pod uwagę że ta instancja obiektu nie jest już „równa” wartości reprezentowanej przez starą instancję.
matt b
2
„problemy podczas próby odzyskania zmienionego elementu z mapy, zestawu lub innych kolekcji opartych na haszowaniu” ... powinny to być „problemy podczas próby pobrania zmienionego obiektu z HashMap, HashSet lub innych kolekcji opartych na haszowaniu”
nanda

Odpowiedzi:

122

Przeczytaj bardzo fajny artykuł na ten temat: Nie pozwól, aby Hibernacja ukradła Twoją tożsamość .

Wniosek tego artykułu wygląda następująco:

Tożsamość obiektu jest oszukańczo trudna do prawidłowego wdrożenia, gdy obiekty są utrwalane w bazie danych. Problemy wynikają jednak całkowicie z faktu, że obiekty mogą istnieć bez identyfikatora przed ich zapisaniem. Możemy rozwiązać te problemy, przejmując odpowiedzialność za przypisywanie identyfikatorów obiektów od ramowych mapowań obiektowo-relacyjnych, takich jak Hibernacja. Zamiast tego można przypisać identyfikatory obiektów, jak tylko obiekt zostanie utworzony. To sprawia, że ​​tożsamość obiektu jest prosta i wolna od błędów, a także zmniejsza ilość kodu potrzebnego w modelu domeny.

Stijn Geukens
źródło
21
Nie, to nie jest fajny artykuł. To jest świetny świetny artykuł na ten temat i powinien być wymagany dla każdego programisty JPA! +1!
Tom Anderson
2
Tak, używam tego samego rozwiązania. Niedopuszczenie do generowania identyfikatora przez DB ma również inne zalety, takie jak możliwość tworzenia obiektu i już tworzenia innych obiektów, które odwołują się do niego przed utrwaleniem. Może to usunąć opóźnienia i wiele cykli żądania / odpowiedzi w aplikacjach klient-serwer. Jeśli potrzebujesz inspiracji dla takiego rozwiązania, sprawdź moje projekty: suid.js i suid-server-java . Zasadniczo suid.jspobiera bloki identyfikacyjne, z suid-server-javaktórych można następnie uzyskać i korzystać po stronie klienta.
Stijn de Witt
2
To jest po prostu szalone. Jestem nowy w pracy hibernacji pod maską, pisałem testy jednostkowe i dowiedziałem się, że nie mogę usunąć obiektu z zestawu po zmodyfikowaniu go, doszedłem do wniosku, że jest to spowodowane zmianą kodu skrótu, ale nie byłem w stanie zrozumieć, w jaki sposób rozwiązać. Artykuł jest po prostu wspaniały!
XMight 26.04.16
To świetny artykuł. Jednak dla osób, które widzą link po raz pierwszy, sugeruję, że może to być przesada w przypadku większości aplikacji. Pozostałe 3 opcje wymienione na tej stronie powinny w mniejszym lub większym stopniu rozwiązać problem na wiele sposobów.
HopeKing
1
Czy Hibernate / JPA korzysta z metody equals i hashcode encji, aby sprawdzić, czy rekord już istnieje w bazie danych?
Tushar Banne,
64

Zawsze zastępuję equals / hashcode i implementuję go na podstawie identyfikatora firmy. Wydaje mi się najbardziej rozsądnym rozwiązaniem. Zobacz poniższy link .

Podsumowując wszystkie te rzeczy, oto lista tego, co zadziała lub nie będzie działać z różnymi sposobami obsługi equals / hashCode: wprowadź opis zdjęcia tutaj

EDYCJA :

Aby wyjaśnić, dlaczego to działa dla mnie:

  1. Zwykle nie używam kolekcji opartej na haszowaniu (HashMap / HashSet) w mojej aplikacji JPA. Jeśli muszę, wolę stworzyć rozwiązanie UniqueList.
  2. Myślę, że zmiana identyfikatora biznesowego w środowisku wykonawczym nie jest najlepszą praktyką dla żadnej aplikacji bazy danych. W rzadkich przypadkach, gdy nie ma innego rozwiązania, zrobiłbym specjalne traktowanie, takie jak usunięcie elementu i odłożenie go z powrotem do kolekcji opartej na haszowaniu.
  3. W moim modelu ustawiam identyfikator biznesowy konstruktora i nie udostępniam dla niego ustawień. Pozwalam implementacji JPA na zmianę pola zamiast właściwości.
  4. Wydaje się, że rozwiązanie UUID to przesada. Dlaczego UUID, jeśli masz naturalny identyfikator firmy? W końcu ustawiłbym unikalność identyfikatora biznesowego w bazie danych. Po co więc TRZY indeksy dla każdej tabeli w bazie danych?
nanda
źródło
1
Ale w tej tabeli brakuje piątej linii „współpracuje z listą / zestawami” (jeśli myślisz o usunięciu elementu, który jest częścią zestawu z odwzorowania OneToMany), na który w przypadku dwóch ostatnich opcji odpowiedź brzmiałaby „nie”, ponieważ jego hashCode ( ) zmiany, które naruszają jego umowę.
MRalwasser
Zobacz komentarz do pytania. Wygląda na to, że źle zrozumiałeś umowę na równość / hashcode
nanda
1
@MRalwasser: Myślę, że masz na myśli właściwą rzecz, po prostu nie jest naruszona sama umowa equals / hashCode (). Ale zmienny equals / hashCode powoduje problemy z kontraktem Set .
Chris Lercher
3
@MRalwasser: Kod skrótu można zmienić tylko w przypadku zmiany identyfikatora firmy, a chodzi o to, że identyfikator firmy się nie zmienia. Tak więc hashcode się nie zmienia, i działa to doskonale z kolekcjami haszowanymi.
Tom Anderson
1
Co jeśli nie masz naturalnego klucza biznesowego? Na przykład w przypadku dwuwymiarowego punktu, punktu (X, Y), w aplikacji do rysowania wykresów? Jak przechowujesz ten punkt jako byt?
jhegedus
35

W naszych podmiotach zwykle mamy dwa identyfikatory:

  1. Dotyczy tylko warstwy trwałości (aby dostawca trwałości i baza danych mogły dowiedzieć się zależności między obiektami).
  2. Czy dla naszych potrzeb aplikacyjnych ( equals()a hashCode()zwłaszcza)

Spójrz:

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

EDYCJA: aby wyjaśnić moją kwestię dotyczącą wywołań setUuid()metody. Oto typowy scenariusz:

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'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

Kiedy uruchamiam testy i widzę dane wyjściowe dziennika, naprawiam problem:

User user = new User();
user.setUuid(UUID.randomUUID());

Alternatywnie można podać osobny konstruktor:

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

Mój przykład wyglądałby tak:

User user = new User(UUID.randomUUID());
...
jediSet.add(user); // no bug this time

em.persist(user); // and no log output

Używam domyślnego konstruktora i setera, ale może się okazać, że podejście dwóch konstruktorów będzie dla ciebie bardziej odpowiednie.

Andrew Андрей Листочкин
źródło
2
Uważam, że jest to prawidłowe i dobre rozwiązanie. Może mieć również niewielką przewagę wydajności, ponieważ liczby całkowite zwykle działają lepiej w indeksach baz danych niż identyfikatory UUID. Ale oprócz tego prawdopodobnie prawdopodobnie możesz wyeliminować bieżącą właściwość id liczby całkowitej i zastąpić ją identyfikatorem użytkownika (przypisany do aplikacji)?
Chris Lercher,
4
Czym różni się to od używania domyślnych hashCode/ equalsmetod dla równości JVM i idrówności trwałości? To nie ma dla mnie żadnego sensu.
Behrang Saeedzadeh
2
Działa w przypadkach, gdy kilka obiektów encji wskazuje na ten sam wiersz w bazie danych. Object„s equals()wróci falsew tym przypadku. equals()Zwraca na podstawie UUID true.
Andrew Андрей Листочкин
4
-1 - nie widzę powodu, aby mieć dwa identyfikatory, a więc dwa rodzaje tożsamości. Wydaje mi się to całkowicie bezcelowe i potencjalnie szkodliwe.
Tom Anderson
1
Przepraszam, że skrytykowałem twoje rozwiązanie bez wskazania na jedno, które wolałbym. Krótko mówiąc, dałbym obiektom jedno pole identyfikatora, zaimplementowałbym na nich równe i hashCode i wygenerowałbym jego wartość przy tworzeniu obiektu, a nie podczas zapisywania w bazie danych. W ten sposób wszystkie formy obiektu działają w ten sam sposób: nietrwały, trwały i odłączony. Hibernacyjne serwery proxy (lub podobne) również powinny działać poprawnie i myślę, że nie trzeba nawet nawadniać, aby obsłużyć wywołania equals i hashCode.
Tom Anderson
31

Wszystkie te trzy strategie osobiście wykorzystałem już w różnych projektach. I muszę powiedzieć, że opcja 1 jest moim zdaniem najbardziej praktyczna w prawdziwej aplikacji. Z mojego doświadczenia wynika, że ​​łamanie zgodności hashCode () / equals () prowadzi do wielu szalonych błędów, ponieważ za każdym razem znajdziesz się w sytuacjach, w których wynik równości zmienia się po dodaniu bytu do kolekcji.

Ale są jeszcze inne opcje (także ich zalety i wady):


a) hashCode / equals na podstawie zestawu niezmiennych , nie zerowych , przypisanych przez konstruktora pól

(+) wszystkie trzy kryteria są gwarantowane

(-) wartości pola muszą być dostępne, aby utworzyć nową instancję

(-) komplikuje obsługę, jeśli musisz zmienić jedną z nich


b) hashCode / equals na podstawie klucza podstawowego przypisanego przez aplikację (w konstruktorze) zamiast JPA

(+) wszystkie trzy kryteria są gwarantowane

(-) nie można korzystać z prostych strategii niezawodnego generowania identyfikatorów, takich jak sekwencje DB

(-) skomplikowane, jeśli nowe podmioty są tworzone w środowisku rozproszonym (klient / serwer) lub w klastrze serwerów aplikacji


c) hashCode / equals na podstawie identyfikatora UUID przypisanego przez konstruktora obiektu

(+) wszystkie trzy kryteria są gwarantowane

(-) narzut związany z generowaniem UUID

(-) może być niewielkie ryzyko, że zostanie użyty dwukrotnie ten sam UUID, w zależności od zastosowanego algorytmu (może zostać wykryty przez unikalny indeks na DB)

lweller
źródło
Jestem fanem Opcji 1 i podejściu C również. Nic nie rób, dopóki nie będziesz absolutnie potrzebny - jest to bardziej zwinne podejście.
Adam Gent
2
+1 dla opcji (b). IMHO, jeśli jednostka ma naturalny identyfikator firmy, to powinien to być również jej podstawowy klucz bazy danych. To prosty, bezpośredni, dobry projekt bazy danych. Jeśli nie ma takiego identyfikatora, potrzebny jest klucz zastępczy. Jeśli ustawisz to na tworzenie obiektu, wszystko inne będzie proste. Kiedy ludzie nie używają klucza naturalnego i nie generują klucza zastępczego wcześniej, wpadają w kłopoty. Jeśli chodzi o złożoność wdrożenia - tak, jest kilka. Ale naprawdę niewiele, i można to zrobić w bardzo ogólny sposób, który rozwiązuje to raz dla wszystkich bytów.
Tom Anderson
Wolę także opcję 1, ale jak napisać test jednostkowy, aby potwierdzić pełną równość, jest dużym problemem, ponieważ musimy wdrożyć metodę równości Collection.
OOD Waterball
Po prostu nie rób tego. Zobacz: Nie zadzieraj z hibernacją
alonana
29

Jeśli chcesz używać equals()/hashCode()w swoich zestawach, w tym sensie, że ten sam byt może tam być tylko raz, istnieje tylko jedna opcja: Opcja 2. Jest tak, ponieważ klucz podstawowy dla bytu z definicji nigdy się nie zmienia (jeśli ktoś rzeczywiście aktualizuje to już nie jest ten sam byt)

Powinieneś wziąć to dosłownie: ponieważ opierasz equals()/hashCode()się na kluczu podstawowym, nie możesz używać tych metod, dopóki klucz podstawowy nie zostanie ustawiony. Dlatego nie powinieneś umieszczać encji w zestawie, dopóki nie zostaną im przypisane klucze podstawowe. (Tak, UUID i podobne pojęcia mogą pomóc wcześniej przypisać klucze podstawowe.)

Teraz teoretycznie jest to również możliwe do osiągnięcia w Opcji 3, nawet jeśli tak zwane „klucze biznesowe” mają paskudną wadę, którą mogą zmienić: „Wszystko, co musisz zrobić, to usunąć już wstawione elementy ze zbioru ( s) i włóż je ponownie. ” To prawda - ale oznacza to również, że w systemie rozproszonym musisz się upewnić, że odbywa się to absolutnie wszędzie, gdzie dane zostały wstawione (i musisz upewnić się, że aktualizacja jest wykonywana , zanim pojawią się inne rzeczy). Będziesz potrzebował wyrafinowanego mechanizmu aktualizacji, szczególnie jeśli niektóre zdalne systemy nie są obecnie dostępne ...

Opcji 1 można użyć tylko wtedy, gdy wszystkie obiekty w zestawach pochodzą z tej samej sesji Hibernacji. Dokumentacja Hibernacji wyjaśnia to bardzo wyraźnie w rozdziale 13.1.3. Biorąc pod uwagę tożsamość obiektu :

W ramach sesji aplikacja może bezpiecznie używać == do porównywania obiektów.

Jednak aplikacja, która używa == poza sesją, może dawać nieoczekiwane wyniki. Może się to zdarzyć nawet w nieoczekiwanych miejscach. Na przykład, jeśli umieścisz dwie odłączone instancje w tym samym zestawie, oba mogą mieć tę samą tożsamość bazy danych (tj. Reprezentują ten sam wiersz). Jednak tożsamość JVM z definicji nie jest gwarantowana dla instancji w stanie odłączonym. Deweloper musi przesłonić metody equals () i hashCode () w trwałych klasach i wdrożyć własne pojęcie równości obiektów.

Nadal opowiada się za wariantem 3:

Jest jedno zastrzeżenie: nigdy nie używaj identyfikatora bazy danych do implementacji równości. Użyj klucza biznesowego, który jest kombinacją unikalnych, zwykle niezmiennych atrybutów. Identyfikator bazy danych zmieni się, jeśli obiekt przejściowy zostanie utrwalony. Jeśli przejściowa instancja (zwykle razem z instancjami odłączonymi) jest przechowywana w zestawie, zmiana kodu skrótu powoduje zerwanie kontraktu zestawu.

To prawda, jeśli ty

  • nie można przypisać identyfikatora wcześniej (np. przy użyciu UUID)
  • a jednak absolutnie chcesz umieścić swoje obiekty w zestawy, gdy są one w stanie przejściowym.

W przeciwnym razie możesz wybrać opcję 2.

Następnie wspomina o potrzebie względnej stabilności:

Atrybuty kluczy biznesowych nie muszą być tak stabilne jak klucze podstawowe bazy danych; musisz tylko zagwarantować stabilność, dopóki obiekty znajdują się w tym samym zestawie.

To jest poprawne. Praktyczny problem, jaki z tym widzę, brzmi: jeśli nie możesz zagwarantować absolutnej stabilności, w jaki sposób będziesz w stanie zagwarantować stabilność „dopóki obiekty są w tym samym zestawie”. Mogę sobie wyobrazić pewne szczególne przypadki (np. Używanie zestawów tylko do rozmowy, a następnie wyrzucanie ich), ale kwestionowałbym ogólną praktyczność tego.


Krótka wersja:

  • Opcji 1 można używać tylko z obiektami w ramach jednej sesji.
  • Jeśli możesz, użyj Opcji 2. (Przypisz PK jak najwcześniej, ponieważ nie możesz używać obiektów w zestawach, dopóki PK nie zostanie przypisany).
  • Jeśli możesz zagwarantować względną stabilność, możesz skorzystać z Opcji 3. Bądź jednak ostrożny.
Chris Lercher
źródło
Twoje założenie, że klucz podstawowy nigdy się nie zmienia, jest fałszywe. Na przykład Hibernacja przydziela klucz podstawowy tylko po zapisaniu sesji. Jeśli więc użyjesz klucza podstawowego jako hashCode, wynik funkcji hashCode () przed pierwszym zapisaniem obiektu i po pierwszym zapisaniu będzie inny. Co gorsza, przed zapisaniem sesji dwa nowo utworzone obiekty będą miały ten sam kod skrótu i ​​mogą się wzajemnie nadpisywać po dodaniu do kolekcji. Być może będziesz musiał wymusić natychmiastowe zapisanie / odłożenie przy tworzeniu obiektu, aby zastosować to podejście.
William Billingsley,
2
@William: Klucz podstawowy encji się nie zmienia. Właściwość id zamapowanego obiektu może ulec zmianie. Dzieje się tak, jak wyjaśniono, zwłaszcza gdy obiekt przejściowy zostaje utrwalony . Proszę uważnie przeczytać część mojej odpowiedzi, w której powiedziałem o metodach equals / hashCode: „nie wolno używać tych metod, dopóki klucz podstawowy nie zostanie ustawiony”.
Chris Lercher,
Kompletnie się zgadzam. Dzięki opcji 2 możesz również rozróżnić equals / hashcode w superklasie i pozwolić, aby był on ponownie wykorzystywany przez wszystkie twoje jednostki.
Theo
+1 Jestem nowy w JPA, ale niektóre komentarze i odpowiedzi sugerują, że ludzie nie rozumieją znaczenia terminu „klucz podstawowy”.
Raedwald,
16
  1. Jeśli masz klucz biznesowy , powinieneś użyć go do equals/ hashCode.
  2. Jeśli nie masz klucza biznesowego, nie powinieneś pozostawiać go z domyślnymi Objectimplementacjami równości i hashCode, ponieważ to nie działa po tobie mergei encji.
  3. Możesz użyć identyfikatora jednostki, jak sugerowano w tym poście . Jedynym haczykiem jest to, że musisz użyć hashCodeimplementacji, która zawsze zwraca tę samą wartość, na przykład:

    @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
    }
Vlad Mihalcea
źródło
Co jest lepsze: (1) onjava.com/pub/a/onjava/2006/09/13/… lub (2) vladmihalcea.com/… ? Rozwiązanie (2) jest łatwiejsze niż (1). Dlaczego więc mam używać (1). Czy skutki obu są takie same? Czy oba gwarantują to samo rozwiązanie?
nimo23
I z Twoim rozwiązaniem: „wartość hashCode się nie zmienia” między tymi samymi instancjami. Zachowuje się tak samo, jakby to był „ten sam” identyfikator UUID (z rozwiązania (1)). Czy mam rację?
nimo23
1
I przechowywać UUID w bazie danych i zwiększyć ślad rekordu i puli buforów? Myślę, że może to prowadzić do problemów z wydajnością na dłuższą metę niż unikalny hashCode. Jeśli chodzi o inne rozwiązanie, możesz to sprawdzić, aby sprawdzić, czy zapewnia ono spójność we wszystkich przejściach stanu encji. Można wybrać test, który sprawdza, czy na GitHub .
Vlad Mihalcea
1
Jeśli masz niezmienny klucz biznesowy, hashCode może go użyć i skorzysta z wielu wiader, więc warto go użyć, jeśli go posiadasz. W przeciwnym razie wystarczy użyć identyfikatora jednostki, jak wyjaśniono w moim artykule.
Vlad Mihalcea
1
Cieszę się, że to lubisz. Mam jeszcze setki artykułów o JPA i Hibernacji.
Vlad Mihalcea
10

Chociaż używanie klucza biznesowego (opcja 3) jest najczęściej zalecanym podejściem ( wiki społeczności Hibernacja , „Trwałość Java przy hibernacji”, s. 398), i tego najczęściej używamy, istnieje błąd hibernacji, który psuje to, gdy jest chętny zestawy: HHH-3799 . W takim przypadku Hibernacja może dodać encję do zestawu przed zainicjowaniem jego pól. Nie jestem pewien, dlaczego ten błąd nie zyskał większej uwagi, ponieważ naprawdę sprawia problemy z zalecanym podejściem opartym na kluczu biznesowym.

Myślę, że sedno sprawy polega na tym, że equals i hashCode powinny opierać się na stanie niezmiennym (odniesienie Odersky i wsp. ), A jednostka Hibernacja z kluczem zarządzanym przez Hibernację nie ma takiego stanu niezmiennego. Hibernacja klucza podstawowego jest modyfikowana, gdy obiekt przejściowy staje się trwały. Klucz biznesowy jest także modyfikowany przez Hibernację, gdy uwadnia obiekt w trakcie inicjalizacji.

Pozostaje tylko opcja 1, dziedziczenie implementacji java.lang.Object na podstawie tożsamości obiektu lub użycie klucza głównego zarządzanego przez aplikację, jak zasugerował James Brundege w „Don't Let Hibernate Steal Your Identity” (do którego już nawiązuje odpowiedź Stijn Geukens) ) oraz Lance'a Arlausa w „Object Generation: A Better Approach to Hibernate Integration” .

Największym problemem z opcją 1 jest to, że odłączonych instancji nie można porównać z instancjami trwałymi za pomocą .equals (). Ale jest dobrze; umowa equals i hashCode pozostawia programistom decyzję, co oznacza równość dla każdej klasy. Więc po prostu pozwól dziedziczyć i hashCode dziedziczyć po Object. Jeśli chcesz porównać instancję odłączoną do instancji trwałej, możesz w tym celu utworzyć nową metodę, na przykład boolean sameEntitylub boolean dbEquivalentlub boolean businessEquals.

jbyler
źródło
5

Zgadzam się z odpowiedzią Andrew. Robimy to samo w naszej aplikacji, ale zamiast przechowywać UUID jako VARCHAR / CHAR, podzieliliśmy go na dwie długie wartości. Zobacz UUID.getLeastSinentantBits () i UUID.getMostSinentantBits ().

Jeszcze jedną rzeczą do rozważenia jest to, że wywołania UUID.randomUUID () są dość powolne, więc możesz leniwie generować UUID tylko wtedy, gdy jest to potrzebne, na przykład podczas trwałości lub wywołań do 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();
    }

}
Rysował
źródło
Właściwie, jeśli przesłonisz equals () / hashCode (), to i tak musisz wygenerować UUID dla każdej encji (zakładam, że chcesz zachować każdą encję utworzoną w kodzie). Robisz to tylko raz - przed pierwszym zapisaniem w bazie danych. Po tym UUID jest właśnie ładowany przez dostawcę trwałości. Dlatego nie widzę sensu robić tego leniwie.
Andrew Андрей Листочкин
Głosowałem za odpowiedzią, ponieważ naprawdę podobają mi się twoje inne pomysły: przechowywanie UUID jako pary liczb w bazie danych i nie przesyłanie do określonego typu w metodzie equals () - ta jest naprawdę fajna! Zdecydowanie wykorzystam te dwie sztuczki w przyszłości.
Andrew Андрей Листочкин
1
Dziękuję za głosowanie w górę. Powodem leniwego zainicjowania UUID było to, że w naszej aplikacji tworzymy wiele encji, które nigdy nie są umieszczane w HashMapie lub się utrzymują. Więc widzieliśmy 100-krotny spadek wydajności podczas tworzenia obiektu (100 000 z nich). Tak więc inicjujemy UUID tylko wtedy, gdy jest potrzebny. Chciałbym po prostu mieć dobre wsparcie w MySql dla liczb 128-bitowych, abyśmy mogli po prostu użyć UUID dla identyfikatora i nie przejmować się auto_increment.
Drew
Rozumiem. W moim przypadku nie deklarujemy nawet pola UUID, jeśli odpowiedni podmiot nie będzie umieszczony w kolekcjach. Wadą jest to, że czasami musimy go dodać, ponieważ później okazuje się, że faktycznie musimy je umieścić w kolekcjach. Zdarza się to czasami podczas programowania, ale na szczęście nigdy nie zdarzyło się nam po pierwszym wdrożeniu u klienta, więc nie było to nic wielkiego. Gdyby tak się stało po uruchomieniu systemu, potrzebowalibyśmy migracji bazy danych. Leniwy UUID jest bardzo pomocny w takich sytuacjach.
Andrew Андрей Листочкин
Może powinieneś również wypróbować szybszy generator UUID, który Adam zasugerował w swojej odpowiedzi, jeśli wydajność jest krytycznym problemem w twojej sytuacji.
Andrew Андрей Листочкин
3

Jak zauważyli już inni ludzie znacznie mądrzejsi ode mnie, istnieje wiele strategii. Wydaje się jednak, że większość stosowanych wzorców projektowych próbuje włamać się do sukcesu. Ograniczają dostęp do konstruktorów, jeśli nie przeszkadzają całkowicie w wywoływaniu konstruktorów za pomocą wyspecjalizowanych konstruktorów i metod fabrycznych. Rzeczywiście, zawsze jest przyjemnie z przejrzystym API. Ale jeśli jedynym powodem jest sprawienie, by przesłonięcia równości i kodu skrótu były zgodne z aplikacją, zastanawiam się, czy te strategie są zgodne z KISS (Keep It Simple Stupid).

Dla mnie lubię zastępować równe i hashcode poprzez sprawdzenie identyfikatora. W tych metodach wymagam, aby identyfikator nie był pusty i dobrze dokumentował to zachowanie. W ten sposób umowa deweloperów będzie utrzymywać nowy byt przed przechowywaniem go gdzie indziej. Aplikacja, która nie honoruje tej umowy, zawiedzie w ciągu minuty (miejmy nadzieję).

Uwaga: jeśli Twoje podmioty są przechowywane w różnych tabelach, a Twój dostawca stosuje strategię automatycznego generowania dla klucza podstawowego, otrzymasz duplikaty kluczy podstawowych dla różnych typów jednostek. W takim przypadku porównaj również typy wykonawcze z wywołaniem Object # getClass (), co oczywiście uniemożliwi niemożliwe uznanie dwóch różnych typów za równe. W większości mi to odpowiada.

Martin Andersson
źródło
Nawet przy brakujących sekwencjach DB (takich jak Mysql) można je symulować (np. Tabela hibernacji_sekwencja). Dlatego zawsze możesz uzyskać unikatowy identyfikator dla różnych tabel. +++ Ale nie potrzebujesz tego. Dzwonienie Object#getClass() jest złe z powodu serwerów proxy H. Dzwonienie Hibernate.getClass(o)pomaga, ale problem równości bytów różnego rodzaju pozostaje. Istnieje rozwiązanie wykorzystujące canEqual , nieco skomplikowane, ale użyteczne. Zgodził się, że zwykle nie jest to potrzebne. +++ Rzucanie eq / hc na zerowy identyfikator narusza umowę, ale jest bardzo pragmatyczne.
maaartinus
2

Oczywiście są tu już bardzo pouczające odpowiedzi, ale powiem wam, co robimy.

Nic nie robimy (tzn. Nie zastępujemy).

Jeśli potrzebujemy równości / kodu skrótu do pracy w kolekcjach, używamy identyfikatorów UUID. Wystarczy utworzyć identyfikator UUID w konstruktorze. Używamy http://wiki.fasterxml.com/JugHome dla UUID. UUID jest nieco droższy pod względem procesora, ale jest tani w porównaniu do serializacji i dostępu do bazy danych.

Adam Gent
źródło
1

W przeszłości zawsze korzystałem z opcji 1, ponieważ byłem świadomy tych dyskusji i pomyślałem, że lepiej nic nie robić, dopóki nie będę wiedział, co należy zrobić. Wszystkie systemy nadal działają poprawnie.

Jednak następnym razem mogę wypróbować opcję 2 - używając wygenerowanego identyfikatora bazy danych.

Kod skrótu i ​​równa się wyrzuci IllegalStateException, jeśli identyfikator nie jest ustawiony.

Zapobiegnie to pojawieniu się subtelnych błędów dotyczących niezapisanych elementów.

Co ludzie myślą o tym podejściu?

Neil Stevens
źródło
1

Podejście do kluczy biznesowych nie jest dla nas odpowiednie. Do rozwiązania dylematu używamy identyfikatora wygenerowanego przez DB , tymczasowej przejściowej tempId i przesłonięcia równości () / hashcode (). Wszystkie byty są potomkami bytu. Plusy:

  1. Brak dodatkowych pól w DB
  2. Bez dodatkowego kodowania w potomkach, jedno podejście dla wszystkich
  3. Brak problemów z wydajnością (jak w przypadku UUID), generowanie identyfikatora DB
  4. Nie ma problemu z Hashmapami (nie trzeba pamiętać o używaniu równości itp.)
  5. Kod skrótu nowej jednostki nie zmienia się w czasie, nawet po utrwaleniu

Cons:

  1. Mogą występować problemy z serializacją i deserializacją nietrwałych bytów
  2. Kod skrótu zapisanej jednostki może ulec zmianie po przeładowaniu z DB
  3. Nietrwałe obiekty uważane zawsze za różne (może to prawda?)
  4. Co jeszcze?

Spójrz na nasz kod:

@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();
    }
}
Demel
źródło
1

Rozważ następujące podejście oparte na predefiniowanym identyfikatorze typu i identyfikatorze.

Szczegółowe założenia dotyczące WZP:

  • jednostki tego samego „typu” i tego samego niepustego identyfikatora są uważane za równe
  • nie utrwalone jednostki (zakładając brak identyfikatora) nigdy nie są równe innym jednostkom

Obiekt abstrakcyjny:

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

Przykład konkretnej jednostki:

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

Przykład testowy:

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

Główne zalety tutaj:

  • prostota
  • zapewnia, że ​​podklasy zapewniają tożsamość typu
  • przewidywane zachowanie z klasami proxy

Niedogodności:

  • Wymaga połączenia z każdym podmiotem super()

Uwagi:

  • Wymaga uwagi podczas korzystania z dziedziczenia. Na przykład równość instancji class Ai class B extends Amoże zależeć od konkretnych szczegółów aplikacji.
  • Najlepiej użyć klucza biznesowego jako identyfikatora

Czekam na twoje komentarze.

aux
źródło
0

Jest to powszechny problem w każdym systemie informatycznym korzystającym z Java i JPA. Punkt bolesny rozciąga się poza implementację equals () i hashCode (), wpływa na to, jak organizacja odwołuje się do jednostki i jak jej klienci odnoszą się do tej samej jednostki. Widziałem dość bólu z powodu braku klucza biznesowego do tego stopnia, że ​​napisałem własny blog, aby wyrazić swój pogląd.

W skrócie: użyj krótkiego, czytelnego dla człowieka, sekwencyjnego ID z znaczącymi prefiksami jako klucza biznesowego, który jest generowany bez żadnej zależności od pamięci innej niż RAM. Śnieżynka Twittera jest bardzo dobrym przykładem.

Christopher Yang
źródło
0

IMO masz 3 opcje implementacji equals / hashCode

  • Użyj tożsamości generowanej przez aplikację, tj. UUID
  • Zaimplementuj go w oparciu o klucz biznesowy
  • Zaimplementuj go w oparciu o klucz podstawowy

Korzystanie z tożsamości generowanej przez aplikację jest najłatwiejszym podejściem, ale ma kilka wad

  • Połączenia są wolniejsze, gdy używa się go jako PK, ponieważ 128-bitowy jest po prostu większy niż 32-bitowy lub 64-bitowy
  • „Debugowanie jest trudniejsze”, ponieważ sprawdzanie na własne oczy, czy niektóre dane są prawidłowe, jest dość trudne

Jeśli możesz pracować z tymi wadami , skorzystaj z tego podejścia.

Aby rozwiązać problem łączenia, można użyć UUID jako klucza naturalnego i wartości sekwencji jako klucza podstawowego, ale nadal możesz napotykać problemy z implementacją equals / hashCode w elementach potomnych kompozycyjnych, które mają osadzone identyfikatory, ponieważ będziesz chciał dołączyć na podstawie na kluczu podstawowym. Użycie klucza naturalnego w identyfikatorze jednostek podrzędnych i klucza podstawowego w odniesieniu do elementu nadrzędnego jest dobrym kompromisem.

@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 jest to najczystsze podejście, ponieważ pozwoli uniknąć wszystkich wad i jednocześnie zapewni Ci wartość (UUID), którą możesz udostępniać systemom zewnętrznym bez ujawniania wewnętrznych elementów systemu.

Zaimplementuj go w oparciu o klucz biznesowy, jeśli możesz oczekiwać, że od użytkownika to fajny pomysł, ale ma również kilka wad

Przez większość czasu ten klucz biznesowy będzie kodem dostarczanym przez użytkownika, rzadziej złożonym z wielu atrybutów.

  • Połączenia są wolniejsze, ponieważ łączenie na podstawie tekstu o zmiennej długości jest po prostu wolne. Niektóre DBMS mogą nawet mieć problemy z utworzeniem indeksu, jeśli klucz przekroczy określoną długość.
  • Z mojego doświadczenia wynika, że ​​klucze biznesowe zwykle się zmieniają, co będzie wymagać aktualizacji kaskadowych odnoszących się do nich obiektów. Jest to niemożliwe, jeśli odnoszą się do niego systemy zewnętrzne

IMO nie powinieneś wdrażać ani pracować wyłącznie z kluczem biznesowym. Jest to miły dodatek, tzn. Użytkownicy mogą szybko wyszukiwać według tego klucza biznesowego, ale system nie powinien na nim polegać.

Wdrożenie go w oparciu o klucz podstawowy ma problemy, ale może nie jest to taka wielka sprawa

Jeśli chcesz udostępnić identyfikatory zewnętrznemu systemowi, skorzystaj z zaproponowanego przeze mnie identyfikatora UUID. Jeśli tego nie zrobisz, nadal możesz zastosować metodę UUID, ale nie musisz. Problem użycia identyfikatora wygenerowanego przez DBMS w equals / hashCode wynika z faktu, że obiekt mógł zostać dodany do kolekcji opartych na haszowaniu przed przypisaniem identyfikatora.

Oczywistym sposobem obejścia tego jest po prostu nie dodawanie obiektu do kolekcji opartych na haszowaniu przed przypisaniem identyfikatora. Rozumiem, że nie zawsze jest to możliwe, ponieważ możesz chcieć deduplikacji przed przypisaniem identyfikatora już. Aby nadal móc korzystać z kolekcji opartych na haszowaniu, wystarczy przypisać kolekcje po przypisaniu identyfikatora.

Możesz zrobić coś takiego:

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

Nie przetestowałem dokładnie tego podejścia, więc nie jestem pewien, jak działa zmiana kolekcji w zdarzeniach przed i po utrwaleniu, ale pomysł jest następujący:

  • Tymczasowo usuń obiekt ze zbiorów opartych na haszowaniu
  • Utrzymaj to
  • Ponownie dodaj obiekt do kolekcji opartych na haszowaniu

Innym sposobem rozwiązania tego jest po prostu przebudowanie wszystkich modeli opartych na haszowaniu po aktualizacji / utrwaleniu.

Ostatecznie to zależy od ciebie. Osobiście przez większość czasu używam podejścia opartego na sekwencji i używam podejścia UUID tylko wtedy, gdy muszę ujawnić identyfikator systemom zewnętrznym.

Christian Beikov
źródło
0

Dzięki nowemu stylowi instanceofz java 14 możesz zaimplementować equalsw jednym wierszu.

@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;
}
Artem
źródło
-1

Jeśli UUID jest odpowiedzią dla wielu osób, dlaczego po prostu nie używamy metod fabrycznych z warstwy biznesowej do tworzenia jednostek i przypisywania klucza podstawowego w czasie tworzenia?

na przykład:

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

w ten sposób otrzymalibyśmy domyślny klucz podstawowy dla jednostki od dostawcy trwałości, a nasze funkcje hashCode () i equals () mogłyby na tym polegać.

Możemy również zadeklarować ochronę konstruktorów samochodu, a następnie użyć refleksji w naszej metodzie biznesowej, aby uzyskać do nich dostęp. W ten sposób programiści nie zamierzaliby tworzyć instancji Car z nowym, ale metodą fabryczną.

Co powiesz na to?

illEatYourPuppies
źródło
Podejście, które działa świetnie, jeśli chcesz wziąć wpływ na wydajność zarówno generowania GUID podczas wyszukiwania bazy danych.
Michael Wiles
1
Co z testowaniem jednostkowym samochodu? W takim przypadku potrzebujesz połączenia z bazą danych do testowania? Ponadto obiekty domeny nie powinny zależeć od trwałości.
jhegedus
-1

Sam próbowałem odpowiedzieć na to pytanie i nigdy nie byłem całkowicie zadowolony ze znalezionych rozwiązań, dopóki nie przeczytałem tego posta, a zwłaszcza DREW. Podobał mi się sposób, w jaki leniwy tworzył UUID i optymalnie go przechowywał.

Chciałem jednak dodać jeszcze więcej elastyczności, tzn. Leniwe tworzenie UUID TYLKO wtedy, gdy hashCode () / equals () jest dostępny przed pierwszym utrwaleniem encji z zaletami każdego rozwiązania:

  • równa się () oznacza „obiekt odnosi się do tej samej jednostki logicznej”
  • używaj identyfikatora bazy danych w jak największym stopniu, ponieważ dlaczego miałbym wykonywać tę pracę dwa razy (problem z wydajnością)
  • zapobieganie problemom podczas uzyskiwania dostępu do hashCode () / equals () na jeszcze nie utrwalonym obiekcie i zachowanie tego samego zachowania po tym, jak rzeczywiście zostało utrwalone

Byłbym bardzo wdzięczny za opinie na temat mojego mieszanego rozwiązania poniżej

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;
    }
user2083808
źródło
co rozumiesz przez „identyfikator UUID wygenerowany na tym obiekcie, zanim zostałem uparty”? czy mógłbyś podać przykład tej sprawy?
jhegedus
czy możesz użyć przypisanego typu generacji? dlaczego potrzebny jest typ generowania tożsamości? czy ma jakąś przewagę nad przypisanym?
jhegedus
co się stanie, jeśli 1) utworzysz nową MyEntity, 2) umieścisz ją na liście, 3) następnie zapiszesz w bazie danych, a następnie 4) załadujesz tę jednostkę z powrotem z bazy danych i 5) spróbujesz sprawdzić, czy załadowana instancja znajduje się na liście . Domyślam się, że tak nie będzie, chociaż powinno być.
jhegedus
Dziękuję za twoje pierwsze komentarze, które pokazały mi, że nie byłem tak jasny, jak powinienem. Po pierwsze, „UUID jeden dzień wygenerowany dla tej encji przed utrwaleniem” był literówką… „zanim IT został utrwalony” powinien zostać odczytany. Jeśli chodzi o inne uwagi, wkrótce zmienię mój post, aby spróbować lepiej wyjaśnić moje rozwiązanie.
user2083808
-1

W praktyce wydaje się, że najczęściej używana jest opcja 2 (klucz podstawowy). Naturalny i NIEZWYKŁY klucz biznesowy jest rzadkością, tworzenie i obsługa kluczy syntetycznych jest zbyt ciężka, aby rozwiązać sytuacje, które prawdopodobnie nigdy się nie zdarzyły. Przyjrzeć się wiosenno-data-WZP AbstractPersistable realizacji (jedyna rzecz: do użytku realizacji hibernacjiHibernate.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;
}

Wystarczy zdawać sobie sprawę z manipulowania nowymi obiektami w HashSet / HashMap. Przeciwnie, opcja 1 (pozostała Objectimplementacja) zostaje zerwana tuż po merge, co jest bardzo powszechną sytuacją.

Jeśli nie masz klucza biznesowego i masz PRAWDZIWĄ potrzebę manipulowania nowym bytem w strukturze skrótu, zastąp hashCodego stałym, jak poniżej doradzono Vlad Mihalcea.

Grigorij Kislin
źródło
-2

Poniżej znajduje się proste (i przetestowane) rozwiązanie dla Scali.

  • Pamiętaj, że to rozwiązanie nie pasuje do żadnej z 3 kategorii podanych w pytaniu.

  • Wszystkie moje byty są podklasami UUIDEntity, więc kieruję się zasadą „nie powtarzaj się” (DRY).

  • W razie potrzeby można bardziej precyzyjnie wygenerować UUID (używając więcej liczb pseudolosowych).

Kod 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()
}
Jhegedus
źródło