Wenn der Wunsch Vater der Dokumentation ist

Der Beitrag könnte Spuren von Emotionen beinhalten. Gerne wird Open-Source-Software mangelnde Dokumentation nachgesagt. Diese Kritik mag manchmal gerechtfertigt sein, auf lange Sicht sind schlecht dokumentierte Frameworks aber einer gewissen negativen Selektion unterworfen – um es vornehm auszudrücken.

Hibernate – das Mapping-Framework von JBoss/Red Hat – besitzt traditionell eine sehr umfangreiche Dokumentation. Diese ist sicherlich ein wesentlicher Grund für die Beliebtheit des Frameworks. Ein Reibungspunkt kann trotzdem entstehen, wenn die Dokumentation Features andeudet, die nicht vorhanden oder fehlerhaft implementiert sind (z.B. HHH-5732). Was mich in den Wahnsinn treibt, ist @NaturalId.

Kurz zur Einordnung: JPA trifft eine Unterscheidung zwischen find-by-id und find-by-query. Der erste Fall ist sehr effizient implementiert. Eine solche Abfrage kommt – falls die Entität bereits geladen ist – ohne weitere Datenbank-Abfrage aus. Dem zweiten Fall fehlt eine solche Optimierung. Falls man keine proprietäre Erweiterung nutzt, führt eine JPQL-Abfrage immer zu einer Datenbank-Abfrage. Eine Optimierungsmöglichkeit besteht, wenn neben dem künstlichen Schlüssel ein alternativer, fachlicher Schlüssel existiert. Würde man für den fachlichen Schlüssel zusätzlich einen In-Memory-Index erstellen, kämen find-by-alternative-key-Abfragen auch ohne Datenbank-Zugriff aus – sofern die Entität bereits bekannt ist. Dieses häufig anzutreffende Muster kann in Hibernate per @NaturalId deklariert werden ([1][2][3]).

Als ich das Feature @NaturalId vor 9 Monaten das erste Mal anschaute, stieß ich direkt auf eine NullPointerException:

java.lang.NullPointerException
  at java.util.concurrent.ConcurrentHashMap.hash(ConcurrentHashMap.java:332)
  at java.util.concurrent.ConcurrentHashMap.get(ConcurrentHashMap.java:987)
  at org.hibernate.engine.internal.NaturalIdXrefDelegate$NaturalIdResolutionCache.cache(NaturalIdXrefDelegate.java:454)
  at org.hibernate.engine.internal.NaturalIdXrefDelegate.cacheNaturalIdCrossReference(NaturalIdXrefDelegate.java:92)
  at org.hibernate.engine.internal.StatefulPersistenceContext$1.manageLocalNaturalIdCrossReference(StatefulPersistenceContext.java:1769)
  at org.hibernate.action.internal.AbstractEntityInsertAction.handleNaturalIdPreSaveNotifications(AbstractEntityInsertAction.java:184)
  at org.hibernate.action.internal.AbstractEntityInsertAction.(AbstractEntityInsertAction.java:75)
  at org.hibernate.action.internal.EntityIdentityInsertAction.(EntityIdentityInsertAction.java:55)
  at org.hibernate.event.internal.AbstractSaveEventListener.addInsertAction(AbstractSaveEventListener.java:317)
  at org.hibernate.event.internal.AbstractSaveEventListener.performSaveOrReplicate(AbstractSaveEventListener.java:287)
  at org.hibernate.event.internal.AbstractSaveEventListener.performSave(AbstractSaveEventListener.java:193)
  at org.hibernate.event.internal.AbstractSaveEventListener.saveWithGeneratedId(AbstractSaveEventListener.java:126)
  at org.hibernate.ejb.event.EJB3PersistEventListener.saveWithGeneratedId(EJB3PersistEventListener.java:78)
  at org.hibernate.event.internal.DefaultPersistEventListener.entityIsTransient(DefaultPersistEventListener.java:208)
  at org.hibernate.event.internal.DefaultPersistEventListener.onPersist(DefaultPersistEventListener.java:151)
  at org.hibernate.event.internal.DefaultPersistEventListener.onPersist(DefaultPersistEventListener.java:78)
  at org.hibernate.internal.SessionImpl.firePersist(SessionImpl.java:811)
  at org.hibernate.internal.SessionImpl.persist(SessionImpl.java:786)
  at org.hibernate.internal.SessionImpl.persist(SessionImpl.java:790)
  at org.hibernate.ejb.AbstractEntityManagerImpl.persist(AbstractEntityManagerImpl.java:859)
  at com.buschmais.xpl.hibernate.naturalid.test.HibernateTest.usecaseQueryEntityManagerCache(HibernateTest.java:59)

Da hatte ich wohl einfach Pech, denn dies betraf nur die Version „4.1.2.Final“. Sowohl der Vorgänger „4.1.1.Final“ als auch später der Nachfolger „4.1.3.Final“ waren in Ordnung (forum.hibernate.org/2454261). Sollte sich hier eine Lücke in der Qualitätssicherung offenbaren?

Mit Version „4.1.4.Final“ führte ich dann die eigentliche Evaluierung durch (ich berichtete). Auch hier stellte sich rasch Ernüchterung ein. Erst nach einigem „Forschen“ war es mir möglich, das Feature überhaupt zu aktivieren. Egal was ich anstellte, es ließ sich keine Optimierung für die Datenbank-Abfragen erzielen. Erst folgender „Trick“ verschaffte Abhilfe:

Employee e1 = new Employee();
entityManager.persist(e1);
e1.setName("Frank Schwarz");
entityManager.flush();

Das Attribut „name“ ist in diesem Beispiel die Natural-Id. Der eigentliche Primärschlüssel wird mittels @GeneratedValue künstlich erzeugt. Der Trick bestand darin, mittels persist() den Primärschlüssel generieren zu lassen und mittels flush() den transaktionalen NaturalId-Cache zu befeuern. Erst dann antwortete dieser auch auf meine Abfragen. Ich stellte kurzerhand ein Ticket ein: HHH-7304. Schon 8 Monate später war der Bug mit Version „4.1.10.Final“ behoben – und das obwohl relativ schnell eine dritte Person einen Patch zur Verfügung stellte.

Mit dem Hibernate-Release 4.2.0.Final wiederholte ich meine Tests. Der Bug HHH-7304 war tatsächlich gefixt: Ein einfaches

Employee e1 = new Employee();
e1.setName("Frank Schwarz");
entityManager.persist(e1);

reichte, um den transaktionalen NaturalId-Cache zu befüllen. Selbst diese Konstellation funktionierte nun:

Employee e1 = new Employee();
entityManager.persist(e1);
e1.setName("Frank Schwarz");

Das ist zwar etwas unerwartet, da vor dem ersten Persistieren der (alternative) Schlüssel feststehen sollte – aber wer wollte hier kleinlich sein.

Der wichtigste Anwendungsfall für Natural-Ids funktioniert immer noch nicht! Es dauerte auch nicht lange, bis jemand das passende Ticket einstellte.

Folgender Code bei aktiviertem Shared-Cache:

// Erzeugen der Daten
entityManager = entityManagerFactory.createEntityManager();
entityManager.getTransaction().begin();
Employee e1 = new Employee();
e1.setName("Frank Schwarz");
entityManager.persist(e1);
entityManager.getTransaction().commit();
entityManager.close();

// Session 1
entityManager = entityManagerFactory.createEntityManager();
session = entityManager.unwrap(Session.class);
Employee e2 = (Employee) session
                .bySimpleNaturalId(Employee.class)
                .load("Frank Schwarz");
entityManager.close();

// Session 2
entityManager = entityManagerFactory.createEntityManager();
session = entityManager.unwrap(Session.class);
Employee e3 = (Employee) session
                .bySimpleNaturalId(Employee.class)
                .load("Frank Schwarz");
entityManager.close();

Der Block „Session 1“ führt zu zwei SQL-Statements:

  1. select employee_.id as id1_0_
      from Employee employee_
      where employee_.name=?
  2. select employee0_.id as id1_0_0_, employee0_.name as name2_0_0_
      from Employee employee0_
      where employee0_.id=?
    

Der Block „Session 2“ erzeugt dieses SQL-Statement:

  1. select employee_.id as id1_0_
      from Employee employee_
      where employee_.name=?

Statement 1 lädt nur die Id und ist mit aller Wahrscheinlichkeit auf einen Cache-Miss des NaturalId-Caches zurückzuführen. Statement 2 lädt die gesamte Entität per find-by-id-Abfrage. Statement 3 ist wieder ein Cache-Miss des NaturalId-Caches. Ein Wiederholung von Statement 2 bleibt wohl deswegen aus, weil die Entität mittlerweile im Share-Cache vorliegt und dort per Id referenziert werden kann.

Welche Ironie! Da nutzt man ein Feature zur Reduktion von SQL-Statements und bekommt statt des regulären Statements noch ein weiteres für den Cache-Miss dazu! Ich gebe nur ungern den Gernot Hassknecht, aber HIER SOLLTE ÜBERHAUPT KEIN SQL-SELECT-STATEMENT ERSCHEINEN!!1! DAS KANN DOCH NICHT SO SCHWER SEIN! VERDAMMT<lautes Piepen . . . >

Referenzen

[1] http://docs.jboss.org/​hibernate/​orm/4.1/manual/en-US/​html_single/​#mapping-declaration-naturalid
[2] http://docs.jboss.org/hibernate/​orm/4.1/​manual/en-US/​html_single/​#query-criteria-naturalid
[3] http://planet.jboss.org/​post/​4_1_feature_loading_by_natualid

Kommentare sind abgeschaltet.