Nejúčinnější způsob mapování vztahu @OneToMany pomocí JPA a Hibernate
Tímto citátem jsem se rozhodl začít tento příspěvek, protože jsem fanouškem Linuse Torvaldse 😉
Toto je můj vůbec první článek. Budu se v něm zabývat všemi možnými případy v asociaci entit One-to-Many/Many-to-One
. Zbývající Many-to-Many
a One-to-One
budou pokryty v dalších článcích.
Doufám, že to určitě pomůže každému nováčkovi, který se chce naučit jpa/hibernate, Prosím přečtěte si celý článek 😛
POZNÁMKA:
Pokryl jsem zde všechny možné případy mapování
One-to-Many/Many-to-One
. Ze všech , Obousměrná asociace `@OneToMany` je nejlepší způsob mapování databázového vztahu one-to-many.
Hibernate asociace klasifikuje na One-to-One
, One-to-Many/Many-to-One
a Many-to-Many
.
- Směr vztahu může být obousměrný nebo jednosměrný.
- Obousměrný vztah má vlastní i inverzní stranu.
- Jednosměrný vztah má pouze vlastní stranu. Vlastní strana vztahu určuje způsob, jakým běhový čas Persistence provádí aktualizace vztahu v databázi.
- Jednosměrný je vztah, kde jedna strana o vztahu neví.
- V jednosměrném vztahu má pouze jedna entita pole nebo vlastnost vztahu, která odkazuje na druhou. Například položka linky by měla vztahové pole, které identifikuje produkt, ale produkt by neměl vztahové pole nebo vlastnost pro položku linky. Jinými slovy, Line Item ví o Product, ale Product neví, které instance Line Item na něj odkazují.
Obousměrné vztahy:
- Obousměrný vztah poskytuje navigační přístup v obou směrech, takže můžete přistupovat k druhé straně bez explicitních dotazů.
- V obousměrném vztahu má každá entita vztahové pole nebo vlastnost, která odkazuje na druhou entitu. Prostřednictvím vztahového pole nebo vlastnosti může kód třídy entit přistupovat k příbuznému objektu. Pokud má entita vztahové pole, říká se, že entita „ví“ o svém souvisejícím objektu. Například pokud Order ví, jaké instance Line Item má, a pokud Line Item ví, k jaké Order patří, mají obousměrný vztah.
Obousměrné vztahy musí dodržovat tato pravidla:
- Inverzní strana obousměrného vztahu musí odkazovat na svou vlastní stranu(Entita, která obsahuje cizí klíč) pomocí prvku
mappedBy
anotace@OneToOne
,@OneToMany
nebo@ManyToMany
. PrvekmappedBy
označuje vlastnost nebo pole v entitě, která je vlastníkem vztahu. - Mnohá strana
@ManyToOne
obousměrných vztahů nesmí definovat prvekmappedBy
. Mnohá strana je vždy vlastnickou stranou vztahu. - U obousměrných vztahů
@OneToOne
odpovídá vlastní strana straně, která obsahuje @JoinColumn, tj. odpovídající cizí klíč. - U obousměrných vztahů
@ManyToMany
může být vlastní stranou kterákoli strana.
@OneToMany vztah s JPA a Hibernate
Zjednodušeně řečeno, mapování one-to-many znamená, že jeden řádek v tabulce je mapován na více řádků v jiné tabulce.
Kdy použít mapování one to many
Mapování one to many použijeme k vytvoření vztahu 1…N mezi entitami nebo objekty.
Musíme zapsat dvě entity, tj.
Company
aBranch
tak, aby k jedné společnosti mohlo být přiřazeno více poboček, ale jedna jediná pobočka nemohla být sdílena mezi dvěma nebo více společnostmi.
Řešení mapování one to many v systému Hibernate:
- Mapování one to many s přiřazením cizího klíče
- Mapování one to many se spojovací tabulkou
Tento problém lze řešit dvěma různými způsoby.
Jedním je mít v tabulce poboček sloupec cizího klíče, tj. sloupec
company_id
. Tento sloupec bude odkazovat na primární klíč tabulky Company. Tímto způsobem nemohou být žádné dvě pobočky přiřazeny k více společnostem.Druhý přístup je mít společnou spojovací tabulku řekněme
Company_Branch,
Tato tabulka bude mít dva sloupce, tj.company_id
, který bude cizím klíčem odkazujícím na primární klíč v tabulce Společnost, a podobněbranch_id
, který bude cizím klíčem odkazujícím na primární klíč tabulky Pobočka.
Pokud @OneToMany/@ManyToOne nemá zrcadlovou asociaci @ManyToOne/@OneToMany respektive na straně potomka, pak je asociace @OneToMany/@ManyToOne jednosměrná.
@OneToMany Jednosměrný vztah
V tomto přístupu bude za vytvoření vztahu a jeho udržování odpovědná kterákoli z entit. Buď Společnost deklaruje vztah jako jeden k mnoha, Nebo Pobočka deklaruje vztah ze svého konce jako mnoho k jednomu.
PŘÍPAD 1: (Mapování s asociací cizího klíče)
Použijeme-li pouze @OneToMany
, pak budou existovat 3 tabulky. Například company
, branch
a company_branch.
POZNÁMKA:
V uvedeném příkladu jsem použil pojmy jako kaskáda, orphanRemoval, fetch a targetEntity, které vysvětlím v dalším příspěvku.
Tabulka company_branch
bude mít dva cizí klíče company_id
a branch_id
.
Nyní, pokud budeme perzistovat jednu firmu a dvě pobočky:
Hibernate provede následující příkazy SQL:
- Asociace
@OneToMany
je z definice nadřazená(nevlastnící) asociace, i když je jednosměrná nebo obousměrná. Pouze na straně nadřazené asociace má smysl kaskádovat přechody stavu její entity na děti. - Při persistenci entity
Company
se kaskáda rozšíří o operaci persist i na základní děti větve. Při odstraněníBranch
z kolekce větví se řádek asociace odstraní z tabulky odkazů a atributorphanRemoval
vyvolá také odstranění větve.
Jednosměrné asociace nejsou příliš efektivní, pokud jde o odstraňování podřízených entit. V tomto konkrétním příkladu Hibernate po propláchnutí kontextu persistence odstraní všechny podřízené položky databáze a znovu vloží ty, které se ještě nacházejí v kontextu persistence v paměti.
Naopak obousměrná asociace @OneToMany
je mnohem efektivnější, protože podřízená entita řídí asociaci.
PŘÍPAD 2: (Mapování s asociací cizího klíče)
Použijeme-li pouze @ManyToOne
, pak budou existovat 2 tabulky. Například firma, pobočka.
Tento výše uvedený kód vytvoří 2 tabulky Company(company_id,name)
a Branch(branch_id,name,company_company_id)
. Zde je Pobočka na straně vlastníka, protože má asociaci cizího klíče.
PŘÍPAD 3: (Mapování s asociací cizího klíče)
Použijeme-li @ManyToOne
i @OneToMany
, pak se vytvoří 3 tabulky Company(id,name), Branch(id,name,company_id), Company_Branch(company_id,branch_id)
Toto níže uvedené mapování může vypadat jako obousměrné, ale není tomu tak. Nedefinuje jeden obousměrný vztah, ale dva samostatné jednosměrné vztahy.
PŘÍPAD 4: (Jednosměrné @OneToMany s @JoinColumn)(Mapování s asociací cizího klíče)
Pokud použijeme @OneToMany
s @JoinColumn
, pak budou existovat 2 tabulky. Například firma, pobočka
Ve výše uvedené entitě uvnitř @JoinColumn
název odkazuje na název sloupce cizího klíče, což je companyId aj.tj. company_id
a rferencedColumnName označuje primární klíč, tj. id
entity (Company), ke které se cizí klíč companyId
vztahuje.
PŘÍPAD 5: (Mapování s přiřazením cizího klíče)
Použijeme-li @ManyToOne
s @JoinColumn
, pak budou existovat 2 tabulky. Například firma,pobočka.
Anotace @JoinColumn
pomůže Hibernate zjistit, že v tabulce pobočka existuje sloupec company_id
Cizí klíč, který definuje tuto asociaci.
Větev bude mít cizí klíč company_id
, takže je to strana vlastníka.
Nyní, pokud přetrvává 1 firma a 2 pobočky:
při odstraňování prvního záznamu z kolekce podřízených:
company.getBranches().remove(0);
Hibernate provede dva příkazy místo jednoho :
- Nejprve nastaví pole cizího klíče na null (aby se přerušila asociace s rodičem), pak záznam odstraní.
Update branch set branch_id = null where where id = 1 delete from branch where id = 1 ;
PŘÍPAD 6. V případě, že se v kolekci podřízených záznamů objevily dva příkazy, provede se jeden příkaz: (
Jednoho dne při jízdě na dálnici našel šerif Carlos několik opuštěných vozidel, pravděpodobně kradených. Nyní musí šerif aktualizovat údaje o vozidlech(číslo,jméno) ve své databázi, ale hlavní problém je, že pro tato vozidla neexistuje žádný vlastník, takže pole person_id(foreign key )
zůstane nulové.
Nyní šerif uloží dvě ukradená vozidla do db následujícím způsobem:
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 provede následující příkazy 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 |
---------------------
Tato výše uvedená strategie nás donutí vložit do sloupce nulové hodnoty, abychom mohli ošetřit volitelné vztahy.
Typicky myslíme na vztahy many-to-many
, když uvažujeme o join table
, ale použití spojovací tabulky nám v tomto případě může pomoci eliminovat tyto nulové hodnoty:
Tento přístup používá spojovací tabulku pro uložení asociací mezi entitami Branch a Company. K vytvoření této asociace byla použita anotace @JoinTable
.
V tomto příkladu jsme použili anotaci @JoinTable
na straně Vehicle(Many Side).
Podívejme se, jak bude vypadat schéma databáze:
Výše uvedený kód vytvoří 3 tabulky, například person(id,name)
, vehicle(id,name)
a vehicle_person(vehicle_id,person_id)
. Zde vehicle_person
bude držet vazbu cizího klíče k entitám Osoba i Vozidlo.
Takže když šerif uloží údaje o vozidle, nemusí se do tabulky vozidla persistovat žádná nulová hodnota, protože udržujeme vazbu cizího klíče v tabulkách vehicle_person
, nikoliv v tabulce vozidla.
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 provede následující příkazy SQL:
insert into vehicle (id, name) values (1, "ford"); insert into vehicle (id, name) values (2, "gmc");id |name|
-----|----|
1 |ford|
2 |benz|
-----------
Tento výše uvedený příklad ukazuje, jak jsme se zbavili vkládání nulových hodnot.
@OneToMany Obousměrný vztah
- Obousměrná asociace
@OneToMany
vyžaduje také asociaci@ManyToOne
na straně potomka. Ačkoli doménový model vystavuje dvě strany pro navigaci této asociace, v zákulisí má relační databáze pro tento vztah pouze jeden cizí klíč. - Každá obousměrná asociace musí mít pouze jednu vlastní stranu (stranu potomka), druhá se označuje jako inverzní (nebo
mappedBy
) strana. - Použijeme-li
@OneToMany
s nastaveným atributemmappedBy
, pak máme obousměrnou asociaci, což znamená, že na podřízené straně, na kterou semappedBy
odkazuje, musíme mít asociaci@ManyToOne
. - Prvek
mappedBy
definuje obousměrný vztah. Tento atribut umožňuje odkazovat na přidružené entity z obou stran.
Nejlepším způsobem mapování asociace @OneToMany
je spoléhat na to, že strana @ManyToOne
bude propagovat všechny změny stavu entit.
Pokud perzistujeme 2 větve (Branch)
Hibernate generuje pouze jeden příkaz SQL pro každou perzistovanou entitu 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);
Pokud odstraníme Branch:
Company company = entityManager.find( Company.class, 1L ); Branch branch = company.getBranches().get(0); company.removeBranches(branch);
Vykoná se pouze jeden SQL příkaz delete:
delete from Branch where id = 1
Obousměrná asociace @OneToMany je tedy nejlepší způsob, jak mapovat databázový vztah one-to-many, když opravdu potřebujeme kolekci na straně rodiče asociace.
@JoinColumn
Určuje sloupec pro připojení asociace entit nebo kolekce prvků. Anotace@JoinColumn
označuje, že tato entita je vlastníkem vztahu. To znamená, že odpovídající tabulka má sloupec s cizím klíčem k odkazované tabulce.Ve výše uvedeném příkladu má vlastník Entity Branch, sloupec Join s názvem
company_id
, který má cizí klíč k entitě Company, která není vlastníkem.Kdykoli je vytvořena obousměrná asociace, musí vývojář aplikace zajistit, aby obě strany byly vždy synchronizovány. Metody
addBranches()
aremoveBranches()
jsou obslužné metody, které synchronizují obě strany vždy, když je přidán nebo odebrán podřízený prvek(tj. Větev).Na rozdíl od jednosměrné
@OneToMany
je obousměrná asociace mnohem efektivnější při správě stavu persistence kolekce.Každé odebrání prvku vyžaduje pouze jedinou aktualizaci (při níž je sloupec cizího klíče nastaven na NULL), a pokud je životní cyklus podřízené entity vázán na jejího vlastního rodiče tak, že podřízená entita nemůže existovat bez svého rodiče, pak můžeme asociaci anotovat atributem orphan-removal a odpojení podřízené entity vyvolá příkaz k odstranění i na vlastním řádku podřízené tabulky.
POZNÁMKA:
- V uvedeném obousměrném příkladu se termín Rodič a Dítě v entitě/OO/Modelu( tj. v java třídě) vztahuje na Nevyhrávající/Inverzní, respektive Vlastní stranu v SQL.
V java pohledu je zde Firma rodičem a pobočka dítětem. Protože větev nemůže existovat bez rodiče.
V pohledu SQL je větev Vlastnická strana a Společnost je Ne-vlastnická(Inverzní) strana. Protože na N poboček připadá 1 firma, každá pobočka obsahuje cizí klíč k firmě, ke které patří. to znamená, že pobočka „vlastní“ (nebo doslova obsahuje) spojení (informace). To je přesně naopak než ve světě OO/modelu.
@OneToMany Obousměrný vztah(mapování pomocí join tabulky)
Z CASE 6
máme jednosměrné mapování pomocí @JoinTable
, takže pokud přidáme atribut mappedBy
k entitě Person, pak se vztah stane obousměrným.
SUMMARY
Na výše uvedeném mapování je třeba upozornit na několik věcí:
- Asociace
@ManyToOne
používáFetchType.LAZY
, protože jinak bychom se vrátili k načítání EAGER, což je špatné pro výkon. - Obousměrné asociace by měly být vždy aktualizovány na obou stranách, proto by nadřazená strana měla obsahovat kombinaci
addChild
aremoveChild
(Nadřazená strana Společnost obsahuje dvě užitkové metodyaddBranches
aremoveBranches
). Tyto metody zajišťují, že vždy synchronizujeme obě strany asociace, abychom se vyhnuli problémům s poškozením objektů nebo relačních dat. - Podřízená entita Branch implementuje metody equals a hashCode. Protože se při kontrole rovnosti nemůžeme spoléhat na přirozený identifikátor, musíme místo něj použít identifikátor entity. Musíme to však udělat správně, aby rovnost byla konzistentní při všech přechodech stavu entity. Protože se spoléháme na rovnost pro removeBranches, je dobrým zvykem přepsat equals a hashCode pro podřízenou entitu v obousměrné asociaci.
- Asociace @OneToMany je z definice nadřazenou asociací, i když je jednosměrná nebo obousměrná. Pouze na straně nadřazené asociace má smysl kaskádovat její přechody stavu entity na děti.