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és mappedBy elemének használatával kell hivatkoznia a tulajdonos oldalra (az idegen kulcsot tartalmazó entitás). A mappedBy 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 a mappedBy 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 és Branch ú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:

  1. One to many mapping with foreign key association
  2. 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, azaz company_id ami idegen kulcs lesz, és a vállalat tábla elsődleges kulcsára utal, és hasonlóan branch_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 a orphanRemoval 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 amappedBy) oldalként hivatkozunk.
  • Ha a @OneToMany-t használjuk a mappedBy attribútummal együtt, akkor kétirányú asszociációnk van, ami azt jelenti, hogy a mappedBy á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 a removeBranches() 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 és removeChild kombót (a szülői oldal Company két segédmódszert tartalmaz addBranches és removeBranches). 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.

Vélemény, hozzászólás?

Az e-mail-címet nem tesszük közzé.