Best Practices für Viele-zu-Viele-Assoziationen mit Hibernate und JPA

Viele-zu-Viele-Assoziationen sind eine der am häufigsten verwendeten Assoziationen mit JPA und Hibernate. Sie können viele Beispiele für sie in der realen Welt finden und sie mit JPA und Hibernate als uni- oder bidirektionale Zuordnung in Ihrem Domänenmodell zuordnen.

Aber Sie wissen wahrscheinlich auch, dass diese Zuordnungen mehrere Fallstricke bieten. In diesem Artikel zeige ich Ihnen 5 Best Practices, die Ihnen helfen, diese Fallstricke zu vermeiden und effiziente Mappings zu implementieren. Du wirst lernen:

  1. Der effizienteste Datentyp für Ihre Assoziation
  2. Warum Sie Hilfsmethoden benötigen, um Ihre Assoziation zu verwalten
  3. Der richtige FetchType für eine effiziente Zuordnung
  4. Wann und wie Sie abfragespezifisches Abrufen verwenden
  5. Der KaskadeNtyp, den Sie um jeden Preis vermeiden sollten

Ich werde nicht auf die Details einer grundlegenden Viele-zu-Viele-Zuordnung eingehen. Wenn Sie nicht genau wissen, wie Sie eine solche Zuordnung erstellen sollen, lesen Sie bitte den Abschnitt Viele-zu-Viele in meinem Handbuch zur Zuordnung von Assoziationen.

Der effizienteste Datentyp für Ihre Assoziation

YouTube video

Die meisten Entwickler machen sich nicht viele Gedanken über den Datentyp einer to-Many-Assoziation. Sie wählen nur ein Java.util.Liste, weil es einfach ist und keine Überprüfungen durchführt, um Duplikate zu vermeiden.

Das ist in Ordnung, wenn Sie eine grundlegende Java-Klasse implementieren oder eine Eins-zu-Viele / Viele-zu-Eins-Assoziation modellieren. Sie sollten jedoch niemals eine Liste verwenden, wenn Sie eine Viele-zu-Viele-Zuordnung modellieren.

@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>();...}

Hibernate behandelt Entfernungsoperationen für Viele-zu-Viele-Beziehungen, die einem Java zugeordnet sind.util.Liste sehr ineffizient.

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

Es entfernt zuerst alle Datensätze aus der Zuordnungstabelle, bevor es alle verbleibenden einfügt.

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 (?, ?)

Sie sollten stattdessen eine Viele-zu-Viele-Assoziation als Java modellieren.util.Setzen.

@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 behandelt dann Entfernungsoperationen auf der Assoziation viel besser. Es werden jetzt nur die erwarteten Datensätze aus der Zuordnung entfernt und die anderen bleiben unberührt.

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=?

Warum Sie Hilfsmethoden benötigen, um Ihre Zuordnung zu verwalten

Bidirektionale Zuordnungen werden an beiden Enden der Beziehungen einem Entitätsattribut zugeordnet. Im vorherigen Beispiel haben Sie also ein authors-Attribut für die Book-Entität und ein books-Attribut für die Author-Entität. Das macht die Implementierung einer JPQL- oder CriteriaQuery sehr komfortabel, da Sie diese Attribute verwenden können, um eine JOIN-Klausel zu definieren.

Aber das Hinzufügen oder Entfernen einer Assoziation wird komplizierter. Sie müssen die Änderung immer an beiden Enden der Zuordnung durchführen. Wenn Sie beispielsweise ein Buch zum Autor hinzufügen möchten, müssen Sie es dem Attribut books der Entität Author hinzufügen, und Sie müssen dem Autor auch das Attribut authors der Entität Book hinzufügen. Andernfalls enthält Ihr aktueller Persistenzkontext inkonsistente Daten, die Sie bis zum Ende Ihrer aktuellen Transaktion verwenden.

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

Hilfsmethoden für Ihre Autor- und Buchentitäten erleichtern das Aktualisieren und Entfernen erheblich. Innerhalb dieser Methoden führen Sie die erforderlichen Vorgänge für beide Entitäten aus.

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

Der richtige FetchType für ein effizientes Mapping

YouTube Video

Dies ist ein schnelles. Sie sollten immer FetchType verwenden.FAUL für Ihre Viele-zu-Viele-Assoziationen. Es weist Ihren Persistenzanbieter an, die zugeordneten Entitäten erst dann aus der Datenbank abzurufen, wenn Sie sie verwenden. Dies ist normalerweise der Fall, wenn Sie die Getter-Methode zum ersten Mal aufrufen.

Zum Glück ist das der Standard für alle zu vielen Assoziationen. Also, bitte stellen Sie sicher, dass Sie es nicht ändern.

Und wenn Sie mehr über die verschiedenen FetchTypen von JPA erfahren möchten, werfen Sie bitte einen Blick auf meine Einführung in JPA FetchTypes .

Wann und wie Sie abfragespezifisches Abrufen verwenden

Wenn Sie FetchType verwenden.DAHER müssen Sie über das abfragespezifische Abrufen Bescheid wissen. Andernfalls ist Ihre Anwendung sehr langsam, da Sie viele n + 1 ausgewählte Probleme erstellt haben.

YouTube video

Wenn Sie eine Entität laden und abfragespezifisches Abrufen verwenden, teilen Sie Hibernate mit, welche zugeordneten Zuordnungen für jede abgerufene Entität initialisiert werden sollen. Anschließend wird die SELECT-Klausel Ihrer Abfrage so erweitert, dass sie die von diesen anderen Entitäten zugeordneten Spalten enthält und die Zuordnungen initialisiert. Da die Zuordnungen bereits initialisiert sind, muss Hibernate beim ersten Zugriff auf die Getter-Methode keine zusätzliche Abfrage ausführen.

Sie können abfragespezifisches Abrufen auf verschiedene Arten implementieren. Die einfachste ist eine JOIN FETCH Klausel, die ich Ihnen hier zeigen werde. Sie können aber auch einen @NamedEntityGraph oder einen EntityGraph , den ich in früheren Artikeln erklärt habe.

Die Definition einer JOIN FETCH-Klausel ist fast identisch mit einer einfachen JOIN-Klausel in einer JPQL-Abfrage. Sie müssen nur das Schlüsselwort FETCH hinzufügen.

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

Obwohl eine JOIN- und eine JOIN FETCH-Klausel sehr ähnlich aussehen, hat die JOIN FETCH-Klausel einen viel größeren Einfluss auf die generierte SQL-Abfrage. Es wird nicht nur in einen SQL-JOIN übersetzt, wie dies bei einer JPQL JOIN-Klausel der Fall ist, sondern zwingt Ihren Persistenzanbieter auch, die SELECT-Klausel um alle Spalten zu erweitern, die von der zugeordneten Entität zugeordnet werden.

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

Der KaskadeNtyp, den Sie unbedingt vermeiden sollten

Wenn Sie die Kaskadierung für eine Zuordnung aktivieren, wendet Ihr Persistenzanbieter die Vorgänge, die Sie für die Entität ausführen, auf alle zugeordneten Entitäten an. Ob dies für alle Vorgänge oder nur für einige ausgewählte Vorgänge der Fall ist, hängt vom konfigurierten Kaskadentyp ab.

YouTube-Video

Das klingt vielleicht nach einer erstaunlichen Idee, die die Implementierung Ihrer Geschäftslogik erheblich vereinfacht. Und das ist nicht ganz falsch.

Vermeiden Sie jedoch die Kaskadentypen REMOVE und ALL , einschließlich REMOVE , für Viele-zu-Viele-Assoziationen. Im besten Fall führt dies nur zu Leistungsproblemen, im schlimmsten Fall werden jedoch möglicherweise auch mehr Datensätze entfernt, als Sie beabsichtigt haben.

Ich habe beide Fallstricke und ihre Lösung in einem früheren Artikel ausführlich erklärt. Wenn Sie es einfach halten möchten, lösen Sie die erforderlichen Informationen programmgesteuert für die zugehörigen Entitäten aus. Dies erfordert möglicherweise einige weitere Codezeilen, vermeidet jedoch unerwartete Nebenwirkungen.

Fazit

Sie können viele Beispiele für Viele-zu-Viele-Assoziationen in der realen Welt finden, und Sie können sie leicht mit JPA und Hibernate abbilden. Leider verbergen diese einfachen Zuordnungen einige Fallstricke, die Sie vermeiden können, indem Sie diese 5 Best Practices befolgen:

  1. Modellieren Sie Zuordnungen als Java.util.Setzen.
  2. Stellen Sie Hilfsmethoden zum Hinzufügen oder Entfernen einer Entität zu einer Assoziation bereit.
  3. Verwenden Sie immer FetchType.FAUL, was der Standard ist, um Leistungsprobleme zu vermeiden.
  4. Wenden Sie abfragespezifisches Abrufen an, um n + 1 Select-Probleme zu vermeiden.
  5. Verwenden Sie nicht die CascadeTypes REMOVE und ALL .

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.