7 Minuten Lesezeit (1346 Worte)

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.

  • Single-Responsibility-Prinzip: 
    DAOs konzentrieren sich auf die Interaktion mit der Datenbank. Ihre Aufgabe ist es, Daten aus Objekten in die Datenbank zu überführen und umgekehrt.
  • Don't-Repeat-Yourself:
    Zugriff-Operationen werden einmalig in der DAO definiert. Auf die DAO-Methoden wird im Code vielfach zugegriffen.
  • Testbarkeit:
    Zugriffe auf die Datenbank lassen sich unabhängig von fachlichen Services testen.
  • Zugriffsbeschränkungen:
    In der DAO lässt sich beispielsweise verhindern, dass mal eben 100 Millionen Datensätze eingelesen werden.

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

  • Performance:
    Die Ausführungsgeschwindigkeit kann in dem DAO gemessen und verbessert werden.
  • Security:
    Der Zugriff auf Daten lässt sich an dieser zentralen Stelle kontrollieren.
  • Mandantenfähigkeit:
    In Abhängigkeit von ihren Berechtigungen bekommen Anwender nur einen Teil der Daten zu sehen.
  • Archivierung von Alt-Daten:
    Jede Änderung von Daten soll persistent nachvollziehbar sein.
  • Fachliche Konsistenz-Prüfung:
    Die Datenbank stellt auch eine Eingangs-Schnittstelle zur Anwendung dar. Manchmal muss die Qualität der eingelesenen Daten geprüft werden.

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.


public abstract class GenericDaoImpl<E, P extends Serializable> implements Dao<E, P> {

    @PersistenceContext(unitName = "exampleds")
    protected EntityManager entityManager;
    private final Class<E> type;

    public GenericDaoImpl(Class<E> clazz) {
        this.type = clazz;
    }

    protected void detach() {
        entityManager.flush();
        entityManager.clear();
    }

    @Override
    public E save(E object) {
        entityManager.persist(object);
        detach();
        return object;

    }
    @Override
    public void delete(E object) {
        E foundObject = entityManager.merge(object);
        entityManager.remove(foundObject);
        detach();
    }

    @Override
    public E getById(P id) throws NoResultDaoException {
        E result = entityManager.find(type, id);
        detach();
        if (result == null) {
            throw new NoResultDaoException();
        }
        return result;
    }

    @Override
    public E update(E object) {
        E returnValue = entityManager.merge(object);
        detach();
        return returnValue;

    } 
Abb. 1: GenericDaoImpl: Generische Dao-Klasse

Abb. 2: Prinzip des Find-By-Examples

Implementierung mit der Criteria API

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

  •  Suche in assoziierten Objekten
  • Verwendung von Relationen (>, <, >=, <=) bei numerischen oder Datumsangaben
  • Angabe von Intervallen bei numerischen oder Datumsangaben
  • Suche in Zeichenketten mit Like

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.


@Test
public void findPersonalAnhandUpload() 
	throws NoResultDaoException {
	Personal personal = createPersonalWithUpload(
		"Koch", "Stefan", "meineDatei");
	personal = save(personal);

	UploadExample upExample = new UploadExample();
	upExample.setDateiname("meine%");
	PersonalExample pExample = new PersonalExample();
	pExample.setUpload(upExample);
	List<Personal> ergebnisListe = 
		findByExample(pExample);
	assertEquals(1, ergebnisListe.size());
}

@Test(expected=NoResultDaoException.class)
public void dontFindPersonalAnhandUpload() 
	throws NoResultDaoException {
	Personal personal = createPersonalWithUpload(
		"Koch", "Stefan", "meineDatei");
	personal = save(personal);

	UploadExample upExample = new UploadExample();
	upExample.setDateiname("falscher Dateiname");
	PersonalExample pExample = new PersonalExample();
	pExample.setUpload(upExample);
	findByExample(pExample);
	fail("Exception wird erwartet");
} 
Abb. 3: PersonalDaoImplTest – Suche nach Personal mit Find-By-Example

protected void addCriteria(Path<E> property
	, Object value, Map<String, Class> targetTypes) {
   Map<String, Object> exmampleProperties = 
		propertyMap(value);
   for (Entry<String, Object> entry : 
	exmampleProperties.entrySet()) {

	String propertyName = entry.getKey();
	Object searchObject = entry.getValue();
	Class targetClass = targetTypes.get(propertyName);
	if (targetClass == null) {
		throw new IllegalStateException();
	}
	if (searchObject == null) {
		continue;
	}

	if (searchObject instanceof String) {
		if (targetTypes.get(propertyName) != null) {
		addCriteria4String(property.get(propertyName), 
			(String) searchObject, targetClass);
		}
	} else if (searchObject.getClass().isEnum()) {
		addCriteria4Enum(property.get(propertyName), 
			searchObject, targetClass);
	} else {
		addCriteria(property.get(propertyName), 
		searchObject, getTargetTypes(targetClass));
	}
   }
}
 
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

Principal Consultant bei ORDIX

 

Kommentare

Derzeit gibt es keine Kommentare. Schreibe den ersten Kommentar!
Samstag, 20. April 2024

Sicherheitscode (Captcha)

×
Informiert bleiben!

Bei Updates im Blog, informieren wir per E-Mail.

Weitere Artikel in der Kategorie