A @OneToMany kapcsolat leképezésének leghatékonyabb módja JPA-val és Hibernate-tel
Azért választottam ezt a bejegyzést ezzel az idézettel, mert Linus Torvalds rajongója vagyok.😉
Ez az első cikkem. Ebben a One-to-Many/Many-to-One
entitás társítás minden lehetséges esetét le fogom fedni. A többi Many-to-Many
és One-to-One
a következő cikkekben lesz lefedve.
Remélem, hogy ez biztosan segít minden újoncnak, aki meg akarja tanulni a jpa/hibernate-t, Kérem olvassa el az egészet 😛
MEGJEGYZÉS:
Itt az
One-to-Many/Many-to-One
leképezés minden lehetséges esetét lefedtem. Az összes közül , a kétirányú `@OneToMany` asszociáció a legjobb módja az egy a sokhoz adatbázis kapcsolat leképezésének.
A hibernate asszociáció One-to-One
, One-to-Many/Many-to-One
és Many-to-Many
kategóriákba sorolható.
- A kapcsolat iránya lehet kétirányú vagy egyirányú.
- A kétirányú kapcsolatnak van egy tulajdonosi és egy fordított oldala.
- Az egyirányú kapcsolatnak csak egy tulajdonosi oldala van. A kapcsolat birtokló oldala határozza meg, hogy a Persistence futási ideje hogyan frissíti a kapcsolatot az adatbázisban.
- Az unidirekcionális olyan kapcsolat, ahol az egyik oldal nem tud a kapcsolatról.
- Az unidirekcionális kapcsolatban csak az egyik entitás rendelkezik olyan kapcsolati mezővel vagy tulajdonsággal, amely a másikra utal. Például a tételsornak lenne egy kapcsolati mezője, amely a terméket azonosítja, de a terméknek nem lenne kapcsolati mezője vagy tulajdonsága a tételsorhoz. Más szóval a Line Item tud a Productről, de a Product nem tudja, hogy mely Line Item példányok hivatkoznak rá.
Bidirekcionális kapcsolatok:
- A bidirekcionális kapcsolat mindkét irányban navigációs hozzáférést biztosít, így explicit lekérdezések nélkül is elérheti a másik oldalt.
- A bidirekcionális kapcsolatban mindkét entitás rendelkezik olyan kapcsolati mezővel vagy tulajdonsággal, amely a másik entitásra utal. A kapcsolati mezőn vagy tulajdonságon keresztül egy entitásosztály kódja elérheti a kapcsolódó objektumot. Ha egy entitásnak van kapcsolati mezője, akkor azt mondjuk, hogy az entitás “tud” a kapcsolódó objektumáról. Például, ha Order tudja, hogy milyen Line Item példányai vannak, és ha Line Item tudja, hogy milyen Order tartozik hozzá, akkor kétirányú kapcsolatuk van.
A kétirányú kapcsolatoknak a következő szabályokat kell követniük.
- A kétirányú kapcsolat inverz oldalának a
@OneToOne
,@OneToMany
vagy@ManyToMany
megjegyzésmappedBy
elemének használatával kell hivatkoznia a tulajdonos oldalra (az idegen kulcsot tartalmazó entitás). AmappedBy
elem azt a tulajdonságot vagy mezőt jelöli az entitásban, amely a kapcsolat tulajdonosa. - A
@ManyToOne
kétirányú kapcsolatok sokadik oldala nem határozhatja meg amappedBy
elemet. A many oldal mindig a kapcsolat tulajdonos oldala. - A
@OneToOne
kétirányú kapcsolatok esetében a birtokos oldal annak az oldalnak felel meg, amelyik @JoinColumn-t, azaz a megfelelő idegen kulcsot tartalmazza. - A
@ManyToMany
kétirányú kapcsolatok esetében bármelyik oldal lehet a tulajdonos oldal.
@OneToMany kapcsolat a JPA-val és a Hibernate-tel
Egyszerűen fogalmazva, az egy a sokhoz leképezés azt jelenti, hogy egy táblázat egy sorát egy másik táblázat több sorára képezzük le.
Mikor használjuk az egy a sokhoz leképezést
Az egy a sokhoz leképezést használjuk az entitások vagy objektumok közötti 1…N kapcsolat létrehozására.
Két entitást kell írnunk, azaz.
Company
ésBranch
úgy, hogy egyetlen vállalathoz több fióktelep is társítható legyen, de egyetlen fióktelep nem osztható meg két vagy több vállalat között.
Hibernate one to many mapping megoldások:
- One to many mapping with foreign key association
- One to many mapping with join table
Ezt a problémát kétféleképpen lehet megoldani.
Az egyik, hogy van egy idegen kulcs oszlop a branch táblában, azaz
company_id
. Ez az oszlop a Cég tábla elsődleges kulcsára fog hivatkozni. Így nem lehet két fióktelepet több vállalathoz társítani.A második megközelítés az, hogy van egy közös join tábla, mondjuk
Company_Branch,
Ez a tábla két oszloppal rendelkezik, azazcompany_id
ami idegen kulcs lesz, és a vállalat tábla elsődleges kulcsára utal, és hasonlóanbranch_id
ami idegen kulcs lesz, és a fióktelep tábla elsődleges kulcsára utal.
Ha a @OneToMany/@ManyToOne nem rendelkezik tükrözött @ManyToOne/@OneToMany társítással a gyermek oldalon, akkor a @OneToMany/@ManyToOne társítás egyirányú.
@OneToMany egyirányú kapcsolat
Ebben a megközelítésben bármelyik entitás felelős a kapcsolat létrehozásáért és fenntartásáért. Vagy a Vállalat deklarálja a kapcsolatot egy a sokhoz, vagy az Ágazat deklarálja a kapcsolatot a végéről sok az egyhez.
1. ESET: (Mapping with foreign key association)
Ha csak a @OneToMany
-t használjuk, akkor 3 tábla lesz. Ilyen a company
, branch
és company_branch.
MEGJEGYZÉS:
A fenti példában olyan kifejezéseket használtam, mint cascade, orphanRemoval, fetch és targetEntity, amelyeket a következő bejegyzésemben fogok elmagyarázni.
A company_branch
táblának két idegen kulcsa lesz company_id
és branch_id
.
Most, ha egy Company-t és két Branch(s)-et tartósítunk:
Hibernate a következő SQL utasításokat fogja végrehajtani:
- A
@OneToMany
asszociáció definíció szerint szülő(nem tulajdonos) asszociáció, még akkor is, ha egyirányú vagy kétirányú. Csak az asszociáció szülői oldalán van értelme az entitásállapot-átmenetek kaszkádosításának a gyermekekre. - A
Company
entitás perszisztálásakor a kaszkádosítás a persist műveletet a mögöttes Branch gyermekekre is továbbítja. Az ágak gyűjteményéből valóBranch
eltávolításakor az asszociációs sor törlődik a link táblából, és aorphanRemoval
attribútum is kiváltja az ágak eltávolítását.
Az egyirányú asszociációk nem túl hatékonyak, amikor a gyermek entitások eltávolításáról van szó. Ebben a konkrét példában a perszisztencia-kontextus kiürítésekor a Hibernate törli az összes gyermek adatbázis-bejegyzést, és újra beszúrja azokat, amelyek még megtalálhatók a memóriában lévő perszisztencia-kontextusban.
A kétirányú @OneToMany
társítás viszont sokkal hatékonyabb, mivel a gyermek entitás irányítja a társítást.
2. ESET: (Mapping with foreign key association)
Ha csak @ManyToOne
-t használunk, akkor 2 tábla lesz. Például cég, fióktelep.
Ez a fenti kód 2 táblát fog generálni Company(company_id,name)
és Branch(branch_id,name,company_company_id)
. Itt a Branch a tulajdonos oldal, mivel idegenkulcs-asszociációval rendelkezik.
3. ESET: (Mapping idegenkulcs-asszociációval)
Ha mind a @ManyToOne
, mind a @OneToMany
kódot használjuk, akkor 3 táblát hoz létre Company(id,name), Branch(id,name,company_id), Company_Branch(company_id,branch_id)
Az alábbi leképezés kétirányúnak tűnhet, de nem az. Nem egy kétirányú relációt definiál, hanem két különálló egyirányú relációt.
4. ESET: (Unidirectional @OneToMany with @JoinColumn)(Mapping with foreign key association)
Ha a @OneToMany
-t használjuk a @JoinColumn
-vel, akkor 2 tábla lesz. Ilyen például a company, branch
A fenti entitásban a @JoinColumn
-en belül a name az idegen kulcs oszlop nevére utal, ami a companyId i.azaz company_id
, a rferencedColumnName pedig az elsődleges kulcsot, azaz id
az entitás (Company) elsődleges kulcsát jelöli, amelyre a companyId
idegen kulcs utal.
5. ESET: (Mapping with foreign key association)
Ha a @ManyToOne
-t használjuk a @JoinColumn
-vel, akkor 2 tábla lesz. Ilyen a company,branch.
A @JoinColumn
megjegyzés segít a Hibernate-nek kitalálni, hogy van egy company_id
Foreign Key oszlop a branch táblában, amely meghatározza ezt a társítást.
Az elágazás rendelkezik a company_id
idegenkulccsal, tehát ez a tulajdonos oldala.
Most, ha megmarad 1 cég és 2 Branch(s):
Az elsős bejegyzés eltávolításakor a gyermek gyűjteményből:
company.getBranches().remove(0);
Hibernate két utasítást hajt végre egy helyett :
- Először az idegen kulcs mezőt nullára teszi (hogy megszakítsa a szülővel való társítást), majd törli a rekordot.
Update branch set branch_id = null where where id = 1 delete from branch where id = 1 ;
6. ESET: (Mapping with join table)
Most, nézzük meg a one-to-many
relációt, ahol Person(id,name)
több Veichle(id,name,number)
-hez társul, és több jármű is tartozhat ugyanahhoz a személyhez.
Egy nap az autópálya benzinkútján Carlos seriff néhány elhagyott járművet talált, valószínűleg lopottat. Most a seriffnek frissítenie kell a járművek adatait (szám, név) az adatbázisban, de a fő probléma az, hogy nincs tulajdonosa ezeknek a járműveknek, így a person_id(foreign key )
mező nulla marad.
Most a sheriff két lopott járművet ment el az adatbázisba a következőképpen:
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);
Ahibernate a következő SQL utasításokat fogja végrehajtani:
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 |
---------------------
Ez a fenti stratégia arra kényszerít minket, hogy null értékeket tegyünk az oszlopba az opcionális kapcsolatok kezelésére.
Tipikusan many-to-many
kapcsolatokra gondolunk, amikor egy join table
-re gondolunk, de egy join tábla használata ebben az esetben segíthet nekünk kiküszöbölni ezeket a null értékeket:
Ez a megközelítés egy join táblát használ a Branch és Company entitások közötti kapcsolatok tárolására. A @JoinTable
megjegyzést használtuk ennek a társításnak a létrehozásához.
Ebben a példában a @JoinTable
megjegyzést alkalmaztuk a Jármű oldalon(Many Side).
Lássuk, hogyan fog kinézni az adatbázis sémája:
A fenti kód 3 táblát fog létrehozni, úgymint person(id,name)
, vehicle(id,name)
és vehicle_person(vehicle_id,person_id)
. Itt a vehicle_person
fogja tartani az idegen kulcs kapcsolatot mind a Személy, mind a Jármű entitásokhoz.
Így amikor a sheriff elmenti a jármű adatait, nem kell null értéket tárolni a jármű táblában, mert az idegen kulcs kapcsolat a vehicle_person
táblákban van, nem pedig a jármű táblában.
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);
A Hibernate a következő SQL utasításokat fogja végrehajtani:
insert into vehicle (id, name) values (1, "ford"); insert into vehicle (id, name) values (2, "gmc");id |name|
-----|----|
1 |ford|
2 |benz|
-----------
A fenti példa azt mutatja, hogyan szabadultunk meg a null érték beszúrásától.
@OneToMany Kétirányú kapcsolat
- A kétirányú
@OneToMany
asszociációhoz a gyermek oldalon is szükséges egy@ManyToOne
asszociáció. Bár a tartománymodell két oldalt tár fel ennek az asszociációnak a navigálásához, a színfalak mögött a relációs adatbázisnak csak egy idegen kulcsa van ehhez a kapcsolathoz. - Minden kétirányú asszociációnak csak egy birtokló oldala (a gyermekoldal) lehet, a másik oldalra inverz (vagy a
mappedBy
) oldalként hivatkozunk. - Ha a
@OneToMany
-t használjuk amappedBy
attribútummal együtt, akkor kétirányú asszociációnk van, ami azt jelenti, hogy amappedBy
által hivatkozott gyermekoldalon egy@ManyToOne
asszociációra van szükségünk. - A
mappedBy
elem kétirányú kapcsolatot definiál. Ez az attribútum lehetővé teszi, hogy mindkét oldalról hivatkozhassunk a kapcsolódó entitásokra.
A @OneToMany
asszociáció leképezésének legjobb módja, ha a @ManyToOne
oldalra támaszkodunk az összes entitásállapot-változás terjedésében.
Ha 2 Branch(s)
Hibernate csak egy SQL utasítást generál minden egyes persistált Branch entitáshoz:
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);
Ha eltávolítunk egy Branch:
Company company = entityManager.find( Company.class, 1L ); Branch branch = company.getBranches().get(0); company.removeBranches(branch);
Mindössze egy delete SQL utasítás kerül végrehajtásra:
delete from Branch where id = 1
A kétirányú @OneToMany asszociáció tehát a legjobb módja az egy-másik adatbázis kapcsolat leképezésének, ha valóban szükségünk van a gyűjteményre az asszociáció szülői oldalán.
@JoinColumn
Megad egy oszlopot egy entitás társításhoz vagy elemgyűjteményhez való csatlakozáshoz. A@JoinColumn
megjegyzés azt jelzi, hogy ez az entitás a kapcsolat tulajdonosa. Ez azt jelenti, hogy a megfelelő táblának van egy idegen kulcsú oszlopa a hivatkozott táblához.A fenti példában a tulajdonos Entity Branch, rendelkezik egy
company_id
nevű Join Column-al, amelynek idegen kulcsa van a nem tulajdonos Company entitáshoz.Ha kétirányú társítás jön létre, az alkalmazásfejlesztőnek gondoskodnia kell arról, hogy mindkét oldal mindig szinkronban legyen. A
addBranches()
és aremoveBranches()
segédmódszerek, amelyek mindkét véget szinkronizálják, amikor egy gyermekelem(pl. Branch) hozzáadása vagy eltávolítása történik.Az egyirányú
@OneToMany
-vel ellentétben a kétirányú társítás sokkal hatékonyabb a gyűjtemény perzisztenciaállapotának kezelése során.Minden elem eltávolítása csak egyetlen frissítést igényel (amelyben az idegen kulcs oszlopot NULL-ra állítjuk), és ha a gyermek entitás életciklusa a tulajdonos szülőhöz van kötve úgy, hogy a gyermek nem létezhet a szülő nélkül, akkor a társítást az orphan-removal attribútummal annotálhatjuk, és a gyermek szétkapcsolása a tényleges gyermek táblasoron is törlési utasítást indít.
MEGJEGYZÉS:
- A fenti kétirányú példában a Szülő és a Gyermek kifejezés az Entitás/OO/Modellben( azaz a java osztályban) az SQL-ben a nem nyertes/fordított, illetve a tulajdonosi oldalra utal.
A java szempontjából a Vállalat a Szülő és az ág a gyermek. Mivel egy ág nem létezhet szülő nélkül.
Az SQL szempontjából a Branch a Tulajdonos oldal, a Company pedig a Nem-győztes (Inverz) oldal. Mivel N ághoz 1 cég tartozik, minden ág tartalmaz egy idegen kulcsot a hozzá tartozó céghez. ez azt jelenti, hogy az ág “birtokolja” (vagy szó szerint tartalmazza) a kapcsolatot (információt). Ez pontosan az ellenkezője az OO/modell világának.
@OneToMany Kétirányú kapcsolat(leképezés join táblával)
A CASE 6
-től egyirányú leképezésünk van a @JoinTable
-vel, így ha a Person entitáshoz hozzáadjuk a mappedBy
attribútumot, akkor a kapcsolat kétirányúvá válik.
SUMMARY
A fent említett leképezéssel kapcsolatban több dolgot is meg kell jegyeznünk:
- A
@ManyToOne
asszociációFetchType.LAZY
-t használ, mert különben visszalépnénk az EAGER lekérdezéshez, ami rossz hatással van a teljesítményre. - A kétirányú asszociációkat mindig mindkét oldalon frissíteni kell, ezért a szülői oldalnak tartalmaznia kell a
addChild
ésremoveChild
kombót (a szülői oldal Company két segédmódszert tartalmazaddBranches
ésremoveBranches
). Ezek a metódusok biztosítják, hogy mindig szinkronizáljuk az asszociáció mindkét oldalát, hogy elkerüljük az objektum- vagy relációs adatrongálási problémákat. - A gyermek entitás, Branch, implementálja az equals és a hashCode metódusokat. Mivel az egyenlőségi ellenőrzéseknél nem támaszkodhatunk természetes azonosítóra, helyette az entitás azonosítót kell használnunk. Ezt azonban megfelelően kell tennünk, hogy az egyenlőség konzisztens legyen az összes entitásállapot-átmenet során. Mivel a removeBranches esetében az egyenlőségre támaszkodunk, jó gyakorlat az equals és a hashCode felülírása a gyermek entitás számára egy kétirányú asszociációban.
- A @OneToMany asszociáció definíció szerint szülői asszociáció, még akkor is, ha egyirányú vagy kétirányú. Csak az asszociáció szülői oldalán van értelme az entitásállapot-átmenetek kaszkádosításának a gyermekekre.