Klaus Schultz  Reverse- & Software-Engineering


 

Home
Nach oben

Erfahrungswerte und Tips für Hibernate 3.2

1. Basics

Für jedes Attribut bzw. jede Collection muss man ein get-/set-Methoden-Paar spendieren. Dieses muss aber nicht unbedingt public sein, private reicht auch. Das ist gut für den Fall, wenn man aus Design-Gründen eine public-Methode mit einer anderen Signatur oder mehr Intelligenz anbieten will. Z.B. zum Zugriff auf eine Collection public nur einen Iterator anbieten will, um die Kapselung zu erhalten.

Hibernate leitet aus dem Java-Typ eines Attributs einen SQL-Typ ab. Das Default-Mapping der Typen ist hier in der Referenz beschrieben (im Buch "Java Persistence with Hibernate" ist es allerdings genauer beschrieben). Dort sollte man vor allem dann hineinschauen, um nachzulesen

bulletob ein Typ wie z.B. BigDecimal ein Default-Mapping hat
bulletwas bei dem Typ Datum zu beachten ist:
in der Datenbank soll in der Regel nur ein Tag-genaues Datum stehen. Das Default-Mapping kann nicht erkennen, ob bei einem Java-Typ Date/Calendar das Ziel ein Tag-genaues Datum ist oder etwas feingranulares, deswegen muss hier mit type=date bzw. mit @Temporal(TemporalType.DATE) nachgeholfen werden.
bulletwas beim Typ Boolean zu beachten ist:
z.B. Oracle kennt diesen Typ nicht als Datenbanktyp. Wenn Boolean als ein Character mit den Werten 'Y'/'N' abgebildet werden soll, muss man das spezifizieren.

Ein Attribut, das einen Geldbetrag darstellt, soll nicht den Typ Double erhalten, sondern BigDecimal (wenn nicht ein genereller Money-Typ zur Verfügung steht). Bei einem Double treten Ungenauigkeiten in den Nachkommastellen auf, die beim Vergleichen von Beträgen zu Fehlern führen.
Hibernate hat ein passendes Default-Mapping für  BigDecimal, z.B.  für Oracle NUMBER(19,2).

Jedes Entity, das durch Hibernate verwaltet wird, muss eine passende equals()- und hash()-Methode erhalten (vgl. Referenz ). Es liegt nahe, dazu den Primärschlüssel bzw. die ID zu verwenden:

@Override
public boolean equals(Object obj) {
   if (this == obj)
       return true;
   if ( obj == null)
       return false;
   if ( obj instanceof MyClazz == false)
       return false;
   MyClazz other = (MyClazz) obj;
   if ( getId() == 0 ) {
       if ( other.getId() != 0 )
          return false;
   }
   else {
      if ( getId() != other.getId() )
         return false;
   }
   return true;
}

Dabei muss beachten:

bulletwenn die ID eine von Hibernate generierte ID ist, dann wird sie erst im Moment des Persistierens zugewiesen. D.h. vorher hat ein solches Objekt eine leere ID, es wird aber evtl. schon mit ihm gearbeitet und die equals()-Methode dabei aufgerufen. Deswegen wird vor dem Vergleich der IDs auf getId() == 0 geprüft.
bulletdie von Eclipse generierte Prüfung
  if (getClass() != obj.getClass())
       return false;

passt hier nicht! Denn Hibernate arbeitet mit Proxy-Klassen, d.h. this und obj können evtl. zu verschiedenen Klassen gehören. Stattdessen ist if ( obj instanceof MyClazz == false)  richtig, wobei MyClazz  für den aktuellen Klassennamen steht.

2. Performance, insbesondere im Batch

Die grundlegende Performance wird natürlich auf der Datenbank erzielt, durch den richtigen Einsatz von Indizes etc. Das ist normales Datenbank-Thema und wird hier nicht weiter erläutert. Hier geht es um Hibernate: werden performante SQL-Statements erzeugt, wie viel SQL-Statements werden erzeugt.

In der Referenzdokumentation ist dem Thema Batch ein eigenes Kapitel gewidmet, ebenso im Buch. Das wiederhole ich hier nicht, sondern kommentiere es teilweise bzw. lenke den Augenmerk auf weitere Aspekte.

2.1 alle Objekte im Hauptspeicher?

Ein Hauptproblem in der Batch-Verarbeitung (bzw. Massenverarbeitung) ist die Unmenge von Objekten, die erzeugt werden und bei naivem Programmieren zu einer out-of-memory-Exception führen (spätestens in der Produktion). Deswegen wird in der Referenzdokumentation bzw. im Buch vorgeschlagen, in regelmässigen Intervallen ein flush() und clear() durchzuführen, um die Objekte aus der Session zu entfernen, so dass sie der GC aufräumen kann (Codezitat aus der Referenzdokumentation) :

Session session = sessionFactory.openSession();
Transaction tx = session.beginTransaction();
   
ScrollableResults customers = session.getNamedQuery("GetCustomers")
    .setCacheMode(CacheMode.IGNORE)
    .scroll(ScrollMode.FORWARD_ONLY);
int count=0;
while ( customers.next() ) {
    Customer customer = (Customer) customers.get(0);
    customer.updateStuff(...);
    if ( ++count % 20 == 0 ) {
        //flush a batch of updates and release memory:
        session.flush();
        session.clear();
    }
}
   
tx.commit();
session.close();

Das ist absolut notwendig, reicht aber nicht. Aus der Datenbank-Programmierung mit DB2 und Oracle ist mir das Pattern bekannt, daß man in regelmässigen Abständen ein COMMIT durchführen sollte - das gilt programmiersprachenunabhängig. Damit wird das obige Beispiel wesentlich komplexer:
bulletan die Stelle des session.flush() tritt ein tx.commit() und session.close()
bulletes muss dann eine neue Session für die nächste Portion aufgemacht werden
bulletim obigen Beispiel kann man nicht mehr das Konstrukt der ScrollableResults verwenden (schade), denn diese sind durch einen Datenbank-Cursor hinterfüttert, und der ist nach tx.commit() geschlossen.
bulletdie neue Session muss neu aufsetzen

Eine Möglichkeit dafür ist, am Anfang des Batches mit einem HQL- oder Criteria-Query einen Arbeitsvorrat zu bestimmen und diesen Arbeitsvorrat dann portionsweise abzuarbeiten. Der Arbeitsvorrat kann natürlich nicht die Menge der zu bearbeitenden Objekte sein (diese würden alle instanziiert, vgl. oben: out-of-memory-Exception), sondern die Menge der IDs der zu bearbeitenden Objekte (Good Bye, Objektorientierung). Bei einem Criteria-Query erreicht man das mit einer Projection:
criteria.setProjection( Projections.id() )   vgl. Kapitel 15.1.3

2.2 Abfragen und Updates gemischt

Ein generelles Problem für Persistenz-Frameworks ist:  wenn eine Abfrage (z.B. Criteria oder HQL oder JPA-QL) gemacht wird, und im Cache des Frameworks sind geänderte Objekte, die noch nicht auf die Datenbank geschrieben wurden - wie soll die Richtigkeit des Abfrageergebnisses gewährleistet werden?

Die Antwort der Hibernate-Autoren und des JPA-Gremiums ist: falls das Abfrageergebnis durch die Objekte im Cache (der Hibernate-Session, des persistence context) beeinflusst werden könnte, wird vor dem Absetzen des SQL-Query die Datenbank mit einem automatischen flush synchronisiert.

Nach meiner Beobachtung führt Hibernate hier ein flush() durch, auch wenn die zu ändernden Tabellen garnichts mit den in der Abfrage berührten Tabellen zu tun haben. Das mag generell OK sein wegen undurchschaubarer Fernwirkungen, aber bremst einen Batchlauf gehörig aus.

Deswegen

bulletnach Möglichkeit den Kontrollfluss so strukturieren, dass zuerst die Abfragen durchgeführt werden und dann die Objekt-Änderungen
bulletwenn das nicht geht, weil z.B. die Fachlichkeit so komplex ist, kann mit
criterium.setFlushMode( FlushMode.COMMIT); bzw.
query.setFlushMode( FlushMode.COMMIT);     bzw.
query.setFlushMode( FlushModeType.COMMIT); in JPA
erreicht werden, dass kein flush bei dieser Abfrage durchgeführt wird, sondern erst beim commit. Das darf man natürlich nur machen, wenn die geänderten Objekte tatsächlich nicht das Abfrageergebnis beeinflussen! Und man muss das sorgfältig dokumentieren, diese Abfrage ist genau für diese Verarbeitung und sollte nicht wie eine normale Methode frei benutzt werden.

2.3 Abfragebeschleunigung durch Batch_Fetch_Size und JOIN FETCH

Nehmen wir folgenden Ausschnitt aus einem Modell und eine Abfrage darauf, die Angestellte einschliesslich ihrer Job-Bezeichnung anlistet.


Hibernate muss dazu die jeweiligen Job-Objekte lesen und instanziieren. Die Einstellung default_batch_fetch_size   beeinflusst, wie effektiv das passiert. Wird default_batch_fetch_size nicht gesetzt, holt Hibernate, nachdem es die Tabelle Employee abgefragt hat, das Job-Objekt zu jedem Angestellten einzeln (sofern noch nicht in der Session vorhanden):

SELECT job0_.job_id as job1_1_0_ ,...alle Attribute von JOB
from JOBS job0_
where job0_.job_id=?

Wird default_batch_fetch_size z.B. auf 10 gesetzt, holt Hibernate auf einen Rutsch 10 Job-Objekte:

SELECT job0_.job_id as job1_1_0_ ,...alle Attribute von JOB
from JOBS job0_
where job0_.job_id in ( ?, ?, ?, ?, ?, ?, ?, ?, ?, ? )

Die Einstellung wirkt übrigens auch im JPA-Modus - in der persistence.xml  ist einfach ein Hibernate-spezifischer Eintrag enthalten:
<property name="hibernate.default_batch_fetch_size"  value="10"/>

Die Assoziation vom Entity Angestellter zum Entity Job ist, wie üblich, mit lazy loading definiert.
@ManyToOne(optional=false , fetch=FetchType.LAZY)
Die Verwendung von FetchType.EAGER verändert die Anzahl SQL-Statements nicht, also bleiben wir bei lazy loading.

Das Optimale für diese Liste von Angestellten ist natürlich, wenn die Jobs mit dem gleichen SQL wie die Angestellten gelesen werden. Das geschieht (sowohl in HQL als auch JPA-QL) mit dem Schlüsselwort JOIN FETCH:
createQuery("select ang from Angestellter ang left join fetch ang.job " )

Während  default_batch_fetch_size eine generelle Einstellung ist, wird hier der Query optimiert, so dass die Job-Objekte eager geladen werden.

 

 

 
Letzte Änderung: 16.06.2009

Zurück zur Startseite