Modo più efficiente per mappare una relazione @OneToMany con JPA e Hibernate

Ho scelto di aprire questo post con questa citazione perché sono un fan di Linus Torvalds.😉

Questo è il mio primo articolo. In questo coprirò tutti i casi possibili nell’associazione di entità One-to-Many/Many-to-One. I rimanenti Many-to-Many e One-to-One saranno coperti nei prossimi articoli.

Spero che questo aiuterà sicuramente tutti i neofiti che vogliono imparare jpa/hibernate, per favore leggete l’intero pezzo 😛

NOTE:

Qui ho coperto tutti i possibili casi di One-to-Many/Many-to-One mappatura. Tra tutti, l’associazione bidirezionale `@OneToMany` è il modo migliore per mappare una relazione di database uno-a-molti.

L’associazione hibernate classificata in One-to-One, One-to-Many/Many-to-One e Many-to-Many.

  • La direzione di una relazione può essere bidirezionale o unidirezionale.
  • Una relazione bidirezionale ha sia un lato proprietario che un lato inverso.
  • Una relazione unidirezionale ha solo un lato proprietario. Il lato proprietario di una relazione determina come il run time di Persistence fa gli aggiornamenti alla relazione nel database.
  • Unidirezionale è una relazione dove un lato non conosce la relazione.
  • In una relazione unidirezionale, solo un’entità ha un campo di relazione o una proprietà che si riferisce all’altra. Per esempio, Line Item avrebbe un campo di relazione che identifica Product, ma Product non avrebbe un campo di relazione o una proprietà per Line Item. In altre parole, Line Item sa di Product, ma Product non sa quali istanze di Line Item si riferiscono ad esso.

Relazioni bidirezionali:

  • La relazione bidirezionale fornisce accesso di navigazione in entrambe le direzioni, in modo da poter accedere all’altra parte senza query esplicite.
  • In una relazione bidirezionale, ogni entità ha un campo di relazione o una proprietà che si riferisce all’altra entità. Attraverso il campo o la proprietà di relazione, il codice di una classe di entità può accedere al suo oggetto correlato. Se un’entità ha un campo di relazione, si dice che l’entità “conosce” il suo oggetto correlato. Per esempio, se Order sa quali istanze di Line Item ha e se Line Item sa a quale Order appartiene, hanno una relazione bidirezionale.

Le relazioni bidirezionali devono seguire queste regole.

  • Il lato inverso di una relazione bidirezionale deve riferirsi al suo lato proprietario (entità che contiene la chiave esterna) usando l’elemento mappedBy dell’annotazione @OneToOne, @OneToMany, o @ManyToMany. L’elemento mappedBy designa la proprietà o il campo nell’entità che è il proprietario della relazione.
  • Il lato molti di @ManyToOne relazioni bidirezionali non deve definire l’elemento mappedBy. Il lato molti è sempre il lato proprietario della relazione.
  • Per le relazioni bidirezionali @OneToOne, il lato proprietario corrisponde al lato che contiene @JoinColumn cioè la chiave esterna corrispondente.
  • Per le relazioni bidirezionali @ManyToMany, ogni lato può essere il lato proprietario.

@OneToMany relationship con JPA e Hibernate

In parole povere, la mappatura one-to-many significa che una riga di una tabella è mappata su più righe di un’altra tabella.

Quando usare la mappatura uno a molti

Utilizzare la mappatura uno a molti per creare una relazione 1…N tra entità o oggetti.

Dobbiamo scrivere due entità cioè Company e Branch tali che più rami possono essere associati ad una singola azienda, ma un singolo ramo non può essere condiviso tra due o più aziende.

Hibernate one to many mapping solutions:

  1. One to many mapping with foreign key association
  2. One to many mapping with join table

Questo problema può essere risolto in due modi diversi.

Uno è avere una colonna foreign key nella branch table cioè company_id. Questa colonna farà riferimento alla chiave primaria della tabella Azienda. In questo modo non è possibile associare due rami a più aziende.

Secondo approccio è quello di avere una tabella comune diciamo Company_Branch, Questa tabella avrà due colonne cioè company_id che sarà la chiave esterna che si riferisce alla chiave primaria nella tabella Azienda e allo stesso modo branch_id che sarà la chiave esterna che si riferisce alla chiave primaria della tabella Filiale.

Se @OneToMany/@ManyToOne non ha un’associazione @ManyToOne/@OneToMany speculare rispettivamente sul lato figlio allora, l’associazione @OneToMany/@ManyToOne è unidirezionale.

@OneToMany Relazione unidirezionale

In questo approccio, una qualsiasi entità sarà responsabile di creare e mantenere la relazione. O l’azienda dichiara la relazione come uno a molti, o Branch dichiara la relazione dalla sua fine come molti a uno.

CASO 1: (Mappatura con associazione di chiave esterna)

Se usiamo solo @OneToMany allora ci saranno 3 tabelle. Come company, branch e company_branch.

NOTE:
Nell’esempio sopra ho usato termini come cascade, orphanRemoval, fetch e targetEntity, che spiegherò nel mio prossimo post.

Tabella company_branch avrà due chiavi esterne company_id e branch_id.

Ora, se persistiamo una Società e due Rami:

Hibernate eseguirà le seguenti istruzioni SQL:

  • L’associazione @OneToMany è per definizione un’associazione padre (non proprietario), anche se è unidirezionale o bidirezionale. Solo il lato genitore di un’associazione ha senso per far cascata delle sue transizioni di stato di entità ai figli.
  • Quando si persiste l’entità Company, la cascata propagherà l’operazione di persistenza anche ai figli del ramo sottostante. Quando si rimuove un Branch dalla collezione dei rami, la riga dell’associazione viene cancellata dalla tabella dei collegamenti, e l’attributo orphanRemoval innescherà anche la rimozione del ramo.

Le associazioni unidirezionali non sono molto efficienti quando si tratta di rimuovere entità figlio. In questo particolare esempio, al momento del lavaggio del contesto di persistenza, Hibernate cancella tutte le voci figlio del database e reinserisce quelle che si trovano ancora nel contesto di persistenza in-memoria.

D’altra parte, un’associazione bidirezionale @OneToMany è molto più efficiente perché l’entità figlia controlla l’associazione.

CASO 2: (Mappatura con associazione a chiave esterna)

Se usiamo solo @ManyToOne allora ci saranno 2 tabelle. Come azienda, filiale.

Questo codice sopra genererà 2 tabelle Company(company_id,name) e Branch(branch_id,name,company_company_id). Qui Branch è il lato proprietario poiché ha l’associazione di chiave esterna.

CASO 3: (Mappatura con associazione di chiave esterna)

Se usiamo entrambi @ManyToOne e @OneToMany allora si creeranno 3 tabelle Company(id,name), Branch(id,name,company_id), Company_Branch(company_id,branch_id)

Questa mappatura sottostante potrebbe sembrare bidirezionale ma non lo è. Non definisce una relazione bidirezionale, ma due relazioni uni-direzionali separate.

CASE 4: (Unidirezionale @OneToMany con @JoinColumn) (Mappatura con associazione di chiave esterna)

Se usiamo @OneToMany con @JoinColumn allora ci saranno 2 tabelle. Come azienda, filiale

Nell’entità di cui sopra dentro @JoinColumn, il nome si riferisce al nome della colonna chiave esterna che è companyId i.e company_id e rferencedColumnName indica la chiave primaria cioè id dell’entità (Company) a cui la chiave esterna companyId si riferisce.

CASO 5: (Mappatura con associazione di chiave esterna)

Se usiamo @ManyToOne con @JoinColumn allora ci saranno 2 tabelle. Come azienda, filiale.

L’annotazione @JoinColumn aiuta Hibernate a capire che c’è una colonna company_id Foreign Key nella tabella filiale che definisce questa associazione.

Branch avrà la chiave straniera company_id, quindi è il lato proprietario.

Ora, se persistiamo 1 azienda e 2 rami:

quando rimuoviamo la prima voce dalla collezione dei figli:

company.getBranches().remove(0);

Hibernate esegue due istruzioni invece di una :

  • Prima rende nullo il campo chiave esterna (per rompere l’associazione con il genitore) poi cancella il record.
Update branch set branch_id = null where where id = 1 delete from branch where id = 1 ;

CASO 6: (Mappatura con join table)

Ora, consideriamo la relazione one-to-many dove Person(id,name) viene associato a più Veichle(id,name,number) e più veicoli possono appartenere alla stessa persona.

Un giorno, mentre era sull’autostrada, lo sceriffo Carlos ha trovato alcuni veicoli abbandonati, molto probabilmente rubati. Ora lo sceriffo deve aggiornare i dettagli dei veicoli (numero, nome) nel loro database ma il problema principale è che non c’è un proprietario per quei veicoli, quindi il campo person_id(foreign key ) rimarrà nullo.

Ora lo sceriffo salva due veicoli rubati nel db come segue:

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 eseguirà le seguenti istruzioni 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 |
---------------------

Questa strategia ci costringerà a mettere valori nulli nella colonna per gestire relazioni opzionali.

In genere, pensiamo alle relazioni many-to-many quando consideriamo una join table, ma, utilizzando una tabella di unione, in questo caso, possiamo eliminare questi valori nulli:

Questo approccio utilizza una tabella di unione per memorizzare le associazioni tra le entità Branch e Company. L’annotazione @JoinTable è stata usata per fare questa associazione.

In questo esempio abbiamo applicato @JoinTable sul lato Vehicle (Many Side).

Vediamo come sarà lo schema del database:

Il codice qui sopra genererà 3 tabelle come person(id,name), vehicle(id,name) e vehicle_person(vehicle_id,person_id). Qui vehicle_person terrà la relazione di chiave esterna per entrambe le entità Persona e Veicolo.

Così quando lo sceriffo salva i dettagli del veicolo, nessun valore nullo deve essere persistito nella tabella veicolo perché stiamo mantenendo l’associazione di chiave esterna nelle tabelle vehicle_person non nella tabella veicolo.

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 eseguirà le seguenti istruzioni SQL:

insert into vehicle (id, name) values (1, "ford"); insert into vehicle (id, name) values (2, "gmc");id |name|
-----|----|
1 |ford|
2 |benz|
-----------

Questo esempio mostra come ci siamo sbarazzati dell’inserimento dei valori nulli.

@OneToMany Relazione bidirezionale

  • L’associazione bidirezionale @OneToMany richiede anche un’associazione @ManyToOne sul lato figlio. Anche se il modello di dominio espone due lati per navigare in questa associazione, dietro le quinte, il database relazionale ha solo una chiave esterna per questa relazione.
  • Ogni associazione bidirezionale deve avere un solo lato proprietario (il lato figlio), l’altro è indicato come il lato inverso (o ilmappedBy).
  • Se usiamo il @OneToMany con l’attributo mappedBy impostato, allora abbiamo un’associazione bidirezionale, il che significa che dobbiamo avere un’associazione @ManyToOne sul lato figlio a cui il mappedBy fa riferimento.
  • L’elemento mappedBy definisce una relazione bidirezionale. Questo attributo permette di referenziare le entità associate da entrambi i lati.

Il modo migliore per mappare un’associazione @OneToMany è quello di affidarsi al lato @ManyToOne per propagare tutti i cambiamenti di stato delle entità.

Se persistiamo 2 Branch(s)

Hibernate genera una sola istruzione SQL per ogni entità Branch persistita:

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);

Se rimuoviamo un Branch:

Company company = entityManager.find( Company.class, 1L ); Branch branch = company.getBranches().get(0); company.removeBranches(branch);

C’è solo un’istruzione SQL di cancellazione che viene eseguita:

delete from Branch where id = 1

Quindi, l’associazione bidirezionale @OneToMany è il modo migliore per mappare una relazione di database uno-a-molti quando abbiamo davvero bisogno della collezione sul lato padre dell’associazione.

@JoinColumn Specifica una colonna per unire un’associazione di entità o una collezione di elementi. L’annotazione @JoinColumn indica che questa entità è il proprietario della relazione. Cioè la tabella corrispondente ha una colonna con una chiave esterna alla tabella di riferimento.

Nell’esempio precedente, il ramo Entità proprietario, ha una colonna Join chiamata company_id che ha una chiave esterna all’entità Azienda non proprietaria.

Ogni volta che si forma un’associazione bidirezionale, lo sviluppatore dell’applicazione deve assicurarsi che entrambi i lati siano sempre in sincronia. addBranches() e removeBranches() sono metodi di utilità che sincronizzano entrambe le estremità ogni volta che un elemento figlio (cioè Branch) viene aggiunto o rimosso.

A differenza dell’unidirezionale @OneToMany, l’associazione bidirezionale è molto più efficiente nella gestione dello stato di persistenza della collezione.

Ogni rimozione di elemento richiede solo un singolo aggiornamento (in cui la colonna della chiave esterna è impostata su NULL) e se il ciclo di vita dell’entità figlia è legato al suo genitore proprietario in modo che il figlio non possa esistere senza il suo genitore, allora possiamo annotare l’associazione con l’attributo orphan-removal e la dissociazione del figlio attiverà anche una dichiarazione di cancellazione sulla riga effettiva della tabella figlia.

NOTA:

  • Nell’esempio bidirezionale di cui sopra il termine Genitore e Figlio in Entità/OO/Modello (cioè nella classe java) si riferisce rispettivamente al lato non vincente/inverso e al lato proprietario in SQL.

Nel punto di vista java la Società è il Genitore e il ramo è il figlio. Poiché un ramo non può esistere senza genitore.

Nel punto di vista SQL Branch è il lato proprietario e Company è il lato non proprietario (inverso). Dato che c’è 1 azienda per N rami, ogni ramo contiene una chiave esterna per l’azienda a cui appartiene. Questo significa che il ramo “possiede” (o letteralmente contiene) la connessione (informazione). Questo è esattamente l’opposto del mondo OO/modello.

@OneToMany Bi-directional Relationship(Mapping con join table)

Da CASE 6 abbiamo un mapping unidirezionale con @JoinTable, quindi se aggiungiamo mappedBy attributo all’entità Person allora la relazione diventerà bidirezionale.

SOMMARIO

Ci sono diverse cose da notare sulla suddetta mappatura:

  • L’associazione @ManyToOne usa FetchType.LAZY perché, altrimenti, ricadremmo nel fetching EAGER che non è buono per le prestazioni.
  • Le associazioni bidirezionali dovrebbero essere sempre aggiornate su entrambi i lati, quindi il lato Parent dovrebbe contenere la combo addChild e removeChild (Parent side Company contiene due metodi di utilità addBranches e removeBranches). Questi metodi ci assicurano di sincronizzare sempre entrambi i lati dell’associazione, per evitare problemi di corruzione di oggetti o dati relazionali.
  • L’entità figlia, Branch, implementa i metodi equals e hashCode. Poiché non possiamo fare affidamento su un identificatore naturale per i controlli di uguaglianza, dobbiamo invece usare l’identificatore di entità. Tuttavia, dobbiamo farlo correttamente in modo che l’uguaglianza sia coerente in tutte le transizioni di stato delle entità. Poiché ci affidiamo all’uguaglianza per il removeBranches, è buona pratica sovrascrivere equals e hashCode per l’entità figlia in un’associazione bidirezionale.
  • L’associazione @OneToMany è per definizione un’associazione genitore, anche se è unidirezionale o bidirezionale. Solo il lato genitore di un’associazione ha senso per far cascata delle sue transizioni di stato di entità ai figli.

Lascia un commento

Il tuo indirizzo email non sarà pubblicato.