28.11.2012

JPA 2.0 - Simple Collection Mapping

Mit JPA 2.0 wurde die Arbeit mit Collections von einfachen Datentypen deutlich vereinfacht. War in der vorangegangenen Version noch eine explizit definierte "Wrapper-Entität" notwendig, ist es nun deutlich einfacher.

Collections, und auch Maps, von einfachen Datentypen können direkt und somit ohne weitere Entitäten gemappt werden. Nahezu alle Konfigurationsmöglichkeiten bleiben dabei erhalten. Ein paar Feinheiten und Besonderheiten sind allerdings zu beachten.

Für die Beispiele wird die JPA Spec (JSR-317) zu Grunde gelegt.


java.util.List<T> / java.util.Set<T>

In JSR-317 ist definiert, dass alle Collections von einfachen Datentypen, wie auch von sogenannten Embeddables, durch eine einfache Annotation gemappt werden können. Ein einfaches Beispiel ist für diesen Fall schnell erstellt.


Die zentrale Annotation ist in diesem Fall @ElementCollection zur Definition einer Liste von Vornamen. Somit ist keine weitere Entität notwendig, die einen Wrapper um den einfachen Vornamen bildet, wie es vorher notwendig war. Entsprechende Tabellen werden automatisiert erstellt.


In diesem Fall wird die Beziehung direkt über eine Foreign-Key-Relation in der Tabelle Person_firstnames abgebildet. Auf den ersten Blick wäre eine sprechendere Tabellenbezeichnung sinnvoller. Daher wird die Entität durch eine Annotation erweitert:


@CollectionTable bietet die Standardkonfigurationsoptionen, wie:
  • name
  • schema
  • catalog
  • joinColumns
  • uniqueConstraints.
Die automatisch erstelle Tabelle wird sofort an das geänderte Mapping angepasst.


Weitere Konfigurationen sind nicht notwendig und wie zu sehen ist, wird das Mapping für einfache Datentypen deutlich einfacher.

@ElementCollection kann aber nicht nur auf einfache Datentypen, sondern auch auf Embeddables angewandt werden. Ein Embeddable ist eine Klasse, die innerhalb der beinhaltenden Entität gespeichert wird und sich die Id mit der beinhaltenden Klasse teilt. Dabei wird jedes persistente Attribut / Embeddable mit persistiert.


Mit der Erweiterung durch die Liste der favorisierten Gerichte, werden die generierten Tabellen ebenfalls umfangreicher.


Wie zu sehen ist, wird aus dem FavoriteFood keine separate Entität, sondern, wie auch bei den Vornamen, eine einfache Liste, verknüpft über eine normale Fremdschlüsselbeziehung.

In beiden Fällen, sowohl bei den einfachen Datentypen, als auch bei den Embeddables, ist allerdings wichtig, dass mit dem Interface java.util.List gearbeitet wird und nicht mit einer konkreten Implementierung. Wird nicht mit dem Interface gearbeitet, tritt bereits einer Fehler bei der Analyse durch dem OR-Mapper auf.


Ein Grund für eine konkrete Implementierung wäre als Beispiel das Sortieren. Das kann an dieser Stelle eleganter gelöst werden.


Mit Hilfe der Annotation @OrderBy kann sowohl die Spalte nach der sortiert werden soll, als auch die Reihenfolge (asc oder desc) definiert werden. @OrderBy sorgt für eine Erweiterung der Statements um eine "ORDER BY" Bedingung auf die angegebene Spalte.

Spannend ist auch die Speicherung einer Liste von Enums. Wie beim Speichern von Enum-Werten, kann auch für die gesamte Liste angegeben werden, ob der Ordinalwert oder die textuelle Darstellung gespeichert werden kann.



java.util.Map<K,V>

Für die Benutzung von Map gelten im wesentlichen die gleichen Regeln, wie auch für das Nutzen von Set oder List. Somit wird der Einsatz von Maps in Entitäten leichter als er es vormals war. Eine Map mit einfachen Werten wird, wie auch Listen oder Sets, leicht gemappt, daher steige ich auch hier direkt mit einem etwas komplexeren Beispiel ein. Die Liste der Haustiere wird ersetzt durch eine Map der Haustiere mit ihren zugehörigen Namen.


Die Tabellenstruktur wird entsprechend um eine Tabelle erweitert.


Dabei ist zu erkennen, dass die neu erstellte Tabelle Person_pets neben dem Fremdschlüssel zur Person zwei weitere Spalten beinhaltet. Die eine Spalte pets_KEY entspricht dem Schlüssel der Map (dem Namen des Haustieres) und die Spalte pets entspricht dem Wert in der Map. Sofort fällt auf, dass neben dem nicht unbedingt glücklichem Namen der Tabelle auch nicht unbedingt der Ordinalwert der Enum gespeichert werden soll. Auch dies kann durch Annotationen behoben werden.



Mit @Enumerated wird festgelegt, dass der Wert des Eintrages in der Map als textuelle Repräsentation gespeichert wird. Gleichzeitig können mit Hilfe der Annotation @MapKeyColumn die Eigenschaften der Schlüsselspalte festgelegt werden. Sollen die Eigenschaften der Wert-Spalte festgelegt werden, wird dafür die normale @Column Annotation verwendet.

Was ist aber, wenn die Schlüsselspalte der Map als Enum werden soll?



Mit der Annotation @MapKeyEnumerated kann die Art der Speicherung für den Enum-Wert in der Schlüsselspalte festgelegt werden. Somit besteht auch an dieser Stelle die Möglichkeit, dass nicht der Ordinalwert, sondern die textuelle Repräsentation gespeichert wird.

Wie also zu sehen ist, wird das Mapping von einfachen Collections mit JSR-317 deutlich leichter und komfortabler. Viel Erfolg beim Ausprobieren!

Die Beispiele finden sich wie immer im Repository.

22.11.2012

Criteria API - CAST ( .. AS .. )

Wer kennt es nicht? Der ewige Zwist zwischen (alt-eingessenen) DBAs und dem Programmierer an sich. Im wesentlichen zwei Spezies, die meist nur schwer miteinander zu vereinen sind.

Ich muss zugeben, dass das nicht immer stimmt, denn die Mehrzahl der Gespräche sind im wesentlichen sinnvoll. Die Probleme bestehen meist eher in dem antiquierten Datenbankmodell, was über die Jahre gewachsen und gewachsen und gewachsen ist. Da schleicht sich meist die eine oder andere Inkonsistenz ein, gerade was die Typen innerhalb einer Tabelle angeht. Da werden Zahlwerte als char-Felder mit festen Längen gespeichert, Datumsangaben mal als Timestamp, dann wieder als char und zwischendurch auch mal ganz anders.

Trifft man auf solche Probleme, sind immer spezielle Lösungen erforderlich um die eigentlichen Anforderungen umzusetzen. Am einfachsten wird das Problem anhand eines Beispiels klar. Die gegebene Tabellenstruktur ist gekennzeichnet durch das folgende SQL:



Dort ist klar zu erkennen, dass die Spalte INT_VALUE in der Datenbank als varchar(5) abgebildet wird. Die Entität selbst bildet die Spalte allerdings auf einen int-Wert ab.


Beim Mapping durch Hibernate werden noch keinerlei Probleme sichbar. Das Mapping kann ohne weitere Probleme durchgeführt werden und ein erster Test ist erfolgreich.



Auch das Auslesen ist für korrekt eingefügte Werte möglich.


Das dies funktioniert liegt allein daran, dass Hibernate bereits einen Teil der Konvertierung übernimmt und die in der Datenbank befindlichen Werte automatisch in die passenden Werte überführt. Deutlich wird dies, wenn einer der Werte nicht mehr numerisch, sondern alphanumerisch ist.


Sofort tritt eine DataException auf, die den Test fehlschlagen lässt, da der geänderte Wert "10A" nicht konvertiert werden kann.

Nun stellt sich die Frage, für welchen Fall ein CAST überhaupt benötigt wird. Die Konvertierung geschieht augenscheinlich automatisch und lieferte korrekte Ergebnisse, wenn die Daten konsistent sind und das Mapping den korrekten Datentypen besitzt. Was passiert aber, wenn die Entität nicht den fachlich korrekten Typen besitzt, sondern an die Deklaration in der Datenbank angelehnt ist?


Damit wird bereits ein einfacher Test zum Problem. Werden alle Produkte mit einem Wert zwischen 1 und 5 abgefragt, sollten 5 Produkte zurückgegeben werden.


Der Test schlägt fehl. Es werden nicht nur die Produkte mit einem Wert zwischen 1 und 5 zurückgegeben, sondern auch das Produkt mit dem Wert 10, da dieses bei einem einfachen Vergleich ebenfalls als korrektes Ergebnis erkannt wird. Das ist in diesem Fall aber gar nicht gewünscht.

Somit muss eine Alternative gefunden werden. Ein möglicher Lösungsansatz ist das Einfügen einer nativen Bedingung mit einem nativen Cast der Werte. Dafür bietet die Criteria-API die Möglichkeit eine Restriktion mit nativem SQL zu definieren.


Mit Hilfe der nativen Einschränkung kann das Statement so erweitert werden, dass ein Vergleich direkt auf numerischer Basis in der Datenbank durchgeführt wird, {alias} sorgt hier für die Angabe der richtigen Tabelle. Anschließend müssen nur noch die Parameter inkl. der zugehörigen Typen gesetzt werden und die Datenbank übernimmt die eigentliche Arbeit. Der Test ist grün.


Sicherlich wäre der elegantere Weg, dass die Entität fachlich korrekt definiert wird und eine automatische Umwandlung durch Hibernate stattfindet. Allerdings ist dies in vielen Fällen nicht möglich, da das Objektmodell nicht angepasst werden soll.

Das Beispiel findet sich wie immer im Repository.




20.11.2012

Maven - Filtering von Resourcen

Auf der Suche nach einer Möglichkeit eine Webanwendung durch den Timestamp und die POM-Version zu kennzeichnen bin ich über die Möglichkeit des Filterings gestolpert.


Über das Filtering ist es recht leicht möglich eine bereits bestehende Properties-Datei zu erweiterten und mit den gewünschten Informationen zu versehen. Der Einfachheit halber wird an dieser Stelle nur das Filtering beschrieben, die Anwendung selbst wird als Konsolenanwendung gebaut. Der Aufbau der Anwendung ist der folgende:


Die Anwendung wird nach dem Packaging auf der Konsole gestartet. Die Anwendung selbst ist nicht mehr als das Lesen der Properties und das nachfolgende Schreiben auf der Konsole.



Ein erster Aufruf zeigt die folgende Ausgabe auf der Konsole:


Wie zu sehen ist, werden die Properties zur Laufzeit noch nicht ersetzt. Für ein Filtering der Resource-Dateien wird eine Erweiterung des POM benötigt.


Nach diesem Schritt ist es möglich, Properties zum Zeitpunkt des Packagings ersetzen zu lassen. Das gilt natürlich auf allen Einträgen im Resource-Verzeichnis. Nach dem Packaging ändert sich die Ausgabe wie folgt:


Zu sehen ist, dass die erste Property, ${pom.version}, als Standardproperty ersetzt werden konnte. Die andere Property, ${last.build}, bleibt allerdings unaufgelöst. Das liegt daran, dass diese noch nicht im POM definiert ist.


Sobald diese definiert ist, kann eine Ausgabe wie gewünscht realisiert werden.


Es gibt unterschiedliche Szenarien in denen ein Filtering zum Einsatz kommen kann. Nicht nur das Hinzufügen von Build-Informationen zu Web-Anwendungen, sondern auch das Ersetzen von speziellen Informationen für Tests, Stage-sepzifische Builds oder anderen spezielleren Einsatzzwecken.

Nice to know.