Best practice per le associazioni molti-a-molti con Hibernate e JPA

Le associazioni molti-a-molti sono una delle associazioni più comunemente usate con JPA e Hibernate. Puoi trovare molti esempi per loro nel mondo reale e puoi mapparli con JPA e Hibernate come associazione uni o bidirezionale nel tuo modello di dominio.

Ma probabilmente sai anche che queste mappature forniscono diverse insidie. In questo articolo, vi mostrerò 5 migliori pratiche che vi aiuteranno a evitare queste insidie e di implementare mappature efficienti. Imparerai:

  1. Il più efficace tipo di dati per la vostra associazione
  2. Perché avete bisogno di metodi di utilità per gestire la vostra associazione
  3. Il diritto di FetchType per un’efficace mappatura
  4. Quando e come utilizzare la query specifiche per il recupero
  5. Il CascadeType si dovrebbe evitare a tutti i costi

Non voglio approfondire i dettagli di base di tipo molti-a-molti mappatura. Se non sei esattamente sicuro di come creare tale mappatura, dai un’occhiata alla sezione molti-a-molti nella mia guida alla mappatura delle associazioni.

Il tipo di dati più efficiente per la tua associazione

Video di YouTube

La maggior parte degli sviluppatori non spende molti pensieri sul tipo di dati di un’associazione a molti. Scelgono solo un java.util.Elenco perché è semplice e non esegue alcun controllo per evitare duplicati.

Va bene, se si implementa una classe Java di base o se si modella un’associazione uno-a-molti / Molti-a-Uno. Ma non dovresti mai usare una lista se modelli un’associazione Molti-a-molti.

@Entitypublic class Book {// DON'T DO THIS!!!@ManyToMany@JoinTable(name = "book_author", joinColumns = { @JoinColumn(name = "fk_book") }, inverseJoinColumns = { @JoinColumn(name = "fk_author") })private List<Author> authors = new ArrayList<Author>();...}

Gli handle di ibernazione rimuovono le operazioni sulle relazioni Molti-a-molti mappate su un java.util.Elenco molto inefficiente.

em = emf.createEntityManager();em.getTransaction().begin();// Get Book entity with 2 Authorsb = em.find(Book.class, 1L);// Remove one of the Authorb.getAuthors().remove(a);em.getTransaction().commit();em.close();

Prima rimuove tutti i record dalla tabella di associazione prima di inserirne tutti i rimanenti.

09:54:28,876 DEBUG - update Book set title=?, version=? where id=? and version=?09:54:28,878 DEBUG - delete from book_author where fk_book=?09:54:28,882 DEBUG - insert into book_author (fk_book, fk_author) values (?, ?)

Dovresti invece modellare un’associazione molti-a-molti come java.util.Impostare.

@Entitypublic class Book {@ManyToMany@JoinTable(name = "book_author", joinColumns = { @JoinColumn(name = "fk_book") }, inverseJoinColumns = { @JoinColumn(name = "fk_author") })private Set<Author> authors = new HashSet<Author>();...}

Hibernate quindi gestisce molto meglio le operazioni di rimozione sull’associazione. Ora rimuove solo i record attesi dall’associazione e mantiene intatti gli altri.

10:00:37,709 DEBUG - update Book set title=?, version=? where id=? and version=?10:00:37,711 DEBUG - delete from book_author where fk_book=? and fk_author=?

Perché sono necessari metodi di utilità per gestire l’associazione

Le associazioni bidirezionali sono mappate su un attributo di entità su entrambe le estremità delle relazioni. Quindi, nell’esempio precedente, hai un attributo authors sull’entità Book e un attributo books sull’entità Author. Ciò rende l’implementazione di JPQL o CriteriaQuery molto confortevole perché è possibile utilizzare questi attributi per definire una clausola JOIN.

Ma aggiungere o rimuovere un’associazione diventa più complicato. È sempre necessario eseguire la modifica su entrambe le estremità dell’associazione. Ad esempio, se si desidera aggiungere un libro all’autore, è necessario aggiungerlo all’attributo libri dell’entità Autore e aggiungere l’Autore all’attributo autori sull’entità Libro. In caso contrario, il contesto di persistenza corrente contiene dati incoerenti che verranno utilizzati fino alla fine della transazione corrente.

Book b = new Book();b.setTitle("Hibernate Tips - More than 70 solutions to common Hibernate problems");em.persist(b);Author a = em.find(Author.class, 1L);a.getBooks().add(b);b.getAuthors().add(a);

I metodi di utilità sulle entità Autore e Libro semplificano notevolmente l’aggiornamento e la rimozione. All’interno di questi metodi, è possibile eseguire le operazioni richieste su entrambe le entità.

@Entitypublic class Author {@ManyToMany(mappedBy = "authors")private Set<Book> books = new HashSet<Book>();...public void addBook(Book book) {this.books.add(book);book.getAuthors().add(this);}public void removeBook(Book book) {this.books.remove(book);book.getAuthors().remove(this);}}

Il FetchType giusto per una mappatura efficiente

Video di YouTube

Questo è veloce. Dovresti sempre usare FetchType.PIGRO per le tue molte-a-molte associazioni. Indica al provider di persistenza di non recuperare le entità associate dal database finché non le utilizzi. Questo è di solito il caso quando si chiama il suo metodo getter per la prima volta.

Fortunatamente, questo è il valore predefinito per tutte le associazioni to-many. Quindi, per favore assicurati di non cambiarlo.

E se vuoi saperne di più sui diversi tipi di fetch di JPA, dai un’occhiata alla mia introduzione a JPA FetchTypes.

Quando e come utilizzare il recupero specifico della query

Se si utilizza FetchType.PIGRO, è necessario conoscere il recupero specifico della query. Altrimenti, la tua applicazione sarà molto lenta perché hai creato molti problemi di selezione n+1.

YouTube video

Quando carichi un’entità e usi il recupero specifico della query, dici a Hibernate quali associazioni mappate deve inizializzare per ogni entità recuperata. Quindi estende la clausola SELECT della query in modo che includa le colonne mappate da queste altre entità e inizializzi le associazioni. E poiché le associazioni sono già inizializzate, Hibernate non ha bisogno di eseguire una query aggiuntiva quando si accede al suo metodo getter per la prima volta.

È possibile implementare il recupero specifico della query in diversi modi. Il più semplice è una clausola JOIN FETCH, che ti mostrerò qui. Ma puoi anche usare @ NamedEntityGraph o EntityGraph, che ho spiegato negli articoli precedenti.

La definizione di una clausola JOIN FETCH è quasi identica a una semplice clausola JOIN in una query JPQL. Hai solo bisogno di aggiungere la parola chiave FETCH.

Author a = em.createQuery("SELECT a FROM Author a JOIN FETCH a.books WHERE a.id = 1", Author.class).getSingleResult();

Anche così, una clausola JOIN e una JOIN FETCH sembrano molto simili, la clausola JOIN FETCH ha un effetto molto più grande sulla query SQL generata. Non solo viene tradotto in un JOIN SQL, come nel caso di una clausola JPQL JOIN, ma costringe anche il provider di persistenza a estendere la clausola SELECT di tutte le colonne mappate dall’entità associata.

16:21:03,046 DEBUG SQL:94 - select author0_.id as id1_0_0_, book2_.id as id1_1_1_, author0_.firstName as firstNam2_0_0_, author0_.lastName as lastName3_0_0_, author0_.version as version4_0_0_, book2_.format as format2_1_1_, book2_.publishingDate as publishi3_1_1_, book2_.title as title4_1_1_, book2_.version as version5_1_1_, books1_.author_id as author_i2_2_0__, books1_.book_id as book_id1_2_0__ from Author author0_ inner join book_author books1_ on author0_.id=books1_.author_id inner join Book book2_ on books1_.book_id=book2_.id where author0_.id=1

Il CascadeType da evitare a tutti i costi

Se si attiva il cascading su un’associazione, il provider di persistenza applica le operazioni eseguite sull’entità a tutte le entità associate. Se lo fa per tutte le operazioni o solo per alcune selezionate dipende dal CascadeType configurato.

Video di YouTube

Potrebbe sembrare un’idea straordinaria che rende molto più semplice l’implementazione della logica aziendale. E questo non è del tutto sbagliato.

Ma si prega di evitare CascadeTypes REMOVE e ALL, che include REMOVE, per molte-a-molte associazioni. Nel migliore dei casi, crea solo problemi di prestazioni, ma nel peggiore dei casi, potrebbe anche rimuovere più record di quanto previsto.

Ho spiegato entrambe le insidie e la loro soluzione in grandi dettagli in un precedente articolo. Oppure, se si desidera mantenerlo semplice, attivare le informazioni richieste a livello di codice sulle entità associate. Ciò potrebbe richiedere alcune righe di codice in più, ma evita effetti collaterali imprevisti.

Conclusione

Puoi trovare molti esempi per molte associazioni nel mondo reale e puoi facilmente mapparle con JPA e Hibernate. Sfortunatamente, queste semplici mappature nascondono alcune insidie che puoi evitare seguendo queste 5 best practice:

  1. Associazioni di modelli come java.util.Impostare.
  2. Fornire metodi di utilità per aggiungere o rimuovere un’entità da un’associazione.
  3. Usa sempre FetchType.PIGRO, che è il valore predefinito, per evitare problemi di prestazioni.
  4. Applica il recupero specifico della query per evitare problemi di selezione n+1.
  5. Non usare CascadeTypes REMOVE e ALL.

Lascia un commento

Il tuo indirizzo email non sarà pubblicato.