Najwydajniejszy sposób mapowania relacji @OneToMany za pomocą JPA i Hibernate

Wybrałem, aby otworzyć ten post tym cytatem, ponieważ jestem fanem Linusa Torvaldsa 😉

To jest mój pierwszy w życiu artykuł. Będę w nim omawiał wszystkie możliwe przypadki w One-to-Many/Many-to-One asocjacji encji. Pozostałe Many-to-Many i One-to-One zostaną omówione w następnych artykułach.

Mam nadzieję, że to zdecydowanie pomoże każdemu początkującemu, który chce się nauczyć jpa/hibernate, proszę przeczytać cały artykuł 😛

UWAGA:

Tutaj omówiłem wszystkie możliwe przypadki One-to-Many/Many-to-One mapowania. Wśród wszystkich, dwukierunkowa asocjacja `@OneToMany` jest najlepszym sposobem na mapowanie relacji jeden do wielu w bazie danych.

Asocjacja hibernacji sklasyfikowana na One-to-One, One-to-Many/Many-to-One i Many-to-Many.

  • Kierunek relacji może być dwukierunkowy lub jednokierunkowy.
  • Relacja dwukierunkowa ma zarówno stronę posiadającą, jak i stronę odwrotną.
  • Relacja jednokierunkowa ma tylko stronę posiadającą. Strona własnościowa relacji określa sposób, w jaki czas działania Persistence dokonuje aktualizacji relacji w bazie danych.
  • Jednokierunkowa to relacja, w której jedna ze stron nie wie o relacji.
  • W relacji jednokierunkowej tylko jedna jednostka ma pole relacji lub właściwość, która odnosi się do drugiej. Na przykład pozycja liniowa miałaby pole relacji, które identyfikuje Produkt, ale Produkt nie miałby pola relacji lub właściwości dla pozycji liniowej. Innymi słowy, Line Item wie o Produkcie, ale Produkt nie wie, które instancje Line Item odnoszą się do niego.

Związki dwukierunkowe:

  • Związek dwukierunkowy zapewnia dostęp nawigacyjny w obu kierunkach, dzięki czemu można uzyskać dostęp do drugiej strony bez wyraźnych zapytań.
  • W związku dwukierunkowym, każda encja ma pole relacji lub właściwość, która odnosi się do drugiej encji. Poprzez pole lub właściwość relacji, kod klasy encji może uzyskać dostęp do jej powiązanego obiektu. Jeśli encja ma powiązane pole, mówi się, że encja „wie” o swoim powiązanym obiekcie. Na przykład, jeśli Zamówienie wie, jakie instancje pozycji asortymentowej posiada i jeśli pozycja asortymentowa wie, do jakiego Zamówienia należy, mają one relację dwukierunkową.

Związki dwukierunkowe muszą być zgodne z tymi zasadami.

  • Odwrotna strona relacji dwukierunkowej musi odnosić się do swojej strony posiadającej (Podmiot, który zawiera klucz obcy) poprzez użycie elementu mappedBy adnotacji @OneToOne, @OneToMany lub @ManyToMany. Element mappedBy określa właściwość lub pole w encji, która jest właścicielem relacji.
  • Strona wielu @ManyToOne relacji dwukierunkowych nie może określać elementu mappedBy. Strona wielu jest zawsze stroną posiadającą związek.
  • Dla @OneToOne relacji dwukierunkowych, strona posiadająca odpowiada stronie, która zawiera @JoinColumn tj. odpowiedni klucz obcy.
  • Dla @ManyToMany relacji dwukierunkowych, każda ze stron może być stroną posiadającą.

@OneToMany relationship with JPA and Hibernate

Simply put, one-to-many mapping means that one row in a table is mapped to multiple rows in another table.

Kiedy używać mapowania jeden do wielu

Używaj mapowania jeden do wielu do tworzenia relacji 1…N pomiędzy encjami lub obiektami.

Musimy napisać dwie encje tj. Company i Branch w taki sposób, że wiele oddziałów może być powiązanych z jedną firmą, ale jeden pojedynczy oddział nie może być współdzielony przez dwie lub więcej firm.

Hibernate one to many mapping solutions:

  1. One to many mapping with foreign key association
  2. One to many mapping with join table

Ten problem może być rozwiązany na dwa różne sposoby.

Jednym z nich jest posiadanie kolumny klucza obcego w tabeli branch tj. company_id. Ta kolumna będzie odnosić się do klucza głównego tabeli Firma. W ten sposób żadne dwa oddziały nie mogą być powiązane z wieloma firmami.

Drugie podejście to posiadanie wspólnej tabeli join powiedzmy Company_Branch, Ta tabela będzie miała dwie kolumny tj. company_id, która będzie kluczem obcym odnoszącym się do klucza podstawowego w tabeli Company i podobnie branch_id, która będzie kluczem obcym odnoszącym się do klucza podstawowego w tabeli Branch.

Jeśli @OneToMany/@ManyToOne nie posiada lustrzanego związku @ManyToOne/@OneToMany odpowiednio po stronie dziecka, wtedy związek @OneToMany/@ManyToOne jest jednokierunkowy.

@OneToMany Związek Jednokierunkowy

W tym podejściu, jeden podmiot będzie odpowiedzialny za stworzenie związku i jego utrzymanie. Albo Firma zadeklaruje relację jako jeden do wielu, albo Oddział zadeklaruje relację od swojego końca jako wiele do jednego.

CASE 1: (Mapowanie z asocjacją klucza obcego)

Jeśli użyjemy tylko @OneToMany to będą 3 tabele. Takie jak company, branch i company_branch.

UWAGA:
W powyższym przykładzie użyłem terminów takich jak cascade, orphanRemoval, fetch i targetEntity, które wyjaśnię w moim następnym poście.

Tabela company_branch będzie miała dwa klucze obce company_id i branch_id.

Teraz, jeśli utrzymamy jedną Firmę i dwa Oddziały:

Hibernate wykona następujące instrukcje SQL:

  • Asocjacja @OneToMany jest z definicji asocjacją rodzicielską (nie będącą właścicielem), nawet jeśli jest to asocjacja jednokierunkowa lub dwukierunkowa. Tylko po stronie rodzica asocjacji ma sens kaskadowanie jej przejść stanu encji do dzieci.
  • Podczas persystowania encji Company, kaskada będzie propagować operację persist również do bazowych dzieci Oddziału. Po usunięciu Branch z kolekcji branchs, wiersz asocjacji zostanie usunięty z tabeli powiązań, a atrybut orphanRemoval również wywoła usunięcie Branch.

Asocjacje jednokierunkowe nie są zbyt wydajne, jeśli chodzi o usuwanie encji dzieci. W tym konkretnym przykładzie, po przepłukaniu kontekstu trwałości, Hibernate usuwa wszystkie wpisy dziecka bazy danych i ponownie wstawia te, które nadal znajdują się w kontekście trwałości w pamięci.

Z drugiej strony, dwukierunkowa asocjacja @OneToMany jest znacznie bardziej wydajna, ponieważ encja dziecka kontroluje asocjację.

PRZYPADEK 2: (Mapowanie z asocjacją klucza obcego)

Jeśli użyjemy tylko @ManyToOne to będą 2 tabele. Takich jak firma, oddział.

Powyższy kod wygeneruje 2 tabele Company(company_id,name) i Branch(branch_id,name,company_company_id). Tutaj Oddział jest stroną posiadającą, ponieważ posiada asocjację z kluczem obcym.

PRZYPADEK 3: (Mapowanie z asocjacją z kluczem obcym)

Jeśli użyjemy zarówno @ManyToOne jak i @OneToMany to utworzą się 3 tabele Firma(id,nazwa), Oddział(id,nazwa,firma_id), Firma_Oddział(firma_id,oddział_id)

To poniższe mapowanie może wyglądać na dwukierunkowe, ale takie nie jest. Definiuje ono nie jedną relację dwukierunkową, ale dwie oddzielne relacje jednokierunkowe.

CASE 4: (Unidirectional @OneToMany with @JoinColumn)(Mapping with foreign key association)

Jeśli użyjemy @OneToMany z @JoinColumn to powstaną 2 tabele. Takich jak firma, oddział

W powyższej encji wewnątrz @JoinColumn, nazwa odnosi się do nazwy kolumny klucza obcego, która jest companyId i.e company_id a rferencedColumnName wskazuje na klucz główny tj. id encji (Company), do której odnosi się klucz obcy companyId.

CASE 5: (Mapowanie z asocjacją klucza obcego)

Jeśli użyjemy @ManyToOne z @JoinColumn to powstaną 2 tabele. Takich jak firma,oddział.

Adnotacja @JoinColumn pomaga Hibernate zorientować się, że w tabeli oddział istnieje kolumna company_id Foreign Key, która definiuje tę asocjację.

Branch będzie miał klucz obcy company_id, więc jest to strona właściciela.

Teraz, jeśli utrzymujemy 1 Firmę i 2 Oddziały:

podczas usuwania pierwszego wpisu z kolekcji dzieci:

company.getBranches().remove(0);

Hibernate wykonuje dwie instrukcje zamiast jednej :

  • Najpierw zmieni pole klucza obcego na null (aby przerwać asocjację z rodzicem) następnie usunie rekord.
Update branch set branch_id = null where where id = 1 delete from branch where id = 1 ;

CASE 6: (Mapowanie z tabelą join)

Teraz rozważmy relację one-to-many, gdzie Person(id,name) jest powiązane z wieloma Veichle(id,name,number) i wiele pojazdów może należeć do tej samej osoby.

Jednego dnia podczas jazdy po autostradzie szeryf Carlos znalazł kilka porzuconych pojazdów, najprawdopodobniej skradzionych. Teraz szeryf musi zaktualizować dane pojazdów (numer, nazwa) w swojej bazie danych, ale głównym problemem jest to, że nie ma właściciela dla tych pojazdów, więc pole person_id(foreign key ) pozostanie puste.

Teraz szeryf zapisuje dwa skradzione pojazdy do db w następujący sposób:

Vehicle vehicle1 = new Vehicle("ford", 1); 
Vehicle vehicle2 = new Vehicle("gmc", 2);
List<Vehicle> vehicles = new ArrayList<>(); vehicles.add(vehicle1);
vehicles.add(vehicle2);
entityManager.persist(veichles);

Hibernate wykona następujące polecenia SQL:

insert into vehicle (id, name, person_id) values (1, "ford", null); insert into vehicle (id, name, person_id) values (2, "gmc", null);id |name|person_id|
-----|----|---------|
1 |ford|NULL |
2 |benz|NULL |
---------------------

Ta powyższa strategia zmusi nas do umieszczenia wartości null w kolumnie, aby obsłużyć opcjonalne relacje.

Typowo, myślimy o relacjach many-to-many, kiedy rozważamy join table, ale użycie tabeli join, w tym przypadku, może pomóc nam wyeliminować te wartości null:

To podejście wykorzystuje tabelę join do przechowywania asocjacji pomiędzy oddziałami i firmami. Adnotacja @JoinTable została użyta do wykonania tej asocjacji.

W tym przykładzie zastosowaliśmy @JoinTable po stronie pojazdu (Many Side).

Zobaczmy jak będzie wyglądał schemat bazy danych:

Powyższy kod wygeneruje 3 tabele takie jak person(id,name), vehicle(id,name) i vehicle_person(vehicle_id,person_id). Tutaj vehicle_person będzie posiadał relację klucza obcego do obu encji Osoba i Pojazd.

Więc kiedy szeryf zapisze szczegóły pojazdu, żadna wartość null nie musi być przechowywana w tabeli Pojazd, ponieważ utrzymujemy asocjację klucza obcego w tabelach vehicle_person, a nie w tabeli Pojazd.

Vehicle vehicle1 = new Vehicle("ford", 1);
Vehicle vehicle2 = new Vehicle("gmc", 2);
List<Vehicle> vehicles = new ArrayList<>(); vehicles.add(vehicle1);
vehicles.add(vehicle2);
entityManager.persist(veichles);

Hibernate wykona następujące polecenia SQL:

insert into vehicle (id, name) values (1, "ford"); insert into vehicle (id, name) values (2, "gmc");id |name|
-----|----|
1 |ford|
2 |benz|
-----------

Powyższy przykład pokazuje, w jaki sposób pozbyliśmy się wstawiania wartości null.

@OneToMany Bi-directional Relationship

  • Asocjacja dwukierunkowa @OneToMany wymaga również asocjacji @ManyToOne po stronie dziecka. Chociaż model domeny eksponuje dwie strony do poruszania się po tej asocjacji, za kulisami relacyjna baza danych ma tylko jeden klucz obcy dla tej relacji.
  • Każda dwukierunkowa asocjacja musi mieć tylko jedną stronę posiadającą (stronę dziecka), druga jest określana jako strona odwrotna (lub stronamappedBy).
  • Jeśli używamy @OneToMany z ustawionym atrybutem mappedBy, to mamy asocjację dwukierunkową, co oznacza, że musimy mieć asocjację @ManyToOne po stronie dziecka, do której odwołuje się mappedBy.
  • Element mappedBy definiuje relację dwukierunkową. Atrybut ten umożliwia odwoływanie się do powiązanych encji z obu stron.

Najlepszym sposobem odwzorowania asocjacji @OneToMany jest poleganie na stronie @ManyToOne w celu propagacji wszystkich zmian stanu encji.

Jeśli persystujemy 2 Branch(s)

Hibernate generuje tylko jedno zapytanie SQL dla każdej persystowanej encji Branch:

insert into company (name, id) values ("company1",1); 
insert into branch (company_id, name, id) values (1,"branch1",1); insert into branch (company_id, name, id) values (1,"branch2",2);

Jeśli usuwamy Branch:

Company company = entityManager.find( Company.class, 1L ); Branch branch = company.getBranches().get(0); company.removeBranches(branch);

Wykonywana jest tylko jedna instrukcja SQL delete:

delete from Branch where id = 1

Więc dwukierunkowa asocjacja @OneToMany jest najlepszym sposobem mapowania relacji bazy danych typu jeden do wielu, gdy naprawdę potrzebujemy kolekcji po stronie rodzica asocjacji.

@JoinColumn Określa kolumnę do dołączenia do asocjacji encji lub kolekcji elementów. Adnotacja @JoinColumn wskazuje, że ten podmiot jest właścicielem relacji. Oznacza to, że odpowiadająca tabela ma kolumnę z kluczem obcym do tabeli, do której się odwołuje.

W powyższym przykładzie, oddział podmiotów będących właścicielami, ma kolumnę dołączenia o nazwie company_id, która ma klucz obcy do podmiotu niebędącego właścicielem Firma.

W każdym przypadku utworzenia dwukierunkowego powiązania, programista aplikacji musi upewnić się, że obie strony są zsynchronizowane przez cały czas. Metody addBranches() i removeBranches() są metodami narzędziowymi, które synchronizują oba końce za każdym razem, gdy element potomny (tj. Oddział) jest dodawany lub usuwany.

W przeciwieństwie do jednokierunkowego @OneToMany, asocjacja dwukierunkowa jest znacznie bardziej wydajna podczas zarządzania stanem trwałości kolekcji.

Każde usunięcie elementu wymaga tylko pojedynczej aktualizacji (w której kolumna klucza obcego jest ustawiona na NULL) i jeśli cykl życia encji dziecięcej jest związany z jej rodzicem, tak że dziecko nie może istnieć bez swojego rodzica, wtedy możemy oznaczyć asocjację atrybutem orphan-removal, a usunięcie dziecka spowoduje wywołanie polecenia usunięcia również na rzeczywistym wierszu tabeli dziecka.

UWAGA:

  • W powyższym dwukierunkowym przykładzie termin Rodzic i Dziecko w Podmiocie/OO/Modelu (tj. w klasie java) odnosi się odpowiednio do strony Nie wygrywającej/odwrotnej i Własnej w SQL.

W punkcie widzenia java Firma jest Rodzicem, a oddział jest dzieckiem. Ponieważ oddział nie może istnieć bez rodzica.

W SQL punkt widzenia Oddział jest stroną Właściciel i Firma jest stroną Nie-właściciel (Inverse). Ponieważ istnieje 1 firma dla N oddziałów, każdy oddział zawiera klucz obcy do firmy, do której należy.Oznacza to, że oddział „posiada” (lub dosłownie zawiera) połączenie (informacje). To jest dokładnie odwrotnie niż w świecie OO/modelu.

@OneToMany Bi-directional Relationship(Mapping with join table)

Z CASE 6 mamy jednokierunkowe mapowanie z @JoinTable, więc jeśli dodamy atrybut mappedBy do encji Person wtedy relacja stanie się dwukierunkowa.

SUMMARY

Jest kilka rzeczy, na które należy zwrócić uwagę w wyżej wymienionym mapowaniu:

  • Asocjacja @ManyToOne używa FetchType.LAZY ponieważ, w przeciwnym razie, cofnęlibyśmy się do pobierania EAGER, co jest złe dla wydajności.
  • Asocjacje dwukierunkowe powinny być zawsze aktualizowane po obu stronach, dlatego strona Parent powinna zawierać combo addChild i removeChild (Parent side Company zawiera dwie metody użytkowe addBranches i removeBranches). Metody te zapewniają, że zawsze synchronizujemy obie strony asocjacji, aby uniknąć problemów z uszkodzeniem obiektu lub danych relacyjnych.
  • Element potomny, Branch, implementuje metody equals i hashCode. Ponieważ nie możemy polegać na naturalnym identyfikatorze dla sprawdzania równości, musimy użyć identyfikatora encji zamiast niego. Jednakże, musimy to zrobić poprawnie, tak aby równość była spójna we wszystkich przejściach stanu encji. Ponieważ polegamy na równości dla removeBranches, dobrą praktyką jest nadpisanie equals i hashCode dla encji dziecka w asocjacji dwukierunkowej.
  • Asocjacja @OneToMany jest z definicji asocjacją rodzica, nawet jeśli jest jednokierunkowa lub dwukierunkowa. Tylko strona nadrzędna asocjacji ma sens, aby kaskadować jej przejścia stanu encji do dzieci.

.

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany.