JPA: Bidirektionale OneToMany/ManyToOne Beziehungen


Dieser Beitrag soll in erster Linie für mich als Gedächtnisstütze dienen. Als Nachwirkung der Schulung beschäftige ich mich nicht nur mit Enterprise Java Beans (EJB) sondern auch mit der Java Persistence API (JPA), dem "neuen" Persistenzstandard, der im gleichen Prozess wie EJB entworfen wurde. Im Entwicklerforum von java.net findet sich ein passender Satz zu dem Thema, den jemand als Antwort bekam, der sich darüber beklagt, dass JPA so unvorhersehbar arbeiten würde:

I doubt that it's a matter of it being "unpredictable" so much as it is getting over the learning curve.

Genau in dieser Lernkurve stecke ich auch derzeit noch und erlerne JPA gerade im Eigenstudium. Für erfahrene Entwickler ist dieser Beitrag daher nicht gedacht.

Das Problem

Es existieren zwei Entitäten, Benutzer und Gruppe. Jeder Benutzer kann einer Gruppe angehören, folglich können zu einer Gruppe mehrere Benutzer gehören. Beide Entitäten sind wie folgt definiert:

JAVA:
  1. public class Benutzer
  2. {
  3.     private String _name;
  4.     private String _vorname;
  5.     private String _benutzername;
  6.     private String _email;
  7.     private int _id;
  8.     private Gruppe _gruppe;
  9.  
  10.     /*
  11.      * Es folgen die ganze Getter und Setter fuer die
  12.      * obigen Eigenschaften.
  13.      * [...]
  14.      */
  15.    
  16.     @ManyToOne
  17.     public Gruppe getGruppe()
  18.     {
  19.         return _gruppe;
  20.     }
  21.  
  22.     public void setGruppe(final Gruppe gruppe)
  23.     {
  24.         _gruppe = gruppe;
  25.     }
  26. }

JAVA:
  1. public class Gruppe
  2. {
  3.     private int _id;
  4.     private String _name;
  5.     private String _beschreibung;
  6.     private Set<Benutzer> _benutzer = new HashSet<Benutzer>();
  7.            
  8.     /*
  9.      * Es folgen die ganze Getter und Setter fuer die
  10.      * obigen Eigenschaften.
  11.      * [...]
  12.      */            
  13.            
  14.     @OneToMany(mappedBy = "gruppe")
  15.     public Set<Benutzer> getBenutzer()
  16.     {
  17.         return _benutzer;
  18.     }
  19.  
  20.     public void setBenutzer(final Set<Benutzer> benutzer)
  21.     {
  22.         _benutzer = benutzer;
  23.     }
  24. }

Ein kleines Programm geht jetzt her und erzeugt drei Benutzer und zwei Gruppen. Zwei der Benutzer werden der ersten Gruppe zugewiesen, der dritte der anderen Gruppe.

JAVA:
  1. EntityManager em = createEntityManager();
  2.  
  3.         final Gruppe g1 = createGruppe("Demokraten", "Die Partei der Demokraten");
  4.         final Gruppe g2 = createGruppe("Republikaner", "Die Partei der Republikaner");
  5.  
  6.         em.persist(g1);
  7.         em.persist(g2);
  8.        
  9.         final Benutzer b1 = createBenutzer("Obama", "Barack", "bobama", "obama@senate.gov");
  10.         final Benutzer b2 = createBenutzer("McCain", "John", "jmccain", "mccain@senate.gov");
  11.         final Benutzer b3 = createBenutzer("Clinton", "Hillary", "hclinton", "clinton@senate.gov");
  12.  
  13.         b1.setGruppe(g1);
  14.         b2.setGruppe(g2);
  15.         b3.setGruppe(g1);
  16.  
  17.         em.persist(b1);
  18.         em.persist(b2);
  19.         em.persist(b3);

Danach geht das Programm her und lädt die Datensätze wieder aus der Datenbank und gibt sie und ihre Beziehungen aus.

JAVA:
  1. final List result = em.createQuery("SELECT b FROM Benutzer b ORDER BY b.name").getResultList();
  2.        
  3.         for (final Object b : result)
  4.         {
  5.             if (b instanceof Benutzer)
  6.             {
  7.                 final Benutzer benutzer = (Benutzer) b;
  8.                 LOG.info(benutzer);
  9.                 LOG.info("\t+--> " + benutzer.getGruppe());
  10.             }
  11.         }
  12.  
  13.         final List result2 = em.createQuery("SELECT g FROM Gruppe g").getResultList();
  14.        
  15.         for (final Object o : result2)
  16.         {
  17.             if (o instanceof Gruppe)
  18.             {
  19.                 final Gruppe g = (Gruppe) o;
  20.                 LOG.info(g);
  21.                 LOG.info("Benutzerzahl: " + g.getBenutzer().size());
  22.  
  23.                 for(final Benutzer b : g.getBenutzer())
  24.                 {
  25.                     LOG.info("\t+--> " + b);
  26.                 }
  27.             }
  28.         }

Es entsteht folgende Ausgabe:

CODE:
  1. JPAClient.showDemo()@107: Benutzer [2902]: Clinton, Hillary (hclinton, clinton@senate.gov)
  2. JPAClient.showDemo()@108:   +--> Gruppe [2800]: Demokraten (Die Partei der Demokraten)
  3. JPAClient.showDemo()@107: Benutzer [2901]: McCain, John (jmccain, mccain@senate.gov)
  4. JPAClient.showDemo()@108:   +--> Gruppe [2801]: Republikaner (Die Partei der Republikaner)
  5. JPAClient.showDemo()@107: Benutzer [2900]: Obama, Barack (bobama, obama@senate.gov)
  6. JPAClient.showDemo()@108:   +--> Gruppe [2800]: Demokraten (Die Partei der Demokraten)
  7. JPAClient.showDemo()@120: Gruppe [2800]: Demokraten (Die Partei der Demokraten)
  8. JPAClient.showDemo()@121: Benutzerzahl: 0
  9. JPAClient.showDemo()@120: Gruppe [2801]: Republikaner (Die Partei der Republikaner)
  10. JPAClient.showDemo()@121: Benutzerzahl: 0

Das Problem ist offensichtlich: Während jeder Benutzer eine Referenz auf das ihm zugeordnete Gruppenobjekt hält, kennt keine der Gruppen die Benutzer, die ihr zugeordnet sind.

Die Lösung

Bidirektionale Beziehungen sollten eigentlich anders funktionieren. Das oben geschilderte Problem ist das, was auch vielen anderen wegen seiner Struktur Probleme bereitet. Die Beziehung funktioniert beidseitig und solange der gleiche EntityManager läuft, können Objekte vom JPA-Provider zwischengespeichert werden. Daher reicht ein einseitiges Zuweisen nicht aus, vielmehr muss in diesem Fall auch der Gruppe der Benutzer zugewiesen werden. Erst dann sind die zwischengespeicherten Objekte vollständig.

JAVA:
  1. EntityManager em = createEntityManager();
  2.  
  3.         final Gruppe g1 = createGruppe("Demokraten", "Die Partei der Demokraten");
  4.         final Gruppe g2 = createGruppe("Republikaner", "Die Partei der Republikaner");
  5.  
  6.         final Benutzer b1 = createBenutzer("Obama", "Barack", "bobama", "obama@senate.gov");
  7.         final Benutzer b2 = createBenutzer("McCain", "John", "jmccain", "mccain@senate.gov");
  8.         final Benutzer b3 = createBenutzer("Clinton", "Hillary", "hclinton", "clinton@senate.gov");
  9.  
  10.         b1.setGruppe(g1);
  11.         b2.setGruppe(g2);
  12.         b3.setGruppe(g1);
  13.        
  14.         g1.getBenutzer().add(b1);
  15.         g2.getBenutzer().add(b2);
  16.         g1.getBenutzer().add(b3)
  17.  
  18.         em.persist(b1);
  19.         em.persist(b2);
  20.         em.persist(b3);
  21.        
  22.         em.persist(g1);
  23.         em.persist(g2);

Folgende Ausgabe zeigt das Ergebnis der Ã?nderung:

CODE:
  1. JPAClient.showDemo()@107: Benutzer [2952]: Clinton, Hillary (hclinton, clinton@senate.gov)
  2. JPAClient.showDemo()@108:   +--> Gruppe [2850]: Demokraten (Die Partei der Demokraten)
  3. JPAClient.showDemo()@107: Benutzer [2951]: McCain, John (jmccain, mccain@senate.gov)
  4. JPAClient.showDemo()@108:   +--> Gruppe [2851]: Republikaner (Die Partei der Republikaner)
  5. JPAClient.showDemo()@107: Benutzer [2950]: Obama, Barack (bobama, obama@senate.gov)
  6. JPAClient.showDemo()@108:   +--> Gruppe [2850]: Demokraten (Die Partei der Demokraten)
  7. JPAClient.showDemo()@112: Lade Gruppen
  8. JPAClient.showDemo()@120: Gruppe [2850]: Demokraten (Die Partei der Demokraten)
  9. JPAClient.showDemo()@121: Benutzerzahl: 2
  10. JPAClient.showDemo()@126:   +--> Benutzer [2952]: Clinton, Hillary (hclinton, clinton@senate.gov)
  11. JPAClient.showDemo()@126:   +--> Benutzer [2950]: Obama, Barack (bobama, obama@senate.gov)
  12. JPAClient.showDemo()@120: Gruppe [2851]: Republikaner (Die Partei der Republikaner)
  13. JPAClient.showDemo()@121: Benutzerzahl: 1
  14. JPAClient.showDemo()@126:   +--> Benutzer [2951]: McCain, John (jmccain, mccain@senate.gov)

Diese Art der Zuweisung im Code eines Client-Programms ist natürlich etwas unschön, sinnvoller wäre es, diese Zuweisung gleich im entsprechenden Setter der Benutzerklasse zu machen.

JAVA:
  1. public void setGruppe(final Gruppe gruppe)
  2.     {
  3.         _gruppe = gruppe;
  4.         if (null != _gruppe.getBenutzer() && !_gruppe.getBenutzer().contains(this))
  5.         {
  6.             _gruppe.getBenutzer().add(this);
  7.         }
  8.     }

Das Problem erübrigt sich auch, wenn man einen anderen EntityManager verwendet als den, der die Objekte persistiert hat.

Tags:

  1. #1 von Steffen am 14. April 2010 - 10:50

    Auszug aus http://en.wikibooks.org/wiki/Java_Persistence/OneToMany:

    Some expect the JPA provider to have magic that automatically maintains relationships. This was actually part of the EJB CMP 2 specification. However the issue is if the objects are detached or serialized to another VM, or new objects are related before being managed, or the object model is used outside the scope of JPA, then the magic is gone, and the application is left figuring things out, so in general it may be better to add the code to the object model. However some JPA providers do have support for automatically maintaining relationships.

  2. #2 von Hendrik Busch am 28. April 2010 - 21:28

    Wenn wir mal ehrlich sind, dann funktionieren die meisten JPA-Provider mit einer Menge, hm, nennen wir es mal Trickserei. Als Beispiele nenne ich nur mal die Selektion von spezifischen Entitäten über eine Abfrage an die gemeinsame Entität von der sie abstammen oder das ausgiebigste Überwachen von Wertzuweisungen und damit speicherungswürdigen Änderungen via dynamischen Proxies.
    Bei dem ganzen Aufwand, der in die Infrastruktur einer JPA-Implementierung hineinprogrammiert wurde, kann einem der explizite Ausschluss des Beziehungsmanagements schon als eher willkürlich erscheinen. Vor allem für Neulinge ist die Selektion, was JPA intern regelt und was der Entwickler selber leisten muss, nicht durchsichtig und auch nicht logisch. Und vermutlich hat auch nicht jeder die Spezifikation von vorne bis hinten gelesen.

(wird nicht veröffentlicht)