Der effizienteste Weg, eine @OneToMany-Beziehung mit JPA und Hibernate abzubilden
Ich habe diesen Beitrag mit diesem Zitat eröffnet, weil ich ein Fan von Linus Torvalds bin 😉
Dies ist mein erster Artikel überhaupt. In diesem werde ich alle möglichen Fälle der One-to-Many/Many-to-One
Entitätsassoziation behandeln. Die verbleibenden Many-to-Many
und One-to-One
werden in den nächsten Artikeln behandelt.
Ich hoffe, dass dies definitiv allen Neulingen helfen wird, die jpa/hibernate lernen wollen, bitte lesen Sie den ganzen Artikel 😛
NOTE:
Hier habe ich alle möglichen Fälle von
One-to-Many/Many-to-One
Mapping behandelt. Unter allen ist die bidirektionale `@OneToMany`-Assoziation der beste Weg, um eine One-to-Many-Datenbankbeziehung abzubilden.
Die Hibernate-Assoziation ist in One-to-One
, One-to-Many/Many-to-One
und Many-to-Many
unterteilt.
- Die Richtung einer Beziehung kann entweder bidirektional oder unidirektional sein.
- Eine bidirektionale Beziehung hat sowohl eine besitzende Seite als auch eine inverse Seite.
- Eine unidirektionale Beziehung hat nur eine besitzende Seite. Die besitzende Seite einer Beziehung bestimmt, wie die Persistenz-Laufzeit Aktualisierungen der Beziehung in der Datenbank vornimmt.
- Unidirektional ist eine Beziehung, bei der eine Seite nichts von der Beziehung weiß.
- Bei einer unidirektionalen Beziehung hat nur eine Entität ein Beziehungsfeld oder eine Eigenschaft, die sich auf die andere bezieht. Zum Beispiel hat Line Item ein Beziehungsfeld, das Product identifiziert, aber Product hat kein Beziehungsfeld oder eine Eigenschaft für Line Item. Mit anderen Worten, Line Item weiß über Product Bescheid, aber Product weiß nicht, welche Line Item-Instanzen auf es verweisen.
Bidirektionale Beziehungen:
- Bidirektionale Beziehungen bieten Navigationszugriff in beide Richtungen, so dass Sie ohne explizite Abfragen auf die andere Seite zugreifen können.
- In einer bidirektionalen Beziehung hat jede Entität ein Beziehungsfeld oder eine Eigenschaft, die auf die andere Entität verweist. Über das Beziehungsfeld oder die Eigenschaft kann der Code einer Entitätsklasse auf das verwandte Objekt zugreifen. Wenn eine Entität ein Beziehungsfeld hat, „weiß“ die Entität über ihr verwandtes Objekt Bescheid. Wenn z. B. „Order“ weiß, welche Instanzen von „Line Item“ es hat, und wenn „Line Item“ weiß, zu welcher „Order“ es gehört, besteht eine bidirektionale Beziehung.
Bidirektionale Beziehungen müssen diese Regeln befolgen.
- Die inverse Seite einer bidirektionalen Beziehung muss auf die besitzende Seite (Entität, die den Fremdschlüssel enthält) verweisen, indem das
mappedBy
-Element der@OneToOne
-,@OneToMany
– oder@ManyToMany
-Anmerkung verwendet wird. DasmappedBy
-Element bezeichnet die Eigenschaft oder das Feld in der Entität, die der Eigentümer der Beziehung ist. - Die Vielfachseite von
@ManyToOne
bidirektionalen Beziehungen darf dasmappedBy
-Element nicht definieren. Die Vielfachseite ist immer die besitzende Seite der Beziehung. - Bei
@OneToOne
bidirektionalen Beziehungen entspricht die besitzende Seite der Seite, die @JoinColumn enthält, d.h. dem entsprechenden Fremdschlüssel. - Bei
@ManyToMany
bidirektionalen Beziehungen kann jede Seite die besitzende Seite sein.
@OneToMany-Beziehung mit JPA und Hibernate
Einfach ausgedrückt, bedeutet One-to-Many-Mapping, dass eine Zeile in einer Tabelle auf mehrere Zeilen in einer anderen Tabelle abgebildet wird.
Wann verwendet man One-to-Many-Mapping
Verwenden Sie One-to-Mapping, um 1…N-Beziehungen zwischen Entitäten oder Objekten zu erstellen.
Wir müssen zwei Entitäten schreiben, d.h.
Company
undBranch
, so dass mehrere Niederlassungen mit einem einzigen Unternehmen verbunden werden können, aber eine einzelne Niederlassung kann nicht von zwei oder mehr Unternehmen gemeinsam genutzt werden.
Hibernate one to many mapping solutions:
- One to many mapping with foreign key association
- One to many mapping with join table
Dieses Problem kann auf zwei verschiedene Arten gelöst werden.
Eine besteht darin, eine Fremdschlüsselspalte in der Zweigstellentabelle zu haben, d.h.
company_id
. Diese Spalte verweist auf den Primärschlüssel der Tabelle „Unternehmen“. Auf diese Weise können keine zwei Zweigstellen mit mehreren Unternehmen verknüpft werden.Der zweite Ansatz besteht darin, eine gemeinsame Join-Tabelle, sagen wir
Company_Branch,
, zu haben. Diese Tabelle hat zwei Spalten, nämlichcompany_id
, die als Fremdschlüssel auf den Primärschlüssel der Tabelle Unternehmen verweist, undbranch_id
, die als Fremdschlüssel auf den Primärschlüssel der Tabelle Zweigstelle verweist.
Wenn @OneToMany/@ManyToOne keine spiegelnde @ManyToOne/@OneToMany-Assoziation auf der untergeordneten Seite hat, dann ist die @OneToMany/@ManyToOne-Assoziation unidirektional.
@OneToMany Unidirectional Relationship
Bei diesem Ansatz ist eine beliebige Entität für die Herstellung und Pflege der Beziehung verantwortlich. Entweder deklariert das Unternehmen die Beziehung als one to many, oder die Branche deklariert die Beziehung von ihrem Ende aus als many to one.
CASE 1: (Mapping mit Fremdschlüsselzuordnung)
Wenn wir nur @OneToMany
verwenden, gibt es 3 Tabellen. Wie company
, branch
und company_branch.
HINWEIS:
Im obigen Beispiel habe ich Begriffe wie Kaskade, orphanRemoval, fetch und targetEntity verwendet, die ich in meinem nächsten Beitrag erklären werde.
Die Tabelle company_branch
wird zwei Fremdschlüssel company_id
und branch_id
haben.
Wenn wir nun eine Firma und zwei Zweigstellen aufrechterhalten:
Hibernate wird die folgenden SQL-Anweisungen ausführen:
- Die
@OneToMany
Assoziation ist per Definition eine übergeordnete (nicht besitzende) Assoziation, auch wenn es sich um eine unidirektionale oder bidirektionale handelt. Nur auf der übergeordneten Seite einer Assoziation ist es sinnvoll, die Zustandsübergänge der Entität an die Kinder zu kaskadieren. - Wenn die
Company
Entität persistiert wird, überträgt die Kaskade die persist-Operation auch auf die zugrunde liegenden Verzweigungskinder. Beim Entfernen einerBranch
aus der Branch-Sammlung wird die Assoziationszeile aus der Verknüpfungstabelle gelöscht, und dasorphanRemoval
-Attribut löst ebenfalls eine Branch-Entfernung aus.
Die unidirektionalen Assoziationen sind nicht sehr effizient, wenn es um das Entfernen von Kind-Entitäten geht. In diesem speziellen Beispiel löscht Hibernate beim Flushen des Persistenzkontextes alle untergeordneten Datenbankeinträge und fügt diejenigen wieder ein, die sich noch im speicherinternen Persistenzkontext befinden.
Auf der anderen Seite ist eine bidirektionale @OneToMany
-Assoziation viel effizienter, da die untergeordnete Entität die Assoziation kontrolliert.
FALL 2: (Mapping mit Fremdschlüssel-Assoziation)
Wenn wir nur @ManyToOne
verwenden, gibt es 2 Tabellen. Z.B. Firma, Zweigstelle.
Dieser obige Code wird 2 Tabellen Company(company_id,name)
und Branch(branch_id,name,company_company_id)
erzeugen. Hier ist Branch die besitzende Seite, da sie die Fremdschlüssel-Assoziation hat.
FALL 3: (Mapping mit Fremdschlüssel-Assoziation)
Wenn wir sowohl @ManyToOne
als auch @OneToMany
verwenden, werden 3 Tabellen Company(id,name), Branch(id,name,company_id), Company_Branch(company_id,branch_id)
Dieses untenstehende Mapping könnte wie bidirektional aussehen, ist es aber nicht. Sie definiert nicht eine bidirektionale Beziehung, sondern zwei separate unidirektionale Beziehungen.
CASE 4: (Unidirektionales @OneToMany mit @JoinColumn)(Mapping mit Fremdschlüssel-Assoziation)
Wenn wir @OneToMany
mit @JoinColumn
verwenden, gibt es 2 Tabellen. Z.B. Firma, Niederlassung
In der obigen Entität innerhalb von @JoinColumn
bezieht sich name auf den Namen der Fremdschlüsselspalte, die companyId i.d.h. company_id
und rferencedColumnName bezeichnet den Primärschlüssel d.h. id
der Entität (Unternehmen), auf die sich der Fremdschlüssel companyId
bezieht.
FALL 5: (Mapping mit Fremdschlüssel-Assoziation)
Wenn wir @ManyToOne
mit @JoinColumn
verwenden, wird es 2 Tabellen geben. Zum Beispiel Firma, Zweigstelle.
Die @JoinColumn
-Anmerkung hilft Hibernate herauszufinden, dass es eine company_id
-Fremdschlüsselspalte in der Tabelle Zweigstelle gibt, die diese Assoziation definiert.
Die Verzweigung hat den Fremdschlüssel company_id
, ist also die Eigentümerseite.
Wenn nun 1 Unternehmen und 2 Zweigstellen bestehen:
wenn der erste Eintrag aus der Child-Sammlung entfernt wird:
company.getBranches().remove(0);
Hibernate führt zwei Anweisungen statt einer aus:
- Erst wird das Fremdschlüsselfeld auf null gesetzt (um die Assoziation mit dem Parent zu brechen), dann wird der Datensatz gelöscht.
Update branch set branch_id = null where where id = 1 delete from branch where id = 1 ;
FALL 6: (Mapping mit Join-Tabelle)
Betrachten wir nun die one-to-many
-Beziehung, bei der Person(id,name)
mit mehreren Veichle(id,name,number)
assoziiert wird und mehrere Fahrzeuge zu derselben Person gehören können.
Eines Tages fand der Sheriff Carlos auf der Autobahn einige verlassene Fahrzeuge, die wahrscheinlich gestohlen waren. Jetzt muss der Sheriff die Fahrzeugdaten (Nummer, Name) in seiner Datenbank aktualisieren, aber das Hauptproblem ist, dass es keinen Besitzer für diese Fahrzeuge gibt, also bleibt das person_id(foreign key )
Feld leer.
Nun speichert der Sheriff zwei gestohlene Fahrzeuge wie folgt in der Datenbank:
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 wird die folgenden SQL-Anweisungen ausführen:
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 |
---------------------
Diese obige Strategie zwingt uns, Nullwerte in die Spalte zu setzen, um optionale Beziehungen zu behandeln.
Typischerweise denken wir an many-to-many
-Beziehungen, wenn wir eine join table
betrachten, aber die Verwendung einer Join-Tabelle kann uns in diesem Fall helfen, diese Nullwerte zu eliminieren:
Dieser Ansatz verwendet eine Join-Tabelle, um die Assoziationen zwischen den Entitäten Branch und Company zu speichern. @JoinTable
Annotation wurde verwendet, um diese Assoziation zu machen.
In diesem Beispiel haben wir @JoinTable
auf der Fahrzeugseite (Many Side) angewendet.
Lassen Sie uns sehen, wie das Datenbankschema aussehen wird:
Der obige Code wird 3 Tabellen wie person(id,name)
, vehicle(id,name)
und vehicle_person(vehicle_id,person_id)
erzeugen. Hier wird vehicle_person
die Fremdschlüsselbeziehung zu den Entitäten „Person“ und „Fahrzeug“ enthalten.
Wenn also der Sheriff die Fahrzeugdetails speichert, muss kein Nullwert in der Fahrzeugtabelle persistiert werden, weil wir die Fremdschlüsselbeziehung in den vehicle_person
-Tabellen und nicht in der Fahrzeugtabelle aufrechterhalten.
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 führt die folgenden SQL-Anweisungen aus:
insert into vehicle (id, name) values (1, "ford"); insert into vehicle (id, name) values (2, "gmc");id |name|
-----|----|
1 |ford|
2 |benz|
-----------
Das obige Beispiel zeigt, wie wir die Einfügung von Nullwerten loswerden.
@OneToMany Bi-directional Relationship
- Die bidirektionale
@OneToMany
Assoziation erfordert auch eine@ManyToOne
Assoziation auf der Child-Seite. Obwohl das Domänenmodell zwei Seiten zur Navigation dieser Assoziation zeigt, hat die relationale Datenbank hinter den Kulissen nur einen Fremdschlüssel für diese Beziehung. - Jede bidirektionale Assoziation muss nur eine besitzende Seite haben (die Kindseite), die andere wird als die inverse (oder die
mappedBy
) Seite bezeichnet. - Wenn wir
@OneToMany
mit demmappedBy
-Attribut verwenden, dann haben wir eine bidirektionale Assoziation, was bedeutet, dass wir eine@ManyToOne
-Assoziation auf der untergeordneten Seite haben müssen, auf diemappedBy
verweist. - Das
mappedBy
-Element definiert eine bidirektionale Beziehung. Dieses Attribut ermöglicht es, die assoziierten Entitäten von beiden Seiten aus zu referenzieren.
Die beste Art, eine @OneToMany
-Assoziation abzubilden, ist, sich auf die @ManyToOne
-Seite zu verlassen, um alle Zustandsänderungen der Entitäten weiterzugeben.
Wenn wir 2 Branch(s)
persistieren, erzeugt Hibernate nur eine SQL-Anweisung für jede persistierte Branch-Entität:
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);
Wenn wir eine Branch entfernen:
Company company = entityManager.find( Company.class, 1L ); Branch branch = company.getBranches().get(0); company.removeBranches(branch);
Es wird nur eine SQL-Anweisung zum Löschen ausgeführt:
delete from Branch where id = 1
Die bidirektionale @OneToMany-Assoziation ist also der beste Weg, um eine One-to-Many-Datenbankbeziehung abzubilden, wenn wir die Sammlung auf der übergeordneten Seite der Assoziation wirklich benötigen.
@JoinColumn
Gibt eine Spalte für die Verbindung einer Entitätsassoziation oder Elementsammlung an. Der Vermerk@JoinColumn
zeigt an, dass diese Entität der Eigentümer der Beziehung ist. Das heißt, die entsprechende Tabelle hat eine Spalte mit einem Fremdschlüssel zur referenzierten Tabelle.Im obigen Beispiel hat der Eigentümer Entity Branch eine Join-Spalte mit dem Namen
company_id
, die einen Fremdschlüssel zur Nicht-Eigentümerin Company Entity hat.Wenn eine bidirektionale Assoziation gebildet wird, muss der Anwendungsentwickler sicherstellen, dass beide Seiten jederzeit synchron sind.
addBranches()
undremoveBranches()
sind Hilfsmethoden, die beide Seiten synchronisieren, wenn ein untergeordnetes Element (d.h. eine Zweigstelle) hinzugefügt oder entfernt wird.Im Gegensatz zur unidirektionalen
@OneToMany
ist die bidirektionale Assoziation viel effizienter bei der Verwaltung des Persistenzstatus der Sammlung.Jedes Entfernen eines Elements erfordert nur eine einzige Aktualisierung (bei der die Fremdschlüsselspalte auf NULL gesetzt wird), und wenn der Lebenszyklus der untergeordneten Entität an die besitzende übergeordnete Entität gebunden ist, so dass die untergeordnete Entität nicht ohne ihre übergeordnete Entität existieren kann, können wir die Assoziation mit dem Attribut „orphan-removal“ versehen, und das Aufheben der Assoziation der untergeordneten Entität löst auch eine Löschanweisung für die eigentliche untergeordnete Tabellenzeile aus.
HINWEIS:
- In dem obigen bidirektionalen Beispiel beziehen sich die Begriffe Parent und Child in Entity/OO/Model (d.h. in der Java-Klasse) auf die nicht-gewinnende/umgekehrte bzw. die besitzende Seite in SQL.
In Java ist hier das Unternehmen das Parent und die Filiale das Child. Da eine Zweigstelle ohne Elternteil nicht existieren kann.
In SQL ist die Zweigstelle die Eigentümerseite und das Unternehmen die Nicht-Eigentümerseite (invers). Da es 1 Unternehmen für N Zweige gibt, enthält jeder Zweig einen Fremdschlüssel zu dem Unternehmen, zu dem er gehört, d.h. der Zweig „besitzt“ (oder enthält buchstäblich) die Verbindung (Informationen).
@OneToMany Bi-direktionale Beziehung (Mapping mit Join-Tabelle)
Ab CASE 6
haben wir ein unidirektionales Mapping mit @JoinTable
, wenn wir also das Attribut mappedBy
zur Entität Person hinzufügen, wird die Beziehung bidirektional.
ZUSAMMENFASSUNG
Bei der oben genannten Zuordnung sind mehrere Dinge zu beachten:
- Die
@ManyToOne
-Assoziation verwendetFetchType.LAZY
, weil wir sonst auf EAGER-Abruf zurückgreifen würden, was schlecht für die Leistung ist. - Die bidirektionalen Assoziationen sollten immer auf beiden Seiten aktualisiert werden, daher sollte die Parent-Seite die
addChild
– undremoveChild
-Kombination enthalten (die Parent-Seite Company enthält zwei Utility-MethodenaddBranches
undremoveBranches
). Diese Methoden stellen sicher, dass wir immer beide Seiten der Assoziation synchronisieren, um Probleme mit der Beschädigung von Objekten oder relationalen Daten zu vermeiden. - Die untergeordnete Entität Branch implementiert die Methoden equals und hashCode. Da wir uns bei Gleichheitsprüfungen nicht auf einen natürlichen Bezeichner verlassen können, müssen wir stattdessen den Entitätsbezeichner verwenden. Allerdings müssen wir es richtig machen, damit die Gleichheit über alle Zustandsübergänge der Entität hinweg konsistent ist. Da wir uns bei removeBranches auf Gleichheit verlassen, ist es gute Praxis, equals und hashCode für die Kind-Entität in einer bidirektionalen Assoziation zu überschreiben.
- Die @OneToMany-Assoziation ist per Definition eine Eltern-Assoziation, auch wenn es eine unidirektionale oder bidirektionale ist. Nur auf der übergeordneten Seite einer Assoziation ist es sinnvoll, die Zustandsübergänge der Entität an die Kinder weiterzugeben.