Archiv für die Kategorie ‘Java’

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


Ich weiß nicht, ob ihr schon wusstet…

Freitag, 11. Dezember 2009

aber ich bin neulich über eine Sache gestolpert, die mir so nicht bewusst war:

Was passiert wohl beim Ausführen des folgenden Java-Codes?

List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> list2 = Arrays.asList(8, 9, 10);
 
list.remove(0);
list.add(2);
list.addAll(list2);

Na wisst ihr es? Man bekommt eine UnsupportedOperationException. Und zwar für jede der list manipulierenden Methoden. Seltsam oder vielleicht doch nicht? Ich für meinen Teil war überrascht. Und auch die Java-Doc für Arrays.asList half mir auf dem ersten Blick nicht sofort weiter:

    /**
     * Returns a fixed-size list backed by the specified array.  (Changes to
     * the returned list "write through" to the array.)  This method acts
     * as bridge between array-based and collection-based APIs, in
     * combination with {@link Collection#toArray}.  The returned list is
     * serializable and implements {@link RandomAccess}.
     *
     * This method also provides a convenient way to create a fixed-size
     * list initialized to contain several elements:
     * 
     *     List<String>; stooges = Arrays.asList("Larry", "Moe", "Curly");
     * 
     *
     * @param a the array by which the list will be backed
     * @return a list view of the specified array
     */
    public static <T> List<T> asList(T... a) {...}

Doch beim genaueren Hinsehen bzw. Überlegen wird es klarer: fixed-size list, a list view of the specified array sowie der Methodenname asList sagen aus, dass es sich nur um eine Ansicht des Arrays bzw. der Eingaben handelt. Die Arrays-Klasse benutzt nämlich unter der Haube die eigene Implementierung java.utils.Arrays.ArrayList und diese implementiert weder add noch remove aus dem List-Interface. Hmm… Irgendwie schon blöd, denn es gibt weder Arrays.toList, noch bietet etwa die konkrete Implementierung ArrayList den komfortablen Var-Args Parameter im Konstruktor. Zudem hätte ich mir eine eindeutigere Dokumentation gewünscht, die ausdrücklich darauf hinweist, dass das Hinzufügen bzw. Entfernen nicht möglich ist.

Möchte man nun trotzdem nicht auf den syntaktischen Zucker verzichten, hilft ein kleiner – zugegebenermasen unperformanter – Workaround:

List<Integer> list = new ArrayList<Integer>(Arrays.asList(1, 2, 3, 4, 5));

Und, habt ihr es gewusst?


Christian Schätzlein


XStream: Object -> XML -> Object

Donnerstag, 19. November 2009

Mit der Bibliothek XStream lassen sich auf einfache Weise Java Objekte in XML zu serialisieren und wieder zurück.
Das kann z.B. dazu dienen, Datensätze als Textdatei abzuspeichern oder XML für Unit-Tests zu erzeugen.
Vorteile von XStream sind die sehr einfache Anwendung und die gute Performance. Neuere Versionen von XStream bieten zudem die Möglichkeit auch JSON zu erzeugen oder die Integration in andere XML APIs.

Nun ein paar Beispiele zur Verwendung von XStream.

XStream verwendet per default die Pull-Parser Implementierung XPP3. Das entsprechende JAR muss hierzu separat heruntergeladen und im Classpath hinzugefügt werden. (Die XPP3 Implementierung am Besten über eine Suchmaschine im Internet suchen.)

Anmerkung:
Wir verwenden hier Aliase für Klassennamen. Das hat den Vorteil, dass das XML lesbarer wird und vor allem eine Deserialisierung auch nach einem Umbenennen oder Verschieben der Klasse funktioniert.

Objekt nach XML serialisieren

  final XStream xstream = new XStream();
  // set alias for the MyData class 
  xstream.alias("my-data", MyData.class);
  // MyData myData = new MyData(....);
  final String xml = xstream.toXML(myData);

Objekt aus XML deserialisieren

  final XStream xstream = new XStream();
  // set alias for the MyData class 
  xstream.alias("my-data", MyData.class);
  // String xml = 
  MyData myData = (MyData) xstream.fromXML(xml);

Interaktion mit dom4J

Mit XStream lassen sich auch Objekte in ein bestehendes XML Document einfügen. In diesem Beispiel wird ein dom4j Element ermittelt und an die Methode addToElement() übergeben, welche das Objekt myData in das Element serialisiert:

  public void addToElement(final MyData myData, final org.dom4j.Element element) {
    final XStream xstream = new XStream();
    xstream.alias("my-data", MyData.class);
    // create new com.thoughtworks.xstream.io.xml.Dom4JWriter
    final Dom4JWriter writer = new Dom4JWriter(element);
    xstream.marshal(myData, writer);
    writer.close();
  }

Natürlich ist der Weg zurück zum Objekt aus dem XML genauso einfach:

  public MyData getFromElement(final org.dom4j.Element element) {
    final XStream xstream = new XStream();
    xstream.alias("my-data", MyData.class);
    final Dom4JReader reader = new Dom4JReader(element);
    return (MyData) xstream.unmarshal(reader);
  }

Viel Spaß und Erfolg beim Ausprobieren und Anwenden.


Christof Aenderl