Von Dr. Stefan Koch auf Dienstag, 04. Juli 2017
Kategorie: Application Development

Data-Access-Objekte (DAO) - BEST-P: Find-By-Example

​Der Zugriff auf die Datenbank ist für Enterprise-Anwendungen von zentraler Bedeutung. Neben der fachlichen Konsistenz spielen Aspekte wie Performance und Security eine wesentliche Rolle. Umfangreiche Anwendungen behandeln solche Aspekte in einer eigenen Persistenz-Schicht, die aus Data-Access-Objekten (DAOs) gebildet wird. In Java können diese Klassen mit JPA und dem Design-Pattern Find-By-Example generisch erstellt werden: Eine Klasse enthält die Logik für alle DAOs.

Ein Credo für DAOs

DAO ist eine Abkürzung für das Design-Pattern Data Access Object [1]. Damit wird der Zugriff auf ein Informationssystem – meistens eine Datenbank – gekapselt. Auch wenn das Design-Pattern nicht unumstritten ist [2], gibt es für dessen Nutzung gute Gründe.

Und dann tauchen in Projekten nicht selten auch Aspekte auf, die ohne DAOs nur schwer zu bewältigen sind.

Sicher lassen sich alternative Design-Patterns finden oder konstruieren, um die oben genannten Aspekte zu berücksichtigen. Doch spricht die normative Kraft des Faktischen für das DAO: Jeder Java-Enterprise-Entwickler kennt es.

Nachteile von DAOs

Der Zugriff auf die Datenbank ist Dank der Java Persistence API (JPA) alles andere als spannend: Die Implementierung der CRUD-Operationen sieht für alle DAOs nahezu gleich aus.

Um diesen Code nicht in jede DAO-Klasse schreiben zu müssen, bietet sich eine generische DAO-Klasse an, die als GenericDaoImpl in Abbildung 1 definiert ist. Diese Klasse ist in dem Beispiel für den Einsatz in einer Java EE-Umgebung gedacht – der EntityManager wird in Folge der Annotation @PersistenceContext injiziert. Für die CRUD-Operationen stehen die Methoden save, getById, update und delete zur Verfügung. Damit ist der Grundbaustein für ein DAO gelegt.

Find-By-Example

Der Großteil der Arbeit besteht in den DAO-Klassen darin, die gewünschten find-Methoden zu implementieren. Beispielsweise können Personen nach ihrem Nachnamen gesucht werden und Kunden möchte man anhand des Umsatzes suchen. Darüber hinaus gibt es auch noch Variationen dieser Suche. Neben dem Nachnamen wird auch der Vorname oder das Geburtsdatum angegeben.

Durch derartige Anforderungen entsteht im Laufe der Entwicklung eine Vielzahl von find-Methoden. Eine gut bekannte Strategie zur Selektion von Daten nach unterschiedlichen Suchbegriffen ist das Design-Pattern Find-By-Example (siehe Abbildung 2). Die Suche wird in einem Example-Objekt in der Weise codiert, dass die Suchkriterien als Attribut angegeben werden. Mit Example.person="Koch" sucht der CriteriaBuilderin der Personen-Tabelle nach den entsprechenden Datensätzen. Enthält das Example-Objekt mehr als ein Such-Attribut, so werden alle Attribute mit einem logischen "Und" verknüpft bei der Selektion berücksichtigt. Als Ergebnis der Suche werden die gefundenen Entity-Objekte zurückgeliefert.

Das Design-Pattern Find-By-Example ist optimal geeignet, wenn in der GUI ein Suchdialog angeboten wird. Der Anwender entscheidet, welche Attribute für die Suche herangezogen werden. Auch in Services sind viele Selektionen mit Find-By-Example abzubilden. Die Anzahl der zu implementierenden find-Methoden in der DAO lässt sich dadurch drastisch reduzieren.

Die Idee für dieses Design-Pattern, das auch als Query-By-Example bezeichnet wird, ist bereits zu Beginn der ORM-Frameworks aufgegriffen und beispielsweise in Hibernate oder EclipseLink implementiert worden. Einen Überblick über die Möglichkeiten und Beschränkungen wird in [3] gegeben. JPA stellt keine standardisierte Schnittstelle für diese Art der Suche zur Verfügung.


Abb. 1: GenericDaoImpl: Generische Dao-Klasse

Implementierung mit der Criteria API

Die bestehenden Lösungen von Hibernate und EclipseLink lassen noch folgende Wünsche offen.

In unserem Projekt haben wir uns für eine eigene Implementierung von Find-By-Example auf Basis der Criteria API entschieden. Maßgebend war die Unabhängigkeit vom JPA-Provider und vor allem die Erweiterbarkeit: Es ist absehbar, dass weitere Anforderungen, wie die Suche nach NULL oder NOT NULL, die Negierung der Bedingung oder die Angabe einer Wertemenge dazukommen werden.

In der eigenen Lösung ist das Example-Objekt ein Pojo, dessen Attribut-Namen mit dem der Entity übereinstimmen. Anstelle der primitiven Datentypen sieht die Example-Klasse den Typ String vor, um Suchbegriffe angeben zu können. Anstelle der Assoziationen werden auch für die assoziierten Objekte Example-Objekte angegeben.

In Abbildung 3 ist ein JUnit-Test dargestellt, in dem das Find-By-Example getestet wird. Ausgangspunkt ist ein Personal-Datensatz, der über eine Datei geladen wurde. Dieser Zusammenhang wird durch die Assoziation des Personal- und Upload-Objekts abgebildet. In dem Test wird dazu ein Upload-Objekt mit dem Dateinamen „meine-Datei", dem Personal-Objekt übergeben und mit diesem abgespeichert.

In der Suche sollen alle Personal-Datensätze gefunden werden, die über eine Datei mit dem Muster „meine%"geladen wurden. Das Muster wird mit dem Attribut Dateiname einem UploadExample-Objekt übergeben.

Danach wird das UploadExample-Objekt einem PersonalExample-Objekt übergeben. Dadurch ist die Suche spezifiziert. Der Test findet den zuvor gespeicherten Datensatz – anschließend wird der Gegentest gemacht, indem für den Dateinamen „falscher Dateiname" als Suchbegriff angegeben wird. 

Das Prinzip für die Eigenimplementierung lässt sich im Listing der Abbildung 4 nachvollziehen. Die Attribute der Entity und die der Example-Klassen werden in Key-Value-Paaren zerlegt. Das Ergebnis sind targetTypes und exampleProperties.entrySet. Für jede Property des Example-Objekts wird dann eine Criteria in der For-Each-Schleife von addCriteria erstellt. Hierbei ist zunächst der Datentyp des Example-Attributs bedeutsam. Im Regelfall ist der Typ ein String. Aufgrund des Inhalts und des Datentyps des Entity-Attributs wird eine passende Criteria erstellt.

Ist der Attribut-Typ der Example-Klasse ein Enum, wird einfach eine Criteria für den Vergleich der Enums erstellt. In allen anderen Fällen muss es sich um ein assoziiertesObjekt handeln. Für diesen Fall wird die Method addCriteria rekursiv aufgerufen. Die vollständige Implementierung können Sie in unserem Blog [4] herunterladen.

Abb. 3: PersonalDaoImplTest – Suche nach Personal mit Find-By-Example

Abb. 4: CriteriaBuilder-Auszug

Fazit

Das Design-Pattern Find-By-Example hat sich in unseren Java-Enterprise-Anwendungen bewährt. Durch diesen generischen Ansatz lässt sich nicht selten ein DAO vollständig generisch erzeugen. Die Eigenimplementierung finden Sie in Form eines Maven-Projekts in unseren Blogbeiträgen [4]. Für weitere Fragen rund um das Thema Find-By-Example kontaktieren Sie unsere Experten unter 0 52 51 / 10 63 -0​.

Links/Quellen

[1] Core J2EE Patterns - Data Access Object, Oracle: http://www.oracle.com/technetwork/java/dataaccessobject-138824.html
[2] The DAO Anti-Patterns, RRees: https://rrees.me/2009/07/11/the-dao-anti-patterns/
[3] Hibernate Query By Example, Donat Szilagyi:, https://dzone.com/articles/hibernate-query-example-qbe
[4] ORDIX Blog: https://blog.ordix.de/best-p-find-by-example
Kommentare hinterlassen