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ść) dlaList
/Set
operacji- 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 :
- 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
- 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
- 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:
- Czy przegapiłem opcję i / lub punkt pro / con?
- 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 Object
dokumentacji API, ale co powoduje problemy, gdy próbuje odebrać zmieniony podmiot od A Map
, Set
lub 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.
hashcode()
coś przeciwnego - wywołanie tej samej instancji obiektu powinno zwrócić tę samą wartość, chyba żeequals()
zmienione zostaną pola użyte w implementacji. Innymi słowy, jeśli masz trzy pola w swojej klasie, a twojaequals()
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ę.Odpowiedzi:
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:
źródło
suid.js
pobiera bloki identyfikacyjne, zsuid-server-java
których można następnie uzyskać i korzystać po stronie klienta.Zawsze zastępuję equals / hashcode i implementuję go na podstawie identyfikatora firmy. Wydaje mi się najbardziej rozsądnym rozwiązaniem. Zobacz poniższy link .
EDYCJA :
Aby wyjaśnić, dlaczego to działa dla mnie:
źródło
W naszych podmiotach zwykle mamy dwa identyfikatory:
equals()
ahashCode()
zwłaszcza)Spójrz:
EDYCJA: aby wyjaśnić moją kwestię dotyczącą wywołań
setUuid()
metody. Oto typowy scenariusz:Kiedy uruchamiam testy i widzę dane wyjściowe dziennika, naprawiam problem:
Alternatywnie można podać osobny konstruktor:
Mój przykład wyglądałby tak:
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.
źródło
hashCode
/equals
metod dla równości JVM iid
równości trwałości? To nie ma dla mnie żadnego sensu.Object
„sequals()
wrócifalse
w tym przypadku.equals()
Zwraca na podstawie UUIDtrue
.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)
źródło
Collection
.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 :
Nadal opowiada się za wariantem 3:
To prawda, jeśli ty
W przeciwnym razie możesz wybrać opcję 2.
Następnie wspomina o potrzebie względnej stabilności:
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:
źródło
equals
/hashCode
.Object
implementacjami równości i hashCode, ponieważ to nie działa po tobiemerge
i encji.Możesz użyć identyfikatora jednostki, jak sugerowano w tym poście . Jedynym haczykiem jest to, że musisz użyć
hashCode
implementacji, która zawsze zwraca tę samą wartość, na przykład:źródło
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 sameEntity
lubboolean dbEquivalent
lubboolean businessEquals
.źródło
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 ()
źródło
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.
źródło
Object#getClass()
jest złe z powodu serwerów proxy H. DzwonienieHibernate.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.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.
źródło
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?
źródło
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:
Cons:
Spójrz na nasz kod:
źródło
Rozważ następujące podejście oparte na predefiniowanym identyfikatorze typu i identyfikatorze.
Szczegółowe założenia dotyczące WZP:
Obiekt abstrakcyjny:
Przykład konkretnej jednostki:
Przykład testowy:
Główne zalety tutaj:
Niedogodności:
super()
Uwagi:
class A
iclass B extends A
może zależeć od konkretnych szczegółów aplikacji.Czekam na twoje komentarze.
źródło
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.
źródło
IMO masz 3 opcje implementacji equals / hashCode
Korzystanie z tożsamości generowanej przez aplikację jest najłatwiejszym podejściem, ale ma kilka wad
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.
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.
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:
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:
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.
źródło
Dzięki nowemu stylowi
instanceof
z java 14 możesz zaimplementowaćequals
w jednym wierszu.źródło
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:
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?
źródło
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:
Byłbym bardzo wdzięczny za opinie na temat mojego mieszanego rozwiązania poniżej
źródło
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 hibernacji
Hibernate.getClass
).Wystarczy zdawać sobie sprawę z manipulowania nowymi obiektami w HashSet / HashMap. Przeciwnie, opcja 1 (pozostała
Object
implementacja) zostaje zerwana tuż pomerge
, co jest bardzo powszechną sytuacją.Jeśli nie masz klucza biznesowego i masz PRAWDZIWĄ potrzebę manipulowania nowym bytem w strukturze skrótu, zastąp
hashCode
go stałym, jak poniżej doradzono Vlad Mihalcea.źródło
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:
źródło