JPA und die Java 8 API

Mit Java 8 wurde eine neue mächtige Date/Time API eingeführt, welche antritt, mit den Nachteilen der java.util.Date API aufzuräumen.

Gerade bei datumsrechenintensiven Anwendungen drängt sich die Verwendung der neuen Funktionen nahezu auf. Aber was passiert, wenn wir gleichzeitig JPA zum Persistieren solcher Daten im Einsatz haben?

@Temporal dient in der aktuellen JPA Spezifikation zur Annotation von zeitbezogenen Attributen in Domain-Klassen.

@Entity
@Table (name="Orders")
public class Order {

    @Id
    @GeneratedValue (strategy=GenerationType.TABLE)
    private Long id;

    @Temporal (TemporalType.DATE)
    private Date orderDate;

    @Temporal (TemporalType.DATE)
    private Date deliveryDate;

    @Temporal (TemporalType.TIME)
    private Date requestedDeliveryTime;


    @Temporal (TemporalType.TIMESTAMP)
    private Date createdDate;
}

Die JPA 2.1 Spezifikation wurde aber bereits vor der Veröffentlichung von Java 8 finalisiert, daher erlaubt die Temporal Annotation nur das Annotieren von java.util.Date oder java.util.Calendar Attributen. Bei Verwendung der Typen LocalDate, LocalTime oder LocalDateTime muss also zwangsläufig auf diese Angabe verzichtet werden.

@Entity
@Table (name="Orders")
public class Order_Java8 {

    @Id
    @GeneratedValue (strategy=GenerationType.TABLE)
    private Long id;

    private LocalDate orderDate;

    private LocalDate deliveryDate;

    private LocalTime requestedDeliveryTime;


    private LocalDateTime createdDate;

    private String remark;
}

Aktuelle JPA-Implementierungen wie Hibernate oder EclipseLink speichern und lesen diese Werte dennoch transparent für den Entwickler, solange das Datenbankschema durch die Provider erstellt wird. Haben wir es aber bereits mit einer bestehenden Tabellenstruktur und Verwendung der korrekten SQL-Datentypen zu tun, so offenbart sich bereits hier, dass die Werte zwar gespeichert werden können, dies aber nicht korrekt als zeitliche Werte geschieht. Der Versuch ein LocalDate-Objekt in eine Date-Column zu speichern, schlägt wegen des falschen Datentyps fehl.

Ein Blick auf die Datenbank, auf eines durch den Provider generierten Datenbank Schemata, offenbart, dass hier kein zeitlicher Datentyp verwendet wird, sondern die neuen Klassen als LOB oder Varbinary auf der Datenbank landen. Ein Verarbeiten als Datum auf SQL-Ebene ist somit nicht möglich.

Wie können wir dennoch die neue Date-API und die korrekten SQL-Datentypen verwenden?
Seit JPA 2.1 bieten die @Converter eine Möglichkeit an, für das Mapping von Java-Typen auf die Datenbanktabelle eigene Klassen zu hinterlegen. Hierdurch haben wir die Möglichkeit, die neuen Datentypen zu verwenden.

Ein solcher Konverter könnte wie folgt aussehen:

@Converter (autoApply=true)
public class LocalDateConverter implements AttributeConverter<LocalDate, Date>{

    @Override
    public Date convertToDatabaseColumn(LocalDate attribute) {
        return Date.from(
attribute.atStartOfDay().
atZone(ZoneId.systemDefault()).toInstant()
);
    }

    @Override
    public LocalDate convertToEntityAttribute(Date date) {
return date.toInstant().
atZone(
ZoneId.systemDefault()
).toLocalDate();
    }

}

Die neuen Date-API Typen werden hiermit in ihre Repräsentation in der bisherigen java.util.Date-API gewandelt. Die JPA Provider können somit die Werte korrekt als zeitliche Werte erkennen und entsprechend verarbeiten.

Durch Angabe des autoApply=true Attributes wird der Konverter automatisch für alle Konvertierungen zwischen java.util.Date und java.time.LocalDate verwendet. Der Konverter kann durch den ComponentScan, also dem Scannen nach Annotationen, zur Startzeit automatisch erkannt werden. Dadurch ist es auch möglich, die notwendigen Konverter für alle benötigten Datentypen einmalig zu implementieren und als JAR bei allen Anwendungen in den Classpath zu legen. Alternativ ist natürlich auch die Angabe in der persistence.xml möglich.

Bei der Konvertierung ist noch die korrekte Zeitzone zu berücksichtigen. java.util.Date beinhaltet nur einen Zeitpunkt auf der Zeitleiste seit dem 01.01.1970, kennt aber selber keine Zeitzonen-Information.

In unserem Beispiel haben wir die aktuelle Default-Zeitzone des Systems verwendet. Andere Lösungen sind hier je nach benötigter Fachlichkeit möglich, wie das Auslesen der Zeitzone aus den Benutzerdaten oder aus der Konfiguration der Applikation.

Short URL for this post: http://wp.me/p4nxik-2lq
This entry was posted in Java EE, Java Persistence and tagged , , , , , , , , . Bookmark the permalink.

3 Responses to JPA und die Java 8 API

  1. pedro rodriguez says:

    Thanks for the post, please put the converter for localTime and localDateTime class

    • ebamberg ebamberg says:

      the converters for localTime and localDateTime look similar to the shown
      converter:

      something like:

      @Converter (autoApply=true)
      public class LocalDateTimeConverter implements AttributeConverter<LocalDateTime, Date>{

      @Override
      public Date convertToDatabaseColumn(LocalDateTime attribute) {
      return
      Date.from(attribute.atZone(ZoneId.systemDefault()).toInstant());
      }

      @Override
      public LocalDateTime convertToEntityAttribute(Date date) {
      Instant instant = Instant.ofEpochMilli(date.getTime());
      return LocalDateTime.ofInstant(instant, ZoneId.systemDefault());
      }

      }

      and

      @Converter (autoApply=true)
      public class LocalTimeConverter implements AttributeConverter<LocalTime, Date>{

      @Override
      public Date convertToDatabaseColumn(LocalTime attribute) {
      Instant instant = attribute.atDate(LocalDate.of(1970, 1, 1)).
      atZone(ZoneId.systemDefault()).toInstant();
      return Date.from(instant);
      }

      @Override
      public LocalTime convertToEntityAttribute(Date date) {
      Instant instant = Instant.ofEpochMilli(date.getTime());
      return LocalDateTime.ofInstant(instant, ZoneId.systemDefault()).toLocalTime();
      }

      }

      will do the work.

  2. Rust says:

    How could we use converters in the case of NativeQuery ? Is it possible to define LocalDateTime parameter?
    LocalDateTime dt = LocalDateTime.now();
    Query query = em.createNativeQuery(“SELECT myfunc(:currentDay) FROM DUAL”);
    query.setParameter(“currentDayStart”, currentDay);

Leave a Reply