Meest efficiënte manier om een @OneToMany relatie in kaart te brengen met JPA en Hibernate
Ik heb ervoor gekozen om deze post met dit citaat te openen omdat ik een fan ben van Linus Torvalds.😉
Dit is mijn allereerste artikel. Hierin zal ik alle mogelijke gevallen in One-to-Many/Many-to-One
entiteitassociatie behandelen. De resterende Many-to-Many
en One-to-One
zullen in volgende artikelen worden behandeld.
Ik hoop dat dit zeker alle nieuwelingen zal helpen die jpa/hibernate willen leren. Lees het hele artikel 😛
NOTE:
Hier heb ik alle mogelijke gevallen van
One-to-Many/Many-to-One
mapping behandeld. Van alle, bidirectionele `@OneToMany` associaties is de beste manier om een één-op-veel database relatie in kaart te brengen.
De hibernate associatie geclassificeerd in One-to-One
, One-to-Many/Many-to-One
en Many-to-Many
.
- De richting van een relatie kan zowel bidirectioneel als unidirectioneel zijn.
- Een bidirectionele relatie heeft zowel een bezittende zijde als een inverse zijde.
- Een unidirectionele relatie heeft alleen een bezittende zijde. De bezittende kant van een relatie bepaalt hoe de Persistence-runtime updates aan de relatie in de database uitvoert.
- Unidirectioneel is een relatie waarbij één kant geen weet heeft van de relatie.
- In een unidirectionele relatie heeft slechts één entiteit een relatieveld of eigenschap die naar de andere verwijst. Bijvoorbeeld, Line Item zou een relatieveld hebben dat Product identificeert, maar Product zou geen relatieveld of eigenschap hebben voor Line Item. Met andere woorden, Line Item weet van Product, maar Product weet niet welke Line Item instanties ernaar verwijzen.
Bidirectionele relaties:
- Bidirectionele relaties bieden navigatietoegang in beide richtingen, zodat u de andere kant kunt benaderen zonder expliciete query’s.
- In een bidirectionele relatie heeft elke entiteit een relatieveld of eigenschap die naar de andere entiteit verwijst. Via het relatieveld of de eigenschap kan de code van een entiteitklasse toegang krijgen tot het gerelateerde object. Als een entiteit een verwant veld heeft, wordt gezegd dat de entiteit “weet” over zijn verwante object. Als bijvoorbeeld Order weet welke instanties van Line Item het heeft en als Line Item weet tot welke Order het behoort, hebben zij een bidirectionele relatie.
Bidirectionele relaties moeten aan deze regels voldoen.
- De inverse kant van een bidirectionele relatie moet verwijzen naar zijn eigen kant (Entity die de foreign key bevat) door gebruik te maken van het
mappedBy
element van de@OneToOne
,@OneToMany
, of@ManyToMany
annotatie. HetmappedBy
element duidt de eigenschap of het veld in de entiteit aan die de eigenaar van de relatie is. - De veel-zijde van
@ManyToOne
bidirectionele relaties mag hetmappedBy
element niet definiëren. De vele kant is altijd de bezittende kant van de relatie. - Voor
@OneToOne
bidirectionele relaties komt de “owning side” overeen met de kant die @JoinColumn bevat, d.w.z. de overeenkomstige foreign key. - Voor
@ManyToMany
bidirectionele relaties kan elke zijde de owning side zijn.
@OneToMany-relatie met JPA en Hibernate
Vereenvoudig gezegd betekent one-to-many mapping dat een rij in een tabel wordt toegewezen aan meerdere rijen in een andere tabel.
Wanneer one to many mapping gebruiken
Gebruik one to mapping om 1…N relatie tussen entiteiten of objecten te creëren.
We moeten twee entiteiten schrijven, nl.
Company
enBranch
zodanig dat meerdere filialen kunnen worden geassocieerd met een enkel bedrijf, maar een enkel filiaal kan niet worden gedeeld tussen twee of meer bedrijven.
Hibernate one to many mapping oplossingen:
- One to many mapping met foreign key associatie
- One to many mapping met join table
Dit probleem kan worden opgelost op twee verschillende manieren.
Een is om een foreign key kolom in branch tabel, dat wil zeggen
company_id
hebben. Deze kolom zal verwijzen naar de primaire sleutel van de tabel Bedrijf. Op deze manier kan geen twee filialen worden geassocieerd met meerdere company.Tweede benadering is om een gemeenschappelijke join tabel laten we zeggen
Company_Branch,
Deze tabel zal twee kolommen hebben, te wetencompany_id
die zal foreign key die verwijst naar de primaire sleutel in Company tabel en evenzobranch_id
die zal foreign key die verwijst naar de primaire sleutel van de Branch tabel zijn.
Als @OneToMany/@ManyToOne geen spiegelende @ManyToOne/@OneToMany-associatie heeft aan de kindzijde, dan is de @OneToMany/@ManyToOne-associatie unidirectioneel.
@OneToMany Unidirectionele relatie
In deze benadering is één entiteit verantwoordelijk voor het leggen en onderhouden van de relatie. Ofwel verklaart het bedrijf de relatie als één op veel, ofwel verklaart de branche de relatie van zijn kant als veel op één.
CASE 1: (Mapping met foreign key associatie)
Als we alleen @OneToMany
gebruiken, dan zullen er 3 tabellen zijn. Zoals company
, branch
en company_branch.
NOTE:
In het bovenstaande voorbeeld heb ik termen gebruikt als cascade, orphanRemoval, fetch en targetEntity, die ik in mijn volgende post zal toelichten.
Tabel company_branch
zal twee foreign key company_id
en branch_id
hebben.
Nu, als we één bedrijf en twee filialen persisteren:
Hibernate gaat de volgende SQL-statements uitvoeren:
- De
@OneToMany
-associatie is per definitie een ouder(niet-eigen)-associatie, zelfs als het een unidirectionele of bidirectionele associatie is. Alleen de ouderzijde van een associatie heeft zin om de transities in de entiteitstoestand te cascaderen naar de kinderen. - Bij het persisteren van de
Company
entiteit, zal de cascade de persist operatie ook propageren naar de onderliggende Branch kinderen. Bij het verwijderen van eenBranch
uit de tak-collectie, wordt de rij van de associatie verwijderd uit de koppelingstabel, en hetorphanRemoval
attribuut zal ook een tak-verwijdering triggeren.
De unidirectionele associaties zijn niet erg efficiënt als het gaat om het verwijderen van kind-entiteiten. In dit specifieke voorbeeld verwijdert Hibernate bij het flushen van de persistentiecontext alle kindentiteiten uit de database en voegt de entiteiten die zich nog in de in-memory persistentiecontext bevinden opnieuw in.
Een bidirectionele @OneToMany
associatie is daarentegen veel efficiënter omdat de kindentiteit de associatie controleert.
CASE 2: (Mapping met foreign key associatie)
Als we alleen @ManyToOne
gebruiken, dan zullen er 2 tabellen zijn. Zoals bedrijf, filiaal.
CASE 4: (Unidirectionele @OneToMany met @JoinColumn)(Mapping met foreign key associatie)
Als we @OneToMany
gebruiken met @JoinColumn
dan zullen er 2 tabellen zijn. Zoals bedrijf, filiaal
In de bovenstaande entiteit binnen @JoinColumn
, verwijst naam naar de naam van de foreign key-kolom die companyId i. isd.w.z. company_id
en rferencedColumnName verwijst naar de primaire sleutel d.w.z. id
van de entiteit (Bedrijf) waarnaar de foreign key companyId
verwijst.
CASE 5: (Mapping met foreign key associatie)
Als we @ManyToOne
gebruiken met @JoinColumn
dan zullen er 2 tabellen zijn. Zoals company,branch.
De @JoinColumn
annotatie helpt Hibernate om uit te vinden dat er een company_id
Foreign Key kolom in de branch tabel is die deze associatie definieert.
Branch zal de foreign key company_id
hebben, dus het is de eigenaar kant.
Nu, als we 1 bedrijf en 2 filialen vasthouden:
wanneer we de eerste invoer uit de kinderverzameling verwijderen:
company.getBranches().remove(0);
Hibernate voert twee statements uit in plaats van één :
- Eerst wordt het foreign key-veld op nul gezet (om de associatie met de ouder te verbreken) en vervolgens wordt het record verwijderd.
Update branch set branch_id = null where where id = 1 delete from branch where id = 1 ;
CASE 6. (Mapping met join-tabel)
(Mapping met join table)
Nu kijken we naar one-to-many
relatie waarbij Person(id,name)
geassocieerd wordt met meerdere Veichle(id,name,number)
en meerdere voertuigen aan dezelfde persoon kunnen toebehoren.
Op een dag vond de sheriff Carlos op de snelweg een paar verlaten voertuigen, hoogstwaarschijnlijk gestolen. Nu sheriff heeft om de voertuigen details (nummer, naam) in hun database, maar het grootste probleem is, is er geen eigenaar voor deze voertuigen, dus person_id(foreign key )
-veld zal blijven null.
Nu slaat sheriff twee gestolen voertuigen op in db als volgt:
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 gaat de volgende SQL-statements uitvoeren:
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 |
---------------------
Deze bovenstaande strategie dwingt ons om null-waarden in de kolom te zetten om optionele relaties te verwerken.
Typisch denken we aan many-to-many
-relaties wanneer we aan een join table
denken, maar het gebruik van een join-tabel kan ons in dit geval helpen deze nulwaarden te elimineren:
Deze aanpak maakt gebruik van een join-tabel om de associaties tussen Branch- en Company-entiteiten op te slaan. @JoinTable
annotatie is gebruikt om deze associatie te maken.
In dit voorbeeld hebben we @JoinTable
toegepast op Voertuig zijde (Veel zijde).
Laten we eens kijken hoe het databaseschema eruit zal zien:
De bovenstaande code zal 3 tabellen genereren, zoals person(id,name)
, vehicle(id,name)
en vehicle_person(vehicle_id,person_id)
. vehicle_person
bevat de vreemde-sleutelrelatie met de entiteiten Persoon en Voertuig.
Dus wanneer de sheriff de gegevens van het voertuig opslaat, hoeft er geen nulwaarde in de voertuigtabel te worden ingevoerd omdat we de vreemde-sleutelrelatie in vehicle_person
-tabellen behouden en niet in de voertuigtabel.
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 gaat de volgende SQL-statements uitvoeren:
insert into vehicle (id, name) values (1, "ford"); insert into vehicle (id, name) values (2, "gmc");id |name|
-----|----|
1 |ford|
2 |benz|
-----------
Dit bovenstaande voorbeeld laat zien, hoe we ons hebben ontdaan van null value insertion.
@OneToMany Bi-directional Relationship
- De bidirectionele
@OneToMany
associatie vereist ook een@ManyToOne
associatie aan de kindzijde. Hoewel het Referentiemodel twee kanten laat zien om door deze associatie te navigeren, heeft de relationele database achter de schermen slechts één foreign key voor deze relatie. - Elke bidirectionele associatie moet slechts één bezittende kant hebben (de kindkant), waarbij de andere kant wordt aangeduid als de inverse (of de
mappedBy
) kant. - Als we de
@OneToMany
gebruiken met hetmappedBy
-attribuut ingesteld, dan hebben we een bidirectionele associatie, wat betekent dat we een@ManyToOne
-associatie moeten hebben aan de kindzijde waarnaar demappedBy
verwijst. - Het
mappedBy
-element definieert een bidirectionele relatie. Met dit attribuut kunt u van beide kanten naar de geassocieerde entiteiten verwijzen.
De beste manier om een @OneToMany
-associatie in kaart te brengen, is door te vertrouwen op de @ManyToOne
-kant om alle veranderingen in de entiteitstatus door te geven.
Als we 2 takken persisteren
Hibernate genereert slechts één SQL-instructie voor elke gepercipieerde tak-entiteit:
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);
Als we een tak verwijderen:
Company company = entityManager.find( Company.class, 1L ); Branch branch = company.getBranches().get(0); company.removeBranches(branch);
Er wordt slechts één delete SQL-instructie uitgevoerd:
delete from Branch where id = 1
Dus, de bidirectionele @OneToMany associatie is de beste manier om een one-to-many database relatie in kaart te brengen wanneer we echt de collectie aan de bovenliggende kant van de associatie nodig hebben.
@JoinColumn
Specificeert een kolom voor het koppelen van een entiteitsassociatie of een elementverzameling. De annotatie@JoinColumn
geeft aan dat deze entiteit de eigenaar is van de relatie. Dat wil zeggen dat de bijbehorende tabel een kolom heeft met een foreign key naar de tabel waarnaar wordt verwezen.In het bovenstaande voorbeeld heeft de eigenaar Entity Branch, een Join Column met de naam
company_id
die een foreign key heeft naar de entiteit Company die geen eigenaar is.Wanneer een bidirectionele associatie wordt gevormd, moet de applicatie-ontwikkelaar ervoor zorgen dat beide zijden te allen tijde in-sync zijn. De
addBranches()
enremoveBranches()
zijn hulpprogramma’s methoden die beide uiteinden synchroniseren wanneer een kind element (dwz tak) wordt toegevoegd of verwijderd.In tegenstelling tot de unidirectionele
@OneToMany
, de bidirectionele vereniging is veel efficiënter bij het beheer van de collectie persistentie staat.Elke element verwijdering vereist slechts een enkele update (waarin de foreign key kolom is ingesteld op NULL) en als het kind entiteit levenscyclus is gebonden aan zijn eigen ouder, zodat het kind niet kan bestaan zonder zijn ouder, dan kunnen we de associatie annoteren met de orphan-removal attribuut en disassociating het kind zal een delete statement teweegbrengen op de eigenlijke kind tabel rij ook.
NOTE:
- In bovenstaand tweerichtingsvoorbeeld verwijst de term Ouder en Kind in Entiteit/OO/Model (d.w.z. in de java-klasse) naar respectievelijk de niet-winnende/tegengestelde en de bezittende kant in SQL.
In java is het bedrijf hier de Ouder en het filiaal is het kind. Aangezien een filiaal niet kan bestaan zonder parent.
In SQL is Branch de Owner-kant en Company de Non-woning(Inverse)-kant. Aangezien er 1 bedrijf is voor N filialen, bevat elk filiaal een foreign key naar het bedrijf waartoe het behoort.Dit betekent dat filiaal “eigenaar” is (of letterlijk bevat) de verbinding (informatie). Dit is precies het tegenovergestelde van de OO/model wereld.
@OneToMany Bi-directionele relatie(Mapping met join table)
Vanuit CASE 6
hebben we Unidirectionele mapping met @JoinTable
, dus als we mappedBy
attribuut toevoegen aan Person entiteit dan zal de relatie bi-directioneel worden.
SUMMARY
Er zijn verschillende dingen op te merken over de bovengenoemde mapping:
- De
@ManyToOne
associatie gebruiktFetchType.LAZY
, omdat we anders zouden terugvallen op EAGER fetching, wat slecht is voor de prestaties. - De bidirectionele associaties moeten altijd aan beide kanten worden bijgewerkt, daarom moet de Ouder-kant de
addChild
enremoveChild
combo bevatten (Ouder-kant Bedrijf bevat twee utility methodenaddBranches
enremoveBranches
). Deze methoden zorgen ervoor dat we altijd beide zijden van de associatie synchroniseren, om problemen met object- of relationele gegevenscorruptie te voorkomen. - De kind-entiteit, Branch, implementeert de methoden equals en hashCode. Aangezien we niet kunnen vertrouwen op een natuurlijke identifier voor gelijkheidscontroles, moeten we in plaats daarvan de entiteitsidentifier gebruiken. We moeten dit echter op de juiste manier doen, zodat de gelijkheid consistent is over alle entiteit toestandsovergangen. Omdat we vertrouwen op gelijkheid voor de removeBranches, is het een goede gewoonte om equals en hashCode te overschrijven voor de kind-entiteit in een bidirectionele associatie.
- De @OneToMany associatie is per definitie een ouder associatie, zelfs als het een unidirectionele of een bidirectionele associatie is. Alleen de ouderzijde van een associatie heeft zin om zijn entiteitstatustransities te cascaderen naar kinderen.