Najwydajniejszy sposób mapowania relacji @OneToMany za pomocą JPA i Hibernate
Wybrałem, aby otworzyć ten post tym cytatem, ponieważ jestem fanem Linusa Torvaldsa 😉
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 strona
mappedBy
). - Jeśli używamy
@OneToMany
z ustawionym atrybutemmappedBy
, 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()
iremoveBranches()
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żywaFetchType.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
iremoveChild
(Parent side Company zawiera dwie metody użytkoweaddBranches
iremoveBranches
). 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.
.