Mit ‘Hibernate’ getaggte Artikel

Erfahrungen mit Hibernate-Criteria und pgAdmin(postgres)

Montag, 15. Februar 2010

Wärend der Arbeit an einer Web Applikation, die Hibernate und eine PostgresDB verwendet, sind mir in letzter Zeit folgende Kleinigkeiten aufgefallen:

Hinzufügen von “Nicht NULL” Spalten im pgAdmin III

Möchte man mit dem pgAdmin III Tool nachträglich eine Spalte zu einer Tabelle hinzufügen, so öffnet man den entsprechenden Dialog z.B. über das Menü “Bearbeiten -> Neues Objekt -> neue Spalte”. Wir versuchen nun eine Spalte “darfAngeln” vom Typ “boolean” anzulegen. Diese soll keine “null”-Werte enthalten und als Standardwert “false” benutzen. Das ganze funktioniert solange, wie noch keine Einträge in der Tabelle existieren. Enthält die Tabelle allerdings Einträge, erhalten wir folgende Fehlermeldung:

Ein Fehler ist aufgetreten:
FEHLER: Spalte >darfAngeln< enthält NULL-Werte

Die Fehlermeldung läßt sich umgehen, indem man das Anlegen der Spalte per simplem SQL erledigt. Ins Query Fenster des pgAdmin wechseln und folgenden Code ausführen:

ALTER TABLE adm_raum_art
ADD COLUMN darfAngeln boolean NOT NULL DEFAULT false;

Punkt-Notation bei Criteria

Leider mußte ich durch eine Exception feststellen, dass die Verwendung der Punkt-Notation (implizite Erzeugung von Joins) zum Zugriff auf verknüpfte Objekte nicht möglich ist (im Gegensatz zu HQL). Um also Restrictions auf verschiedenen, assozierten Objekten “oder” verknüpfen zu können, bedarf es “createAlias” für die Erzeugung von expliziten Joins. Kleines Beispiel:

final Criteria criteria = getSession().createCriteria(Schule.class);
criteria.createAlias("schulAnsprechpartner", "schulAP", Criteria.LEFT_JOIN)
.createAlias("schulAP.person", "pers", Criteria.LEFT_JOIN)
.createAlias("pers.benutzer", "benutzer", Criteria.LEFT_JOIN)
.add(Restrictions.or(Restrictions.eq("darfAngeln", true),
Restrictions.eq("benutzer.id", 0815)));

Im Beispiel wurde nun doch die Punkt-Notation verwendet, allerdings immer nur mit der “Rekursionstiefe 1″.
Vielleicht kommt irgendwann auch für Criteria die Möglichkeit eine beliebige Tiefe zu verwenden.


Sven Seiler


Datum in Hibernate: Der Wolf(Timestamp) im Schafspelz(Date)

Freitag, 22. Januar 2010

Benutzt man Hibernate als OR-Mapper und will ein java.util.Date (Datum und Uhrzeit) speichern, ist Vorsicht geboten. Da java.sql.Timestamp von java.util.Date erbt, und der Typ in der Datenbank i.d.R. timestamp ist, bekommt man von Hibernate gegebenenfalls ein Timestamp-Objekt, und nicht ein Date-Objekt. Eigentlich kein Problem, doch die Klasse Timestamp ist …kaputt! Auszug aus dem Javadoc von Timestamp:

The Timestamp.equals(Object) method never returns true when passed a value of type java.util.Date because the nanos component of a date is unknown.

Will man zwei Dates vergleichen und befindet sich hinter der ersten Referenz ein Timestamp-Objekt, schlägt equals fehl.

Date d = new Date();
Date ts = new Timestamp(d.getTime());
System.out.println(ts.equals(d));    //returns false

Wie soll ich also zwei Dates miteinander vergleichen? Es wird noch schlimmer:

System.out.println(ts.compareTo(d)); // return 0
System.out.println(d.compareTo(ts)); // returns 1

Auch die compareTo-Methode ist fehlerhaft liefert ein anderes Ergebnis als erwartet. Schaut man sich den Konstruktor von Timstamp an, kommt man aus dem Staunen Entsetzen nicht mehr heraus. Die Klasse Date speichert das Datum millisekundengenau in dem Feld fastTime. Die Klasse Timestamp hingegen speichert das Datum sekundengenau in dem Feld fastTime und die Milli- und Nanosekunden im Feld nanos. Warum dies so ist, und nicht nur die Nanosekunden im Feld nanos gespeichert werden, ist mir ein Rätsel.

Will man nun ein Date und einen Timestamp miteinander vergleichen (z.B. auch mit before() und after()) schlägt dies fehl. Die compareTo-Methode von Date vergleicht die Millisekunden aus dem Feld fastTime, beim Timestamp-Objekt ist dies nur sekundengenau, die Millisekunden sind im Feld nanos gespeichert, und werden nicht berücksichtigt.

Dieses Problem tritt häufig in Verbindung mit Hibernate auf, die Schuld liegt aber bei der Standard Java-API. Allerdings lässt sich das Problem umgehen, indem man Hierbernate mit einem UserType zwingt, nur Objekte vom Typ Date zu liefern.

public class DateTimeType implements UserType {
    private static final int[] SQL_TYPES = new int[] {Types.TIMESTAMP};
    public DateTimeType() {
        super();
    }
 
    @SuppressWarnings("unchecked")
    public Class returnedClass() {
        return java.util.Date.class;
    }
 
    public int[] sqlTypes() {
        return SQL_TYPES;
    }
 
    public boolean equals(final Object x, final Object y) {
        if (x == y) {
            return true;
        }
        if (x == null || y == null) {
            return false;
        }
        Date xDate = (Date) x;
        Date yDate = (Date) y;
        return xDate.equals(yDate);
    }
 
    public Object nullSafeGet(final ResultSet rs, final String[] names, final Object owner) throws SQLException {
        // extract Timestamp from the result set
        Timestamp timestamp = (Timestamp) Hibernate.TIMESTAMP.nullSafeGet(rs, names[0]);
        // return the value as a java.util.Date (dropping the nanoseconds)
        if (timestamp == null) {
            return null;
        } else {
            return new Date(timestamp.getTime());
        }
    }
 
    public void nullSafeSet(final PreparedStatement st, final Object value, final int index) throws SQLException {
        // handle the NULL special case immediately
        if (value == null) {
            st.setTimestamp(index, null);
            return;
        }
        // make sure the received value is of the right type
        if (!Date.class.isAssignableFrom(value.getClass())) {
            throw new IllegalArgumentException("Received value is not a [java.util.Date] but [" + value.getClass() + "]");
        }
        // set the value into the resultset
        Timestamp tstamp = null;
        if (value instanceof Timestamp) {
            tstamp = (Timestamp) value;
        } else {
            tstamp = new Timestamp(((Date) value).getTime());
        }
        st.setTimestamp(index, tstamp);
    }
 
    public Object deepCopy(final Object value) {
        if (value == null) {
            return null;
        } else {
            return ((Date) value).clone();
        }
    }
 
    public boolean isMutable() {
        return true;
    }
 
    @Override
    public Object assemble(final Serializable cached, final Object owner) {
        return deepCopy(cached);
    }
 
    @Override
    public Serializable disassemble(final Object value) {
        return (Date) value;
    }
 
    @Override
    public int hashCode(final Object x) {
        return x.hashCode();
    }
 
    @Override
    public Object replace(final Object original, final Object target, final Object owner) {
        return deepCopy(original);
    }
}

Möglich ist eine Konfiguration auf Packet-Ebene, indem man eine package-info.java erstellt. Folgend die Konfiguration mit Annotationen auf Klassenebene:

@TypeDefs( {@TypeDef(name = "dateTimeType", typeClass = DateTimeType.class)})
public class MyBusinessObject {
 
    @Type(type = "dateTimeType")
    @Column(name = "time")
    private Date time;
    ...
}

Eventuell wird die freie Joda Time API in Java 1.7 die alten Datum und Kalendarklassen ersetzten. Bis dahin kann man mit diesem UserType sicher sein, dass Hibernate nur Date-Objekte zur Verfügung stellt, und somit kann man wieder equals und compareTo ohne Bedenken verwenden.
Siehe auch


Jan Kuenstler


Hibernate 3.3 Update und c3p0 connection pool

Donnerstag, 15. Januar 2009

Die Umstellung einer Anwendung von Hibernate Version 3.2 auf Version 3.3 ist weniger trivial als zu vermuten wäre. Ein einfaches Austauschen des hibernate3.jar führt hier nicht zum Erfolg sondern endet in einer Reihe von Fehlermeldungen.
Ab Version 3.3 benötigt Hibernate zusätzliche Bibliotheken, da z.B. das Logging auf SLF4J umgestellt wurde. Hier eine kurze Übersicht der zusätzlich nötigen JAR Dateien und welche konkret auf unserem System zum Einsatz kommen:

  • commons-collection ab Version 3.1 : commons-collection-3.2.jar
  • slf4j-api : slf4j-api-1.5.5.jar
  • slf4j Adapter für log4j: slf4j-log4j12-1.5.5.jar

Nachdem die Anwendung nun fehlerfrei startete, dachte man. alles sei in Ordnung. Doch nach einigen Stunden meldete die Überwachung Fehler beim Zugriff auf die Datenbank:

Caused by: com.mysql.jdbc.exceptions.jdbc4.CommunicationsException: The last packet successfully received from the server was100413 seconds ago.The last packet sent successfully to the server was 100413 seconds ago, which is longer than the server configured value of ‘wait_timeout’. You should consider either expiring and/or testing connection validity before use in your application, increasing the server configured values for client timeouts, or using the Connector/J connection property ‘autoReconnect=true’ to avoid this problem.

Nach einiger Suche stellte sich heraus, dass Hibernate gar nicht, wie gewünscht, für das Connection Pooling C3P0 verwendetet sondern seinen eigenen, rudimentären Connection Pool. Die hibernate.cfg.xml enthielt aber alle nötigen hibernate.c3p0 Properties weshalb die Ursache nicht offensichtlich war. Des Rätsels Lösung liegt in der ab Version 3.3 nötigen expliziten Angabe der ConnectionProviderClass. Mit Hilfe der Property hibernate.connection.provider_class lässt sich diese angeben. Folgender Eintrag muss also in der hibernate.cfg.xml hinzugefügt werden um C3P0 als Connection Pool zu verwenden:

<property name="hibernate.connection.provider_class">
org.hibernate.connection.C3P0ConnectionProvider
</property>

Nun verwendet Hibernate auch wieder C3P0 und die Anwendung läuft seitdem ohne Probleme.

Viel Spaß und Erfolg beim Ausprobieren und Anwenden.


Christof Aenderl


Volltextsuche in persistierten Objekten mit Hibernate Search

Montag, 01. Dezember 2008

Das Hibernate Projekt bietet mit Hibernate Search die Möglichkeit, die in einer Datenbank persistierten Objekte bequem zu durchsuchen. Als Suchmaschine für die Volltextsuche kommt dabei Apache Lucene zum Einsatz. Die Einrichtung ist in nur wenigen Schritten erledigt:

  1. Zusätzlich zur vorhandenen Hibernate Installation müssen die Jar-Dateien aus dem Hibernate Search Release in das eigene Projekt eingebunden werden.
  2. Die Hibernate Konfiguration muss um 2 Properties ergänzt werden:
    <session-factory>
    ...
    <property name="hibernate.search.default.directory_provider">
    org.hibernate.search.store.FSDirectoryProvider
    </property>
    <property name="hibernate.search.default.indexBase">
    luceneIndexPfad
    </property>
    ...
    <session-factory>

    Die erste Property legt dabei den Directory-Provider fest. In diesem Fall wird der FSDirectoryProvider gewählt, der den Index für die Volltextsuche im Filesystem ablegt. Die zweite Property gibt das Basisverzeichnis für diesen Index an. Einige in Lucene bereits enthaltene Alternativen zu dem hier verwendeten Directory-Provider sind unter [1] beschrieben.

  3. Als nächstes müssen die Hibernate-Mappings der zu durchsuchenden Entitäten um einige Annotations ergänzt werden:
    1. Die Entity-Klasse muss zunächst um die Annotation
      @Indexed

      ergänzt werden. Optional kann hier noch mit

      @Analyzer(impl = MyAnalyzer.class)

      eine eigene Analyzer-Klasse angegeben werden, da der voreingestellte StandardAnalyzer nur für die Suche in englischen Texten optimiert ist und sich mit einem an die jeweilige Sprache angepaßten Analyzer bessere Ergebnisse erzielen lassen. Weitere Analyzer (u.A. für deutschsprachige Texte) findet man unter [2].

    2. Die zu durchsuchenden Felder werden mit
      @Field(index=Index.TOKENIZED)

      annotiert, wobei Index.TOKENIZED bedeutet, dass der Inhalt des Feldes beim Anlegen des Index in Tokens zerlegt wird. Dies ist nötig, damit später innerhalb des Feldes nach einzelnen Worten gesucht werden kann.

    3. Für jede Entität muss genau ein Feld mit der Annotation
      @DocumentId

      als Id für Lucene markiert werden. Dieses Feld muss ein Objekt innerhalb des gesamten Index eindeutig identifizieren (auch gegenüber Objekten andere Entity-Klassen).

Als nächstes muß der aktuelle Bestand erstmalig indexiert werden. Für eine vorhandene Hibernate Session mySession und eine Liste myArticles von Artikel-Objekten könnte das wie folgt geschehen:

FullTextSession fullTextSession = Search.createFullTextSession(mySession);
Transaction tx = fullTextSession.beginTransaction();
for (Article article : myArticles) {
   fullTextSession.index(article);
}
tx.commit();

Nachdem dies abgeschlossen ist, können Volltextsuchen durchgeführt werden. Neue Objekte werden zukünftig beim Persistieren automatisch in den Index aufgenommen.

Eine Suchanfrage mit vorhandener Hibernate Session mySession in einem Bestand von Artikeln, die ein mit @Field annotiertes Feld textContent haben, könnte dann wie folgt aussehen:

String searchTerm = "Hibernate";
FullTextSession fullTextSession = Search.createFullTextSession( mySession );
Transaction tx = fullTextSession.beginTransaction();
QueryParser parser = new QueryParser("textContent", new StandardAnalyzer());
org.apache.lucene.search.Query query = parser.parse( searchTerm );
org.hibernate.Query hibQuery = fullTextSession.createFullTextQuery( query, Article.class );
List result = hibQuery.list();
tx.commit();
session.close();

Im Anschluß befinden sich in result alle Artikel, deren textContent das Wort Hibernate enthält. Neben der exakten Suche gibt es noch viele weitere Suchmöglichkeiten (z.B. mit Wildcards und unscharfe Suchen). Mehr zu diesem Thema findet man unter [3].


Malte Wulf