Das Spring Framework bietet seit Anbeginn Möglichkeiten ohne große Komplexität oder Aufwand die eigene Anwendung an einen Datenspeicher anzubinden.
Mit Konstrukten wie dem Jdbc
– oder HibernateTemplate
konnte man auf relationale Datenbanken zugreifen und Daten in diesen ablegen.
Mit dem Aufkommen der NoSQL-Datenspeicher stieg die Anzahl an zu unterstützenden Mechanismen enorm an. Um diesen Bedarf gerecht zu werden, wurde das Spring Data Schirm-Projekt eingeführt. Dort werden die Aktivitäten in Bezug auf Datenspeicher-Technologien gebündelt.
Das Projekt umfasst derzeit folgende offizielle Module:
- JPA (Relationale Datenbanken)
- MongoDB
- Neo4J
- Redis
- Solr
- Hadoop
- Gemfire
- JDBC Extensions
Zusätzlich werden durch die Community die Module Couchbase, ElasticSearch, Cassandra und DynamoDB bereitgestellt.
Repositories
Das markanteste Feature von Spring Data ist sicherlich die Repository-Abstraction. Man definiert lediglich ein Interface und gibt durch Generics an auf welche Entität, Dokument etc. sich dieses Repository bezieht. Zusätzlich bestimmt man durch die Wahl des Eltern-Interface über welche Funktionalitäten das Repository bereits verfügen soll.
Sofern ein Modul die Repository-Abstraction unterstützt gibt es für dieses Modul ein entsprechendes Interface das neben Grund-Funktionalitäten wie findOne, findAll, delete und save mit spezifischen Funktionalitäten für den verwendeten Datenspeicher ausgestattet ist. Das im Beispiel verwendete MongoRepository bietet neben den angesprochenen CRUD-Operationen auch Methoden an, die einen Pageable
– oder Sort
-Parameter akzeptieren.
@Document public class Person { @Id protected ObjectId id; public final String name; @PersistenceConstuctor protected Person(ObjectId id, String name) { this.id = id; this.name = name; } public Person(String name) { this(null, name); } }
public interface PersonRepository extends CrudRepository<Person, ObjectId> { }
Die definierten Repositories dienen Spring Data als Rezept um eine Instanz (genauer ein Proxy-Objekt) zu erzeugen. Dieses delegiert die Aufrufe an eine Referenz-Implementierung bei der die den Basis-Interfaces definierten Funktionalitäten implementiert sind.
Nun sollte man sich fragen, warum die Definition eines Interfaces und das Instantiieren eines Proxy-Objekts notwendig ist, wenn es doch bereits eine Referenz-Implementierung gibt, die alle Funktionalitäten bereitstellt. Oftmals hat eine Anwendung nicht immer das Recht Daten in einen Datenspeicher zu schreiben oder Daten aus einem solchen zu lesen. Oder man möchte nur eine eine Mixtur aus Lösch- und Speicher-Funktionalität bereitstellen. Um nun nicht die Gefahr einzugehen, dass jemand genau diese nicht erlaubten oder nicht gewünschten Operationen ausführt, können die Interfaces entsprechend definiert werden. Bei einer Klasse ist man eingeschränkt welche Funktionalität man anbietet, ohne dabei eine mehrstufige Hierarchie aufbauen zu müssen. Eine Mixtur (Mixin) ist, zumindest unter Java, mit immensem Aufwand verbunden, da sämtliche Kombinationen in Betracht gezogen werden müssten.
Der von Spring Data verwendete Mechanismus erlaubt die einfache Definition einer solchen Mixtur. Es müssen lediglich die Methoden-Signatur der gewünschten Funktionalität in das eigene Repository kopiert werden. Wichtig ist, dass das eigene Repository von einem der Basis-Interfaces erbt (Repository
, CrudRepository
usw.). Repository
dient Spring Data als Markierung und muss, falls die anderen Basis-Interface nicht geneignet sind, als Basis-Interface angegeben werden.
Paging & Sorting
Das Basis-Interface PagingAndSortingRepository
erlaubt es beim Abfragen der Ergebnisse die Menge dieser zu beschränken. Dazu definiert das Interface jeweils Methode die einen Pagable
-Parameter bzw. einen Sort
-Parameter akzeptiert. Über Sort
kann gesteuert werden nach welcher Eigenschaft die Ergebnis-Menge sortiert ist. Mit Pageable
kann die Ergebnis-Menge eingeschränkt und ein Offset definiert werden. Zusätzlich hat Pagable
auch eine Sort
-Eigenschaft.
Das Beispiel unten zeigt wie man mit Pageable
eine sortierte Liste abfragt. Die Parameter von Pageable sind: Seite, Anzahl Elemente, die Richtung in der sortiert werden soll und die Attribute nach denen sortiert werden soll. Pro Iteration werden immer 10 Personen aus der Datenbank geladen, bis es keine weiteren Einträge mehr gibt.
@Service public class FunkyPersonService { private final PersonRepository repository; @Autowired public FunkyPersonService(PersonRepository repository) { this.repository = repository; } public List<Person> doFunkyPersonStuff() { final List<Person> persons = Lists.newArrayList(); Pageable pageable = new PageRequest(0, 10, Direction.DESC, "name"); do { final Page<Person> page = repository.findAll(pageable); persons.addAll(page.getContent()); pageable = page.nextPageable(); } while(pageable != null); return persons; } }
Dieses Feature ist vor allem für Web-Anwendungen die über eine Seitennavigation verfügen oder Inhalte stückchenweise laden geeignet. In Kombination mit Spring MVC kann das Pageable
-Objekt direkt aus den Parametern des HTTP-Request erstellt werden. Siehe Spring Data – Web Support.
Query Methods
Oftmals reicht die von den Basis-Interfaces bereitgestellte Abfrage-Funktionalität nicht aus und man muss eigene Abfragen definieren. Die Verwendung von Interfaces beschränkt die Möglichkeit eine eigene Methode für die Abfrage zu definieren stark. Um dennoch die eigene Abfrage deklarieren zu können, bietet die Repository-Abstraction drei Optionen. Die erste Möglichkeit stellt die Definition von sogenannten Query Methods dar. Dabei definiert man eine Methode die einem festen Namenschema folgt. Query Methods beginnen mit „findBy“ gefolgt von einem Attribut-Namen der Entität, Dokuments etc., und optional einer Relation (z.B. In oder Like). Weitere Attribute können mit And oder Or angehangen werden. Für jedes Attribut muss ein Methoden-Parameter existieren.
Die Repository-Abstraction erzeugt aus dem Methodennamen eine für die verwendete Technologie gültige Abfrage. Das folgende Beispiel zeigt welche JPQL-Abfrage aus der definierten Methode resultiert.
Person findByName(String name);
SELECT p FROM Person p WHERE name = :name
Für den Fall, dass die Abfrage etwas komplexer ist, kann die Methode mit @Query
annotiert werden. Als Wert muss eine gültige Abfrage für die jeweils gewählte Technologie angegeben werden.
Diese Methoden können mit einem Pageable
oder Sort
-Parameter versehen werden. Dieser wird auf die Abfrage angewandt.
Custom Repository
Sollten die oben genannten Möglichkeiten nicht für den Anwendungsfall genügen, kann eine eigene Methode implementiert werden. Damit dies funktioniert müssen einige Konventionen eingehalten werden.
- Die zu implementierende Methode muss in einem separaten Interface definiert werden
- Das eigentlich Repository muss dieses Interface erweitern (
extends
) - Es existiert eine Klasse mit dem Namen des Repository und dem Zusatz Impl
- Die Klasse implementiert das zusätzliche Interface
Das folgende Beispiel zeigt das PersonRepository
mit der Erweiterung PersonRepositoryCustom
und der Implementierung.
public interface PersonRepository extends MongoRepository<Person, ObjectId>, PersonRepositoryCustom { Person findByName(String name); } public interface PersonRepositoryCustom { long countByName(String name); } public class PersonRepositoryImpl implements PersonRepositoryCustom { private final MongoTemplate template; @Autowired public PersonRepositoryImpl(MongoTemplate template) { this.template = template; } @Override public long countByName(String name) { final Query q = new Query(Criteria.where("name").is(name)); return template.count(q, Person.class); } }
Alle hier angesprochenen Features können in der Referenz von Spring Data nachgeschlagen werden und sind dort weitreichend beschrieben. In den nächsten Beiträgen zu Spring Data werde ich auf die einzelnen Module eingehen und mich deren Besonderheiten und möglichen Anwendungsszenarien widmen. Zudem zeige ich, wie die Anwendung konfiguriert werden muss, damit Spring Data verwendet werden kann.
Eine Antwort