Hibernate 4.x mit Spring 3.x

Eingetragen bei: Hibernate, Java, spring | 0

Der nachfolgende Artikel beschreibt die Verwendung des ORM – Object-Relational Mapping – Frameworks Hibernate 4 inklusive der Hibernate Integration von Spring 3, wobei letztere auch die Vorgängerversion Hibernate 3 unterstützt. Als Persistenz-Schicht wird die In-Memory Datenbank HSQLDB verwendet. Es entfällt also die Installation einer Datenbank, da für den Einsatz der HSQLDB lediglich die entsprechende Library eingebunden werden muss.

In diesem Beispiel sollen Benutzer- und Adressdaten persistiert werden, wobei einem Benutzer-Eintrag beliebig viele Adress-Informationen zugewiesen werden können. Es gibt also ein 1..n Mapping zwischen Benutzer und Adressen. Weiterhin wird ein DAO – Data Access Object – implementiert, welches die CRUD – Create/Read/Update/Delete – Datenbank Operationen für die Haupt-Entität (Benutzer) zur Verfügung stellt.

Im ersten Schritt wird ein Maven-Projekt erstellt, in dessen pom.xml die folgenden Dependencys ergänzt werden:

<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-core</artifactId>
<version>4.1.7.Final</version>
</dependency>

<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>2.2</version>
</dependency>

<dependency>
<groupId>org.hsqldb</groupId>
<artifactId>hsqldb</artifactId>
<version>2.2.9</version>
</dependency>

<dependency>
<groupId>commons-dbcp</groupId>
<artifactId>commons-dbcp</artifactId>
<version>1.4</version>
</dependency>

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>3.1.0.RELEASE</version>
</dependency>

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>3.1.0.RELEASE</version>
</dependency>

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-orm</artifactId>
<version>3.1.0.RELEASE</version>
</dependency>

Zuerst werden die Entitäten erstellt, welche die Tabellen der relationalen Datenbank als Java-Klassen repräsentieren. Da der Benutzer Kenntnis über die zu ihm gehörenden Adressen benötigt, umgekehrt aber keine Verknüpfung notwendig ist, kennt die Adress-Entität lediglich sich selber. Ein guter Grund mit dieser zu beginnen:

@Entity
public class Address {

@Id
@GeneratedValue
private Long id;
private String street;
private String city;

// getter setter methods

}

Die Klasse Address wird mit der Annotation Entity versehen, um sie als solche zu kennzeichnen. Weiterhin erhält sie neben den benötigten Eigenschaften (Straße, Stadt, …) einen Identifier. Die entsprechende Annotation definiert diesen als Primär-Schlüssel, während GeneratedValue dafür sorgt, dass sein Wert automatisch von der Datenbank vergeben werden soll.

Nun kann auch die Klasse User mit Zugriff auf die Adress-Datensätze erzeugt werden:

@Entity
public class User {

@Id
@GeneratedValue
private Long id;
private String forname;
private String lastname;

@OneToMany
@Cascade(value=CascadeType.ALL)
private List<Address> addresses;

// getter setter methods

}

Da ein User eine beliebige Anzahl an Adressen erhalten kann, wird hier als Mapping die OneToMany-Annotation verwendet. Weiterhin wird der CascadeType ALL gewählt, der unter anderem die CascadeTypen SAVE_UPDATE und DELETE beinhaltet. Hiermit wird erreicht, dass beim Speichern des Users gleichzeitig seine Adress-Daten persistiert werden. Wäre der CascadeTyp SAVE_UPDATE nicht gewählt, müssten Adress- und Benutzerdaten separat gespeichert werden, wobei ein Benutzer nur auf Adressen verweisen darf, die bereits in der Datenbank existieren. Der CascadeType DELETE sorgt dafür, dass beim Löschen eines Benutzers auch seine Adressen aus der Datenbank entfernt werden.

Wie man an obigem Beispiel erkennt, gibt es Attribute die sich in den Entitäten wiederholen. In diesem Beispiel ist es nur der Identifier, der sowohl beim User als auch bei der Adresse vorkommt. Aber auch das Erstellungs- und letztes Änderungsdatum einer Entität wären Kandidaten, die zu Code-Wiederholung führen könnten. Doppelte Attribute können in eine gemeinsame Oberklasse ausgelagert werden, sofern die Oberklasse mit MappedSuperclass annotiert wird:

@MappedSuperclass
public abstract class AbstractEntity {

@Id
@GeneratedValue
private Long id;

// getter setter methods
}

Nachdem die Entitäten erstellt wurden, soll nun der UserDAO erzeugt werden, der sukzessive um die CRUD-Operationen erweitert wird. Hierfür benötigt er lediglich die SessionFactory. Wer bislang das HibernateTemplate verwendete, wird feststellen, dass dies unter Hibernate 4 nicht mehr unterstützt wird. Das ist nicht weiter verwunderlich, da dessen Verwendung bereits seit Hibernate 3.0.1 obsolet ist. Stattdessen kann bei gültigem Transaktions-Management über die SessionFactory auf die aktuelle Session zugegriffen und auf dieser Datenbank-Operationen ausgeführt werden. Auf die Konfiguration des Transaktions-Management wird später noch eingegangen. Aber zurück zum UserDAO, der zunächst nur aus folgendem Rumpf besteht:

public class UserDAO {

private SessionFactory sessionFactory;

// methods for crud operations
// and a setter method for the sessionFactory
}

Wenden wir uns nun der Spring Konfiguration zu, die in der Datei src/main/resources/applicationContext.xml erstellt wird. Zunächst muss die Verbindung zur Datenbank in einer Datasource konfiguriert werden. Es werden Details zum Datenbanktreiber, URL zur Datenbank, Benutzername und Passwort benötigt.

<bean id=“dataSource“ class=“org.apache.commons.dbcp.BasicDataSource“ destroy-method=“close“>
<property name=“driverClassName“ value=“org.hsqldb.jdbcDriver“/>
<property name=“url“ value=“jdbc:hsqldb:mem:myDB“/>
<property name=“username“ value=“sa“/>
<property name=“password“ value=““/>
</bean>

Bei Verwendung einer HSQLDB kommt der Treiber org.hsqldb.jdbcDriver zum Einsatz. Die URL besteht aus drei Teilen, wobei der erste für alle HSQLDBs gleich ist und jdbc:hsqldb: lauten muss. Der Prefix mem: signalisiert, dass es sich um eine In-Memory Datenbank handelt, während der letzte Teil den Namen der zu verwendeten Datenbank – myDB – bestimmt.

Will man später von einer In-Memory Datenhaltung auf eine dauerhafte Persistierung wechseln, kann die Datasource ersetzt und hier die Verbindungsdaten an die neue Datenbank angepasst werden.

Nun soll die SessionFactory definiert werden, die der UserDAO für die Datenbank-Operationen benötigt:

<bean id=“sessionFactory“ class=“org.springframework.orm.hibernate4.LocalSessionFactoryBean“>
<property name=“dataSource“ ref=“dataSource“/>
<property name=“packagesToScan“>
<list>
<value>de.mz.entity</value>
</list>
</property>
<property name=“hibernateProperties“>
<props>
<prop key=“hibernate.dialect“>org.hibernate.dialect.HSQLDialect</prop>
<prop key=“hibernate.hbm2ddl.auto“>create</prop>
<prop key=“hibernate.show_sql“>true</prop>
</props>
</property>
</bean>

Die SessionFactory braucht Zugriff auf die bereits definierte Datasource und verwaltet alle verfügbaren Entitäten. Hierzu muss entweder eine Liste aller annotierten Klassen – User und Address – über das Property annotatedClasses angegeben werden oder es wird, wie in diesem Beispiel, eine Liste von zu scannenden Packages übergeben. Anschließend können noch Hibernate-Eigenschaften konfiguriert werden, wobei die Angabe des Datenbank-Dialekts zwingend nötig ist. Bislang hat nur die Datasource Informationen über die zu konnektierende Datenbank. Aber auch Hibernate muss wissen, um welche Datenbank es sich handelt, damit die entsprechende Kommunikation gewählt werden kann. Für eine HSQLDB wird der Dialekt org.hibernate.dialect.HSQLDialect verwendet. Die weiteren Einstellungen können die Entwicklung erheblich erleichtern. So sorgt die Property hibernate.hbm2ddl.auto mit dem Wert create dafür, dass Hibernate die Datenbank-Struktur mit Tabellen, Primär- und Fremdschlüsseln automatisch anlegt. Erhält hibernate.show_sql den Wert true, wird jedes von Hibernate ausgeführte SQL-Statement geloggt und kann so nachvollzogen werden. Dies ist zwar für eine Live-Umgebung tödlich, kann aber beim Aufsetzten eines neuen Projektes durchaus hilfreich sein.

Wie bereits erwähnt, wird ein Transaktion-Management benötigt, damit die SessionFactory den UserDAO mit einer gültigen Session versorgen kann. Die Transaktionen sollen dabei über Annotationen definierbar sein. Zunächst wird der HibernateTransactionManager konfiguriert, der Zugriff auf die SessionFactory benötigt:

<bean id=“transactionManager“ class=“org.springframework.orm.hibernate4.HibernateTransactionManager“>
<property name=“sessionFactory“ ref=“sessionFactory“ />
</bean>

Um die Steuerung von Transaktionen mit der Annotation Transactional zu ermöglichen, muss dieses Verhalten aktiviert werden:

<tx:annotation-driven/>

Sofern der Name des TransactionManagers nicht transactionManager lauten würde, müsste hier noch der entsprechende Name angegeben werden:

<tx:annotation-driven transaction-manager=“transactionManager“/>

Zu guter Letzt muss der UserDAO in der Spring-Konfiguration aufgenommen werden und Zugriff auf die SessionFactory erhalten:

<bean id=“userDAO“ class=“de.mz.dao.UserDAO“>
<property name=“sessionFactory“ ref=“sessionFactory“ />
</bean>

Nun wird der UserDAO um die erste CRUD-Operationen erweitert, wobei mit dem Speichern eines Benutzers begonnen wird:

@Transactional
public User save(User user) {
Session session = sessionFactory.getCurrentSession();
session.saveOrUpdate(user);
return user;
}

Die save-Methode wird mit der Annotation Transactional versehen. Beim Aufruf der Methode wird nun eine Transaktion erzeugt, die erst beim Verlassen wieder geschlossen wird, womit dann die Datenbank-Änderungen wirksam werden. Träte zuvor eine Exception auf, so fände keine der Datenbank-Operationen der save-Methode statt. Weiterhin sorgt die Transaktion dafür, dass die SessionFactory eine gültige Session liefert, über die der Benutzer gespeichert werden kann.

Durch die Verwendung der saveOrUpdate-Methode kann die gleiche Methode zum Anlegen und Aktualisieren von Benutzerdaten verwendet werden. Besitzt der User keine Id, vergibt Hibernate den Primär-Schlüssel und führt das SQL-Statement Insert aus, andernfalls wird ein Update ausgeführt.

Anschließend wird zum Testen der Anwendung eine Klasse mit ausführbarer main-Methode erstellt, die den Spring-Context hochfährt und den UserDAO aus selbigem lädt:

ApplicationContext context = new ClassPathXmlApplicationContext(„applicationContext.xml“);
UserDAO userDAO = (UserDAO) context.getBean(„userDAO“);

Nun wird ein Benutzter mitsamt zugehöriger Adresse erstellt und über den UserDAO gespeichert:

List<Address> addresses = new ArrayList<Address>();
Address address = new Address();
address.setCity(„Cologne“);
addresses.add(address);

User user = new User();
user.setForname(„Max“);
user.setLastname(„Mustermann“);
user.setAddresses(addresses);

userDAO.save(user);

Dank der Hibernate-Einstellung show_sql lässt sich in den Log-Meldungen gut nachvollziehen, was für SQL-Statements Hibernate durchführt. Zuerst werden die Datenbank-Tabellen erstellt:

Hibernate: create table Address (id bigint generated by default as identity (start with 1), city varchar(255), street varchar(255), primary key (id))
Hibernate: create table User (id bigint generated by default as identity (start with 1), forname varchar(255), lastname varchar(255), primary key (id))
Hibernate: create table User_Address (User_id bigint not null, addresses_id bigint not null, unique (addresses_id))

Die Tabellen Address und User entsprechen unseren Java-Entitäten, während die Tabelle User_Address als Mapping-Tabelle zwischen Benutzer und Adressen fungiert. Anschließend werden der Mapping-Tabelle per constraints Fremdschlüssel auf die Haupt-Entitäten hinzugefügt:

Hibernate: alter table User_Address add constraint FK838B1F807A26EF9A foreign key (User_id) references User
Hibernate: alter table User_Address add constraint FK838B1F805ABA690C foreign key (addresses_id) references Address

Als nächstes werden nun die Benutzerdaten gespeichert. Es lässt sich gut nachvollziehen wie aufgrund des CascadeTypes SAVE_UPDATE der Benutzer und die dazugehörigen Adressen gespeichert werden. Natürlich darf auch der Eintrag in der Mapping-Tabelle nicht fehlen:

Hibernate: insert into User (id, forname, lastname) values (default, ?, ?)
Hibernate: insert into Address (id, city, street) values (default, ?, ?)
Hibernate: insert into User_Address (User_id, addresses_id) values (?, ?)

Die ersten zwei CRUD-Operationen wurden umgesetzt, als nächstes sollen Benutzer-Daten aus der Datenbank ausgelesen werden. Hierzu wird der UserDAO um die find-Methode erweitert:

@Transactional
public List<User> find() {
Criteria criteria = sessionFactory.getCurrentSession().createCriteria(User.class);
List<User> users = criteria.list();
return users;
}

Hibernate bietet verschiedene Möglichkeiten, um Datenbank-Abfragen umzusetzen, so kann auf einer Session ein HQL- (Hibernate Query Language) oder SQL-Statement ausgeführt oder wie in diesem Beispiel eine Criteria erstellt werden. Criterias bieten die Möglichkeit Objekt-Orientierte-Abfragen formulieren zu können. Wie auf einer Criteria Einschränkungen definiert werden können, wird im Folgenden noch erläutert. Zunächst soll nur eine Liste aller Benutzer zurückgegeben werden.

Bei der Verarbeitung der Ergebnisliste ist zu beachten, dass Hibernate verschiedene FetchMode zum Umgang mit verknüpften Entitäten bietet. Per Default ist dies Lazy-Loading, wobei verknüpfte Entitäten nur geladen werden, sofern auf sie zugegriffen wird. In diesem Beispiel werden die Adress-Datensätze nicht geladen, da innerhalb der Transaktion kein Zugriff stattfindet. Nach Abschluss der Transaktion – also beim Verlassen der find-Methode – kann nicht mehr auf sie zugegriffen werden, ohne dass eine LazyLoadingException geworfen wird:

org.hibernate.LazyInitializationException: could not initialize proxy – no Session
at org.hibernate.collection.internal.AbstractPersistentCollection.initialize(AbstractPersistentCollection.java:430)
at org.hibernate.collection.internal.AbstractPersistentCollection.read(AbstractPersistentCollection.java:121)
at org.hibernate.collection.internal.PersistentBag.iterator(PersistentBag.java:266)

Wenn die Transaktion im verarbeitenden Service gestartet würde – nicht wie hier im UserDAO – wäre der Zugriff auf die entsprechenden Adress-Daten möglich. Hierbei wird der Vorteil des Lazy-Loadings ausgenutzt, es werden nur Daten geladen, die auch tatsächlich benötigt werden. Bei verteilten Systemen ist dies natürlich nicht unbedingt so möglich. Benötigt ein Client Adress-Informationen, die im Server geladen werden, so ist hier die Session bereits geschlossen. Daher kann für Entity-Verknüpfungen, die zwangsläufig mit der Haupt-Entität geladen werden sollen, der FetchMode Eager-Loading verwendet werden. Hier werden beim Laden des Benutzers direkt alle Adressen mitgeladen. Dies wird in der Annotation OneToMany definiert, die zusätzlich das Attribut fetch erhält. Die Property addresses der Klasse User besitzt nun folgende Annotationen:

@OneToMany(fetch=FetchType.EAGER)
@Cascade(value=CascadeType.ALL)
private List<Address> addresses;

Für große Daten-Mengen, die nicht immer benötigt werden, bietet sich dieses Verfahren natürlich nicht an. Hier kann das DTO – Data Transfair Object – Pattern zum Einsatz kommen. Hierbei wird nicht die User-Entity selber zum Client geschickt, sondern ein UserDTO, welches aber die gleichen Attribute besitzt. Bei der Anforderung der Daten werden die mitzuladenden Abhängigkeiten der Serverseite mitgeteilt, so dass die Antwort alle benötigten Daten in Form von DTOs beinhaltet. In unserem Beispiel würden beim Server die Benutzerdaten mitsamt der Adress-Datensätze angefragt werden. Sowohl die User– als auch die Adress-Daten werden in DTOs umgewandelt, wobei das UserDTO die Adressen in Form einer Liste von AddressDTOs besitzt. Da die Konvertierung innerhalb einer gültigen Session stattfindet, kommt es bei diesem Verfahren zu keiner LazyLoadingException und es werden nur die Daten geladen, die der Client auch tatsächlich benötigt.

Der Nachteil dieses Verfahrens liegt auf der Hand, bei jeder Client-Server-Kommunikation müssen DTOs in Entitäten und anschließend wieder zurück in DTOs konvertiert werden. Außerdem gibt es für jede Entität eine entsprechendes DTO, was zu dupliziertem Code führt. Der Vorteil der DTOs ist die Kapselung, da die Datenschicht vor dem Client-System versteckt wird.

Aber zurück zum Beispiel. Die find-Methode soll nicht mehr alle Benutzer liefern, sondern nur solche, deren Nachname Mustermann lautet. Hierfür wird auf der Criteria eine entsprechende Einschränkung definiert:

Criteria criteria = sessionFactory.getCurrentSession().createCriteria(User.class);
criteria.add(Restrictions.eq(„lastname“, „Mustermann“));
List<User> users = criteria.list();

Der Criteria wird hierzu eine Restriction hinzugefügt, deren erster Parameter den Namen der Property erhält, der zweite den zu prüfenden Wert. Die Restriction besitzt verschiedene Prüfungs-Methoden. Hier wurde auf Gleichheit getestet – equals – aber auch die anderen aus der SQL-Syntax bekannten Prüf-Operationen wie like, between usw. stehen zur Verfügung. Anschließend wird die Criteria wie gehabt ausgeführt und man erhält eine passende Liste von Benutzern.

Es sind aber nicht nur Suchkriterien auf dem Benutzer möglich, es können auch Einschränkungen auf der Adresse festgelegt werden. So interessieren im Folgenden nur Benutzer, die aus Köln kommen:

Criteria criteria = sessionFactory.getCurrentSession().createCriteria(User.class);
Criteria addressCiteria = criteria.createCriteria(„addresses“);
addressCiteria.add(Restrictions.eq(„city“, „Cologne“));
List<User> users = criteria.list();

Auf der bereits erstellen UserCriteria, wird eine weitere Criteria erzeugt. Der übergebene String enthält hierbei den Namen der Property aus der User-Entität, die auf die entsprechende Tabelle verweisst – in diesem Beispiel also addresses. Über die neue Criteria können nun Suchkritieren auf der Adressen definiert werden. Die list-Methode wird allerdings weiterhin auf der UserCriteria ausgeführt, da als Ergebnis-Menge eine Liste von Benutzern und keine Adressen erwartet werden.

Soweit ein kleiner Einblick in die Welt der Criterias. Bereits einen vorangegangenen Artikel – http://martinzimmermann1979.wordpress.com/2012/03/20/verwendung-einer-having-bedingung-bei-hibernate-criterias/ – widmete ich diesem Thema, wobei hier die Having-Bedingung im Vordergrund stand.

Aber zurück zum Thema, noch ist der UserDAO nicht vollständig, da die Lösch-Funktion fehlt:

@Transactional
public void delete(User user) {
sessionFactory.getCurrentSession().delete(user);
}

Diese ist denkbar einfach zu realisieren. Es muss lediglich auf der aktuellen Session die delete-Methode aufgerufen werden, die als Parameter die entsprechende Entität erwartet. Es mag merkwürdig erscheinen, dass zunächst die ganze Entität geladen werden muss, nur um sie anschließend zu löschen. Alternativ könnte eine HQL-Query zum Löschen des Benutzers verwendet werden, aber auch hier wird der Benutzer zuerst in den Hibernate internen Cache geladen, bevor er gelöscht wird. Die einzige Möglichkeit die obige Lösch-Funktion performanter zu gestalten, wäre das Laden und Löschen des Benutzers in derselben Session.

Der Artikel ist für den Einstieg in Hibernate und für das initiale Aufsetzten eines Hibernate Projektes mit Spring Integration gedacht. Es wurde der Einsatz der CRUD-Operationen und ein einfaches Mapping zwischen zwei Enitäten erläutert. Wirklich knifflig wird es erst anschließend, wenn es an das Mapping von Entitäten geht. Welche Entitäten kennen sich untereinander, wie werden sie miteinander verknüpft, welchen CascadeType und welchen FetchMode erhält welche Verbindung. Diese Fragen müssen für jedes Projekt und jede Anwendung einzeln Entschieden werden.

Verwendung einer HAVING-Bedingung bei Hibernate Criterias

Eingetragen bei: Hibernate, Java | 0

Ist bei HQL – Hibernate Query Language – die Benutzung von Having-Bedingungen
erlaubt, stehen diese bei Criteria-Queries leider nicht zur Verfügung.
Hier kann man sich aber mit einer Unter-Abfrage behelfen.

Kommen wir zu einem konkreten Beispiel, welches im Bereich Benutzerberechtigung
angeordnet ist. Es werden die Datenbank-Tabellen User und UserRole benötigt.
Jedem Benutzer kann hierbei genau eine Rolle, z.B. Administrator, Gast oder Ähnliches, zugewiesen werden.

Umgekehrt steht jede definierte Rolle keinem, einem oder mehreren Benutzern zur Verfügung.
Aus Sicht des Benutzers handelt es sich also um eine ManyToOne-Verbindung.

Hier nun die User-Entity:

@Entity
public class User {
@Id
private Long id
private String name;
@ManyToOne(optional = true, fetch = FetchType.EAGER)
@JoinColumn(name = „roleId“)
private UserRole role;

// … getter and setter …
}

Die Definition des Mappings besagt, dass die Tabelle User eine Spalte roleId enthält, welche die Id einer UserRole enthalten kann. Hiermit wird dem Benutzer also die jeweilige UserRole zugewiesen.
Aus Java Sicht hat man Zugriff auf die komplette UserRole-Entity, wobei der FetchMode EAGER dafür sorgt, dass die UserRole – sofern vorhanden – beim Laden des Users mit geladen wird.

Die UserRole sieht recht unspektakulär aus:

@Entity
public class UserRole {
@Id
private Long id;
private String role;

// … getter and setter
}

Nun möchte man alle UserRoles erhalten, die mehr als einem Benutzer zugewiesen sind.
Das SQL-Statement hierzu sähe dann so aus:

SELECT roleId FROM User GROUP BY roleId Having COUNT(*) > 1;

Da Criteria-Queries keine HAVING-Bedingungen unterstützten, muss die Query umformuliert werden:

SELECT * FROM User outer WHERE 1 <
(SELECT COUNT(*) FROM User inner WHERE inner.roleId = outer.roleId);

Das SQL-Statement wird also in zwei Abfragen aufgeteilt. Der innere Teil ist dafür zuständig, die Anzahl an Benutzern mit der entsprechenden Rolle zu liefern. In der äußeren Query wird überprüft, ob die Anzahl größer als Eins ist, womit das Ergebnis der beiden Abfragen dasselbe ist.

Diese abgewandelte Abfrage kann nun auch mit Criterias abgebildet werden:

DetachedCriteria innerQuery = DetachedCriteria.forClass(User.class, „inner“);
innerQuery.setProjection(Projections.rowCount());
innerQuery.add(Restrictions.eqProperty(„role.id“, „outer.role.id“));

DetachedCriteria criteria = DetachedCriteria.forClass(User.class, „outer“);
criteria.add(Subqueries.lt(1L, innerQuery));

List<User> list = hibernateTemplate.findByCriteria(criteria);

Zunächst wird die innere Abfrage als DetachedCriteria erstellt, wobei ein alias „inner“ verwendet wird, um die innere und äußere Query unterscheiden zu können. Anschließend wird eine Projektion auf die Anzahl der Spalten definiert, da in der inneren Abfrage nur die Anzahl der Rollen entscheidend ist. Mittels einer Restrictions wird auf Gleichheit der roleId von äußerer und innerer Query geprüft. Der Alias „inner“ kann hierbei weggelassen werden, da die Restriction schließlich auf der inneren Abfrage ausgeführt wird. Um die roleId der äußeren Query zu verwenden, muss explizit das Alias „outer“ angegeben werden.

Wie im SQL-Statement auch, überprüft nun die äußere Abfrage, ob Eins kleiner ist, als die in der inneren Abfrage erhaltene Anzahl. Trifft diese Bedingung zu, so erscheint der entsprechende User-Eintrag in Ergebnis.

Auch wenn Hibernate Criterias die HAVING-Bedingung nicht direkt unterstützen, können entsprechende Queries durch die Verwendung von Unter-Abfragen so umformuliert werden, dass nicht zwangsläufig HQL eingesetzt werden muss. Auch der Übersichtlichkeit tut die objektorientierte Schreibweise keinen Abbruch.

Aufsetzten eines CQRS Projektes mit dem Axon-Framework

Eingetragen bei: Java | 0

Das Axon-Framework ist eine Implementierung des CQRS – Command Query Responsiblity Segregation – Pattern, welches zur Erstellung von skalierbaren und erweiterbaren Software-Architekturen dient. Zum Einsatz kommen hier Commands, Events und Querys, die in verschiedenen Bereichen der Software-Architektur verwendet werden.

Will der Benutzer über die UI mit dem System interagieren, stehen zunächst nur Commands zur Verfügung, die mit Hilfe von einem CommandBus ausgeführt werden. Für die Darstellung von Informationen werden wiederum Querys verwendet, die unveränderbare DTOs – Data Transfer Objects – liefern. Für diesen Teilbereich sind im Axon-Framework keine Implementierungen verfügbar, diese müssen aufgrund verschiedenster Einsatzbereiche selbst erstellt werden.

Die Commands werden in den CommandHandlern behandelt, die sich in einem von der UI getrennten Bereich der Software-Architektur befinden.

Hier können nun entweder direkt Events über einen EventBus verbreitet werden, wobei im Gegensatz zu Commands mehrere EventHandler auf ein Event reagieren können. Die Alternative zur direkten Verwendung von Events ist die Benutzung von Aggregates. Diese können entweder neu erzeugt und anschließend in einem Repository gespeichert werden oder es wird ein bestehendes Aggregate aus einem Repository geladen. Erst nachdem alle Operationen auf einem Aggregate ausgeführt wurden, werden die dazugehörigen Events veröffentlicht und von den EventHandlern verarbeitet. Dies passiert automatisch, sofern das Aggregate aus einem Repository geladen wurde, bzw. sobald ein neu erzeugtes Aggregate gespeichert wurde.

Die EventHandler sind unter anderem für die Speicherung von Daten in einer Datenbank zuständig. Dem Benutzer werden über diesen Weg allerdings keine Daten angezeigt, sondern lediglich manipuliert. Zum Anzeigen von Datenbank-Inhalten werden die anfangs erwähnten Querys und DTOs verwendet.

Kommen wir nun zu einem Beispiel, bei dem zunächst ein Benutzer angelegt und in einem zweiten Schritt manipuliert werden. Es wird hierbei ein UserAggregate eingesetzt, d.h. die entsprechenden CommandHandler Arbeiten nicht direkt mit Events, sondern verwenden stattdessen das Domain Model.

Zunächst einmal wird folgende Dependency benötigt:

<dependency>
<groupId>org.axonframework</groupId>
<artifactId>axon-core</artifactId>
<version>1.0</version>
</dependency>

Für die Integration des Axon-Frameworks in die Anwendung wird in diesem Beispiel Spring verwendet, wobei außer den Spring Bibliotheken keine weiteren Abhängigkeiten benötigt werden.

Hierfür wird der AnnotationCommandHandlerBeanPostProcessor zur Erkennung von Java-Beans mit der CommandHandler-Annotation, der CommandBus und zwei CommandHandler im Spring Kontext registriert. Letztere dienen zum Anlegen bzw. Aktualisieren des Benutzers.

<bean class=“org…AnnotationCommandHandlerBeanPostProcessor“ />

<bean id=“commandBus“ class=“org…SimpleCommandBus“/>
<bean id=“createHandler“ class=“..CreateUserHandler“ />
<bean id=“updateHandler“ class=“..UpdateUserHandler“ />

Der CreateUserHandler erzeugt zunächst ein neues UserAggregate, führt auf diesem die save-Operation aus und speichert es im UserRepository. Events die beim Erzeugen und innerhalb der save-Operation generiert werden, werden erst publiziert wenn das UserAggregate dem Repository mittels add hinzugefügt wird. Es folgt die Implementierung des CreateUserHandlers, der auf das CreateUserCommand reagiert:

@Component
public class CreateUserHandler {

@Autowired
private UserRepository userRepository;

@CommandHandler
public void handle(CreateUserCommand command) {
UserAggregate userAggregate = new UserAggregate(command.getUser());
userAggregate.save(command.getUser());
userRepository.add(userAggregate);
}
}

Das CreateUserCommand wird auf dem CommandBus ausgeführt, welcher mit Hilfe der Inject-Annotation in die gewünschte Klasse injiziert wird. Das Axon-Framework sorgt dafür, dass der gewünschte CommandHandler mit dem Command  als Übergabe-Parameter aufgerufen wird. Zum Ausführen des Commands wird folgende Zeile benötigt:

commandBus.dispatch(new CreateUserCommand(new User()));

Nun wird das UserAggregate erzeugt, dass von der Klasse AbstractEventSourcedAggregateRoot erbt. Wird ein UserAggregate erzeugt, wird gleichzeitig ein AggregateIdentifier angelegt, anhand dessen das Aggregate beim Speichern im UserRepository identifiziert wird. Jede Änderung des Aggregates die im Repository gespeichert wird, erhält eine neue Version, die von der eigenen Repository Implementierung gespeichert werden kann. Neben der Erzeugung des Identifiers wird im Konstruktor ein Event angelegt, dass in der zu implementierenden handle-Methode behandelt wird. Die Events, die in diesem Beispiel verwendet werden, erben alle vom DomainEvent, welches hier als Übergabe-Parameter dient.

Im CommandHandler wurde weiterhin die save-Methode aufgerufen, auch diese wird hier implementiert, wobei ein weiteres Event registriert wird. Hier nun der Beispiel-Code:

public class UserAggregate extends AbstractEventSourcedAggregateRoot {

public UserAggregate(User user) {
super(new UserAggregateIdentifier(user.getDsn()));
apply(new UserCreatedEvent(user));
}

@Override
protected void handle(DomainEvent event) {
// do what you have to do with the created user
}

public void save(User user) {
registerEvent(new UserSaveEvent(user));
}
}

Wie anfangs beschrieben, werden die Events erst ausgeführt, sobald das Aggregate im Repository gespeichert wird.

Folgendermaßen kann nun das UserRepositorys aussehen:

public class UserRepository extends AbstractRepository{

@Override
protected UserAggregate doLoad(AggregateIdentifier aggregateIdentifier, Long version) {
// load aggregate from a map for example
}

@Override
protected void doSave(UserAggregate aggregate) {

// save aggregate in a map for example
}
}

Im CommandHandler wird beim Aufruf der add-Methode des Repositorys die abstrakte doSave-Methode ausgeführt und das Aggregate gespeichert. Anschließend werden die registrierten Events ausgeführt. Hierbei wird das UserCreateEvent im Aggregate selber behandelt, dass UserSaveEvent aber im folgenden SaveEventListener, welcher lediglich zum Speichern des im Event befindlichen Users in der Datenbank zuständig ist:

@Component
public class SaveEventListener {

@Autowired
private HibernateTemplate hibernateTemplate;

@EventHandler
public void handle(UserSaveEvent event) {
hibernateTemplate.saveOrUpdate(event.getUser());
}
}

Für die Behandlung von Events sind folgende Spring-Konfigurationen vorzunehmen:

<bean class=“class=“org..AnnotationEventListenerBeanPostProcessor“ />
<bean id=“eventBus“ class=“org..SimpleEventBus“ />
<bean id=“updateListener“ class=“..SaveEventListener“ />

Interessant dürfte noch die Umsetzung eines UpdateUserHandler sein, da hierfür ein bestehendes Aggregate aus dem UserRepository geladen wird:

@Component
public class UpdateUserHandler {

  @Autowired
  private UserRepository userRepository;

  @CommandHandler
  public void handle(UpdateUserCommand command) {
    UserAggregate userAggregate = userRepository.load(new UserAggregateIdentifier(command.getIdentifier()), command.getVersion());
    userAggregate.save(command.getUser());
  }
}

Nachdem die save-Operation mit aktualisierten Benutzer-Daten aufgerufen wurde, muss das Aggregate nicht erneut im Repository gespeichert werden. Das Axon-Framework erkennt automatisch, dass es sich hierbei um eine neue Version des geladenen UserAggregates handelt und führt nach vollständigem Durchlauf des CommandHandlers alle registrierten Events aus.

In diesem Fall wird erneut das UserSaveEvent aufgerufen, der entsprechende Listener ausgeführt und damit die Änderung des Benutzers in der Datenbank gespeichert.

Dieses Beispiel soll einen Einblick in die Funktionsweise von CQRS und das Axon-Framework liefern, wobei einige Teilbereiche, wie z.B. der EventStore, ungenannt blieben. Ein Blick in den Reference-Guide sei allemal empfohlen.