Wer kennt es nicht, das Persistenz-Framework Hibernate? Es implementiert die JSRs 220 und 317 und ist die wahrscheinlich am häufigsten verwendete Bibliothek seiner Art. Wird Hibernate nicht verwendet, so wurde seine Einführung zumeist diskutiert. Die JBoss Community leistete ganze Arbeit und prägte mit innovativen Ideen und Lösungsansätzen die oben genannten JSRs nicht nur geringfügig. Sogar in der .NET-Welt hinterlässt Hibernate seine Spuren.
Doch hin und wieder stolpert man über Verhaltensweisen, die erst bei sehr genauem Hinsehen nachvollziehbar sind – manch einer möchte sie auch als Bugs bezeichnen. So ist nicht nur das kaskadierende Persistierungsverhalten von Hibernate ungewöhnlich bis fehlerhaft, sondern auch das Abbilden des Datentyps @Temporal(TemporalType.TIMESTAMP) java.util.Date
in Verbindung mit dem RDBMS Oracle ist tückisch. Letzteres ist Gegenstand dieses Artikels.
Es ist durchaus gängig, eine Funktionalität von Hibernate zu nutzen, die es dem Entwickler ermöglicht, das Datenbankschema zu generieren. So spart man zu Beginn eines Projekts Zeit, wenn man inkrementell entwickelt und das Datenmodell noch modelliert. Zur Nutzung der Funktionalität ergänzt man die persistence.xml-Datei um folgenden Eintrag:
<property name="hibernate.hbm2ddl.auto" value="create-drop" />
Hat man die notwendigen Rechte auf dem Schema, so generiert Hibernate bei der Initialisierung der Persistenzeinheit ein ziemlich gutes Datenmodell – vorausgesetzt die Definition der Persistent Entities ist entsprechend.
Fast jedes RDBMS hat einen eigenen Dialekt. Hibernate geht dieses Problem sehr geschickt an und ermöglicht es dem Entwickler, mit folgendem Eintrag in der persistence.xml-Datei den Dialekt anzugeben:
<property name="hibernate.dialect" value="org.hibernate.dialect.OracleDialect" />
Mit dieser Einstellung wird Hibernate bekannt gegeben, dass es den Oracle-Dialekt verwenden soll. Es kann so problemlos SQL-Statements (seien es DDL oder DML) generieren und gegen die Datenbank absetzen.
Folgendes SQL-Statement wird von Hibernate für die Generierung einer Tabelle angelegt. Hierbei handelt es sich nicht um das Original-Statement, es weicht evtl. stark ab.
create table MY_ENTITY ( MY_ENTITY_PK number(19) not null, LAST_TOUCH timestamp(9), primary key (MY_ENTITY_PK) );
Die nachfolgend aufgeführte Klasse ist die Grundlage für die zuvor dargestellte DDL.
package org.acme.entity; import java.io.Serializable; import java.util.Date; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.SequenceGenerator; import javax.persistence.Table; import javax.persistence.Temporal; import javax.persistence.TemporalType; import javax.persistence.Version; @SuppressWarnings("serial") @Entity @Table(name = "MY_ENTITY") public class MyEntity implements Serializable { private Long id; private Date lastTouch; @Id @SequenceGenerator(name = "MyEntity.Sequence", sequenceName = "SEQ_MY_ENTITY") @GeneratedValue(strategy = GenerationType.AUTO, generator = "MyEntity.Sequence") @Column(name = "MY_ENTITY_PK") public Long getId() { return id; } public void setId(Long id) { this.id = id; } @Version @Temporal(TemporalType.TIMESTAMP) @Column(name = "LAST_TOUCH") public Date getLastTouch() { return lastTouch; } public void setLastTouch(Date lastTouch) { this.lastTouch = lastTouch; } }
Persistiert man nun eine neue Entität und liest diese wieder aus, so wundert sich der eine oder andere vielleicht, dass die Millisekunden am Feld LAST_TOUCH
fehlen. Dies ist keineswegs ein Bug, sondern vielmehr die Folge eines anderen Fehlers. Um der Sache auf den Grund zu gehen, schaut man in die Datenbank, ob alles stimmt. Und schon dort liegt der Hund begraben. Das Feld LAST_TOUCH
wurde nicht wie erwartet als Timestamp(9)
angelegt, sondern als Date
. Aber Date
speichert doch nur das Datum, nicht die Uhrzeit? Nein, Oracle speichert bei Date
auch die Uhrzeit – bis auf die Sekunde genau. Aber eben die Millisekunden fehlen, und auf die kommt es an.
Welche Maßnahmen kann man also ergreifen, um diesem Verhalten zu begegnen? Nun, zum einen könnte man die DDL manuell erstellen. Aber das ist nicht notwendig, denn Hibernate ist – wie schon gesagt – wirklich gut darin, DDL zu generieren, wenn alle Parameter stimmen. Um diesem Verhalten ohne große Mühen zu begegnen, nimmt man den SQL-Dialekt einer anderen Oracle-Version.
<property name="hibernate.dialect" value="org.hibernate.dialect.Oracle10gDialect" />
Der obige Dialekt ist – Sie ahnen es – explizit für Oracle 10g geschrieben worden. Mit ihm werden Tabellen korrekt generiert und Timestamps können problemlos gespeichert werden. Auch der Dialekt für 9i ist fehlerfrei. Das Geheimnis ist, dass Oracle erst ab Version 9 zwischen Date
und Timestamp
unterscheidet. Wirklich fehlerhaft in dieser Hinsicht ist nur eine frühe Version des Oracle 9-Dialekts.
Anmerkung:
Die Dialekte
org.hibernate.dialect.OracleDialect
org.hibernate.dialect.Oracle9Dialect
sind überholt und deswegen mit @Deprecated
annotiert.
Besonders interessant ist zusätzlich noch die @Version Annotation
am Timestamp
-Feld. Würde man auf die neu angelegte Persistent Entity noch eine Referenz behalten, ein Attribut modifizieren und dann „mergen“, so würde eine org.hibernate.StaleObjectStateException
geworfen werden. Das liegt ganz einfach daran, dass das gespeicherte Versionsattribut nicht mit der Version in der Persistent Entity-Instanz übereinstimmt, denn die Version wird von Hibernate gesetzt und nicht vom RDBMS. Deswegen hat die Persistent Entity-Instanz noch einen Timestamp
mit Millisekunden, das Datenbankpendant aber nicht.
Und einmal mehr wird klar, dass der Teufel im Detail steckt. Die Dialekte von Hibernate sind mit Vorsicht zu genießen. Dies liegt aber weniger an Hibernate, sondern vielmehr an den einzelnen Datenbankherstellern.
Sehr guter Artikel.
Schöne Grüße,
Jens