Eine Spring Data Anwendung mit einem Apache Cassandra Backend

Eine Spring Data Anwendung mit einem Apache Cassandra Backend ist recht einfach zu erstellen, da Spring Data hierfür ein eigenes Subprojekt etabliert hat. Weiterhin gibt es selbstverständlich einen Embedded Cassandra Server als Bestandteil von Spring Boot. Ein externer Server ist kann über einen Docker-Container realisiert werden, das offizielle Docker-Image liegt auf dem Docker Hub.  

Cassandra im Docker-Container

Für eine einfache Test-Anwendung wird eine einzelne Cassandra-Instanz über den Aufruf 

docker run --rm --name cassandra -p 9142:9142 cassandra

gestartet. Damit läuft ein einzelner Cassandra-Knoten.

Nach dem Starten des Containers kann eine Konsole im Container geöffnet werden, mit deren Hilfe die Cassandra Shell cqlsh geöffnet werden kann:

docker exec --it cassandra /bin/bash

cqlsh

Damit kann dann für das folgende Beispiel nötige KeySpace sowie die Tabelle angelegt werden:

CREATE KEYSPACE PUBLISHING_KEYSPACE
WITH REPLICATION = {
'class': 'SimpleStrategy',
'replication_factor': 1
};

CREATE TABLE PUBLISHING(
publisher_name text,
title text,
isbn text,
price double,
pages int,
available boolean,
primary key(publisher_name, title, pages)
);

 

Die Spring Boot Anwendung

Wie üblich ist der Einstiegspunkt in die Spring Boot-Anwendung die pom.xml. Diese definiert neben dem Spring Boot Parent den Starter für Cassandra:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.8.RELEASE</version>
<relativePath /> <!-- lookup parent from repository -->
</parent>
<groupId>org.javacream.training</groupId>
<artifactId>org.javacream.training.apache.cassandra.springboot</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>org.javacream.training.apache.cassandra.springboot</name>
<description>Cassandra project for Spring Boot</description>

<properties>
<java.version>1.8</java.version>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-cassandra</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>

Damit ist die Konfiguration der Anbindung an den im Docker-Container laufenden Datenbankserver einfach:

@EnableCassandraRepositories
@Configuration
public class PublishingConfiguration

}

und die application.properties:

spring.data.cassandra.port=9142
spring.data.cassandra.keyspace-name=PUBLISHING_KEYSPACE

Entity und Repository

Die in der Cassandra-Datenbank abzulegende Datenstruktur wird über Annotationen gemapped:

@Table
public class Book {

@PrimaryKeyColumn(name = "publisher_name", ordinal = 0, type = PrimaryKeyType.PARTITIONED)
private String publisherName;
@PrimaryKeyColumn(name = "pages", ordinal = 2, type = PrimaryKeyType.CLUSTERED)
private int pages;
private double price;
private String isbn;
private boolean available;
@PrimaryKeyColumn(name = "title", ordinal = 1, type = PrimaryKeyType.CLUSTERED)
private String title;
//getter, setter, hashCode, equals, toString

}

Das Repository selbst ist dann eine einfache Schnittstellen-Erweiterung des CassandraRepository:

@Repository
public interface PublishingRepository extends CassandraRepository<Book, String>{

}

Die Anwendung

Die Anwendung selber schreibt nun nur 4 Bücher in die Datenbank:

@Component
public class PublishingApplication {

@Autowired private PublishingRepository publishingRepository;

@PostConstruct public void startApplication() {
Book cassandraInAction1 = new Book("Manning", 100, 19.99, "ISBN-1", false, "Cassandra in Action");
publishingRepository.save(cassandraInAction1);
Book cassandraInAction2 = new Book("Manning", 200, 29.99, "ISBN-2", true, "Cassandra in Action");
publishingRepository.save(cassandraInAction2);
Book cassandraInAction3 = new Book("Manning", 500, 39.99, "ISBN-3", true, "Cassandra in Action");
publishingRepository.save(cassandraInAction3);
Book java = new Book("Manning", 5000, 9.99, "ISBN-4", true, "Java");
publishingRepository.save(java);
}
}

Die geschriebenen Daten können nun einfach über die CQL-Shell abgefragt werden:

cqlsh:publishing_keyspace> select * from publishing where publisher_name='Manning' ;

publisher_name | title | pages | available | isbn | price
----------------+---------------------+-------+-----------+--------+-------
Manning | Cassandra in Action | 100 | False | ISBN-1 | 19.99
Manning | Cassandra in Action | 200 | True | ISBN-2 | 29.99
Manning | Cassandra in Action | 500 | True | ISBN-3 | 39.99
Manning | Java | 5000 | True | ISBN-4 | 9.99

(4 rows)

Oder mit einer zusätzlichen Selektion auf den Titel:

cqlsh:publishing_keyspace> select * from publishing where publisher_name='Manning' and title = 'Cassandra in Action';

publisher_name | title | pages | available | isbn | price
----------------+---------------------+-------+-----------+--------+-------
Manning | Cassandra in Action | 100 | False | ISBN-1 | 19.99
Manning | Cassandra in Action | 200 | True | ISBN-2 | 29.99
Manning | Cassandra in Action | 500 | True | ISBN-3 | 39.99

(3 rows)

Und schließlich noch auf die Seiten:

cqlsh:publishing_keyspace> select * from publishing where publisher_name='Manning' and title = 'Cassandra in Action' and pages = 200;

publisher_name | title | pages | available | isbn | price
----------------+---------------------+-------+-----------+--------+-------
Manning | Cassandra in Action | 200 | True | ISBN-2 | 29.99

(1 rows)

Die folgende Abfrage führt nun jedoch zu keinem sinnvollen Ergebnis:

cqlsh:publishing_keyspace> select * from publishing where pages = 200;
InvalidRequest: Error from server: code=2200 [Invalid query] message="PRIMARY KEY column "pages" cannot be restricted as preceding column "title" is not restricted"

 


Seminare zum Thema

Weiterlesen

Apache Cassandra

Teil 1: Das Datenbanksystem

Apache Cassandra ist ein horizontal skalierendes, Master-loses Datenbanksystem, das durch einen Ring-Cluster realisiert wird. Doch was genau bedeutet dies?

Der Cluster

  • Wie bereits in einem vorherigen Artikel einer allgemeinen Einführung zum Thema NoSQL beschrieben, bedeutet horizontale Skalierung, dass beim Auftreten eines Engpasses bezüglich CPU, Speicher oder Storage einfach ein weiterer Knoten in einen Cluster hinzugefügt werden kann. Dieser kann nach kürzester Zeit Last übernehmen und somit den Engpass beheben.
    Ein horizontaler Cluster

     

  • In einem Master-losen Ring-Cluster sind alle Knoten des Systems gleichberechtigt: Jeder Knoten kann Anfragen eines Clients entgegen nehmen und entweder direkt selber verarbeiten oder an einen anderen Knoten delegieren. Solange von einem Client aus auch nur ein Knoten im Netz sichtbar ist, wird der Cluster funktionieren und antworten.

 

 

Der Ring-Cluster

 

Ob diese Antwort konsistent ist, hängt von der internen Kommunikation der Knoten im Ring ab. Dies ist die Aussage des bekannten CAP-Theorems.  

Replikation

Im Ring-Cluster von Cassandra wird ein Datensatz auf jeweils einem Knoten gehalten. Um bei einem Ausfall eines Cluster-Knotens Informations-Verlust zu verhindern, werden Datensätze auf mehrere Knoten repliziert. Dies wird durch den Replikationsfaktor RF beschrieben. So werden im folgenden Bild die Daten mit RF=3 abgelegt:

 

 

BASE

Schreibt ein Client Daten in den Ring-Cluster und liest beispielsweise ein anderer Client kurz danach die Daten, so können Inkonsistenzen auftreten. Es dauert eine gewisse Zeit, bis alle beteiligten Knoten des Rings alle Datenänderungen mitbekommen haben. Ist so ein Zeitfenster der Inkonsistenz fachlich möglich, so spricht man von einer BASE-Architektur, in der das E für die “Eventual Consistency” steht. “Eventual” ist hier selbstverständlich mit “letztendlich” zu übersetzen; letztendlich wird ein Ring-Cluster einen konsistenten Zustand erreichen.  

Tunable Consistency

Wird fachlich Konsistenz gefordert, so kann dies in einem Ring-Cluster dadurch erreicht werden, dass die Antwort des Clients so lange verzögert wird, bis die Daten auf allen replizierten Knoten angekommen sind. 

Eine nahe liegende Lösung wäre also, bei einer Schreib-Operation W zu verlangen, dass RF Knoten die Daten geschrieben haben. 

Dann kann ein Lesevorgang von einem beliebigen Knoten ausgeführt werden.

Also insgesamt:

W = RF und R = 1

Aber auch “anders herum” wird Konsistenz erreicht:

W = 1 und R = RF

Auf diese Art und Weise kann ausbalanciert werden, ob Wert auf schnelle Schreib- oder auf schnelle Lese-Vorgänge gelegt werden soll. Dies wird als  “Tunable Consistency” bezeichnet.

Beide Verfahren haben jedoch den Nachteil, dass bei einem Partitionsfehler, also einem Ausfall der Kommunikation zwischen Knoten, der gesamte Ring-Cluster in einem Standby-Modus geschaltet werden muss und keine Antwort liefern kann. Dies kann vermieden werden, in dem für Schreib und Lesevorgänge das Quorum benutzt wird:

W = R = (RF + 1)/2

Auch hier ist Konsistenz garantiert, allerdings muss nur noch in die Majorität der Knoten geschrieben werden.


Seminare zum Thema

Weiterlesen

Datenmodellierung für Apache Cassandra

Cassandra ist eine Datenbank, die nicht eindeutig in die 5 Kategorien der NoSQL-Datenbanksysteme eingeordnet werden kann: Einesteils ist sie Spalten-orientiert, andererseits wird für den effizienten Zugriff auf Daten ein “Partition Key” benötigt, also ein eindeutiger Primärschlüssel. Dies entspricht damit eher einem Key-Value-Store. Die Datenmodellierung für Apache Cassandra muss diese Besonderheiten berücksichtigen, um eine effiziente Datenverarbeitung ermöglichen zu können.

CQL – Die Cassandra Query Language

Um die Datenmodellierung mit Cassandra zu erlernen scheint es in erster Näherung sinnvoll, sich mit der Abfragesprache CQL zu beschäftigen. Diese Sprache wird wie SQL sowohl zur Struktur-Definition als auch für lesende und schreibende Zugriffe benutzt. 

Ein erster naiver Eindruck von CQL scheint nun zu zeigen, dass CQL und SQL sehr viel Analogien aufweisen. Es gibt ein CREATE TABLE ebenso wie ein SELECT FROM mit einer fast identischen Parametrisierung. 

Allerdings zeigt schon eine erste konkrete Umsetzung, dass diese Analogie äußerst beschränkt ist.

So muss zu allererst ein so genannter Keyspace eingerichtet werden, der in jedem Falle einen Replikationsfaktor definiert.

CREATE KEYSPACE PUBLISHING
WITH REPLICATION = {
'class': 'SimpleStrategy',
'replication_factor': 1
};

 

Dies ist in der Ring-Cluster-Architektur von Cassandra begründet, die eine Ausfallsicherheit durch Daten-Replikation über Knoten hinweg realisiert.

Das Anlegen einer Tabelle erfolgt dann scheinbar analog zu SQL. So wird im Folgenden eine Tabelle PUBLISHING angelegt, deren Intention es sein soll, einem Verleger Bücher zuzuordnen:

CREATE TABLE PUBLISHING(
publisher_name text,
title text,
isbn text,
price double,
pages int,
available boolean,
primary key(publisher_name, title)
);

Aber eine etwas genauere Analyse zeigt, dass diese Tabelle einen komplett anderen Aufbau hat als bei einem relationalen Modell. So würde ja in einem relationalen Datenbanksystem wohl eine Tabelle PUBLISHERS über einen Fremdschlüssel mit der Tabelle BOOKS verknüpft werden. 

Was ist nun aber der Sinn der obigen Tabellen-Definition:

  1. Der angegebene Primärschlüssel ist komplett etwas anderes. Der erste Teil, hier der publisher_name, ist der so genannte Partition Key. Cassandra verteilt die Datensätze an Hand des Partition Keys auf die Knoten des Clusters. Damit müssen effiziente Abfragen stets mit einer einleitenden WHERE <PARTITION_KEY> = <VALUE> eingeleitet werden. Ohne diese zentrale Selektion muss der Cluster die Abfrage auf allen Knoten des Rings ausführen, also eine Art “Full Table Scan” ausführen, was nicht sonderlich effizient ist.
  2. Der zweite Teil des Primary Key ist eine Liste von so genannten “Clustering Columns”.  Nur mit Hilfe einer Clustering Column kann ergänzend zum Partition Key die Abfrage erweitert werden. So kann in obigem Beispiel die Where-Klausel geschrieben werden als WHERE pulisher_name = 'Manning' and title = 'Cassandra in Action'. Eine weitere Selektion, also beispielsweise AND price = 19.99 ist nicht möglich. 

Die zweite obige Anmerkung zeigt den Spalten-orientierten Charakter von Cassandra. Die Clustering Column title ist nämlich ein Key in einer Map, dessen Value dann den zusätzlichen Buch-Informationen entspricht. Dieses Value wird von Cassandra aber als unstrukturiertes Dokument angenommen und wird deshalb in effizienten Abfragen nicht benutzt. 

Was wäre aber die Konsequenz, wenn eine weitere Clustering Column, also beispielsweise die pages mit in den Primary Key aufgenommen werden würden: 

primary key(publisher_name, title)

Fangen wir mit der Spalten-orientierten Arbeitsweise an: Die über den Partition Key definierte Map enthält als Schlüssel beispielsweise den Titel  'Cassandra in Action' . Das zugehörige Value ist nun aber wiederum eine Map! Und diese enthält für jeden Key-Eintrag der Seiten wiederum die restlichen Informationen des Buchs. Oder anders formuliert: Die PUBLISHING-Tabelle enthält für jeden Buch-Titel eine Liste von Seiten, die dann jeweils die ISBN, den Preis sowie die Verfügbarkeit enthält. 

Eine Abfrage der Form WHERE publisher_name = 'Manning' AND title = 'Cassandra in Action' AND pages = 500 ist vollkommen korrekt, ebenso title = 'Cassandra in Action'. Falsch hingegen wäre  WHERE publisher_name = 'Manning' AND pages = 500: Hier wird in der Navigation über die verschachtelten Maps eine Ebene ausgelassen, so dass wiederum eine sehr ineffiziente Abfrage resultieren würde.

Query First

Wir sehen damit, dass bei der Datenmodellierung mit Cassandra der Ausgangspunkt nicht das statische Datenmodell sein muss, sondern die auf den Datenbestand auszuführenden Abfragen. Dies wird als “Query First”-Ansatz bezeichnet.

Notwendige Konsequenz von Query First ist eine komplette Denormalisierung des Datenbestandes. Sollen verschiedene Kombinationen von Abfrage-Kriterien unterstützt werden so muss in Cassandra jeweils eine eigene Tabelle angelegt werden, so dass Daten redundant abgelegt werden müssen. 

Moderne Cassandra-Versionen

Die durch den “Query First”-Ansatz entstandenen Daten-Redundanzen sind für viele Modellierer schwer zu akzeptieren. Die Cassandra-Community hat deshalb in den letzten Jahren immer mehr Features und Optimierungen eingeführt, die dieses Problem lösen sollen. So können beispielsweise durch Indizes sehr wohl auch Abfragen bis in die Ebene der Value-Dokumente durchgeführt werden. Weiterhin führen Abfragen ohne die Angaben des Partition Key “nur” zu einer Warnung. Trotzdem müssen für einen sinnvollen Einsatz des Apache Cassandra-Datenbanksystems auch in den neuesten Versionen die technische Grundlagen sicher beherrscht und berücksichtigt werden. Sonst ist eine Ergänzung oder sogar Ablösung eines relationalen Datenbanksystems durch Cassandra sinnlos bzw. sogar  nachteilig. 


Seminare zum Thema

Weiterlesen

Spring Data

Spring Data

Spring Data ist ein zentrales Projekt des Spring Frameworks und besticht durch seine Einfachheit und Konsistenz. So können selbst komplexe Datenabfragen mit wenigen Zeilen an Quellcode erstellt werden. Insbesondere im Zusammenspiel mit Spring Boot ist es so möglich, mit wenigen Programmzeilen eine komplette Anwendung wie beispielsweise einen Restful WebService zu realisieren.

Eine Übersicht

Der Begriff “Datenabfragen” ist im Übrigen nicht zufällig so allgemein gewählt: Spring Data selbst definiert nur eine relativ schlanke Abstraktionsschicht auf einen Datenbestand. So besitzen alle Datenobjekte eine eindeutige ID und werden über ein Repository mit  CRUD-Operationen verwaltet.

Alle weiteren Details sind abhängig von der konkret verwendeten Datenbank-Technologie. So unterscheidet sich selbstverständlich ein konkretes JPA-Repository mit einer transaktionsfähigen relationalen Datenbank im Hintergrund von einem Mongo-Repository, bei dem eine Dokumenten-orientierte Datenbank aus dem NoSQL-Umfeld angesprochen wird.

Spring Data Projekte

Die Spring-Entwickler haben damit nicht den Fehler gemacht zu versuchen, jegliche Datenzugriffs-Technologien allgemein zu abstrahieren. Dieser Versuch ist mit der allgemeinen JEE-Connector-Spezifikation bereits misslungen. Statt dessen gruppiert Spring Data verschiedene Implementierungen mit durchaus unterschiedlichen APIs unter einem gemeinsamen Namen.

Die Anzahl der Projekte ist selbst in der offiziellen Spring-Distribution bereits sehr umfassend und wird durch die Community permanent weiter ergänzt.

Gemeinsam ist all diesen Teilprojekten jedoch die grundsätzliche Arbeitsweise: Entitäten werden über ein dynamisch generiertes Repository im Storage abgelegt.

Ein einfaches Beispiel mit Spring Data JPA

Diese Arbeitsweise wird im folgenden Beispiel näher erläutert:

Wir beginnen hierzu mit einer einfachen Daten-Entität:

@Entity
@Table(name="PEOPLE")
public class Person {
	
	private String lastname;
	
	private String firstname;
	private int height;
	@Id
	@GeneratedValue(strategy=GenerationType.AUTO)
        private Long id;

...
}

Zu beachten ist hierbei bereits, dass diese Entität die normalen Annotationen des Java Persistence APIs benutzt. Insbesondere kann @javax.persistence.Id benutzt werden, die Verwendung von @org.springframework.data.annotation.Id ist optional.

Das Repository selber ist für eine einfache Anwendung komplett trivial:

public interface PeopleRepository extends CrudRepository<Person, Long>{

}

Die Methoden des CrudRepository werden von Spring zur Laufzeit automatisch generiert, so dass das Repository ohne jegliche eigene Programmierung injiziert und sofort benutzt werden kann. Es ist nicht notwendig, das umfangreiche API zu implementieren! Zur Laufzeit generiert Spring hierfür einen generischen Proxy, der die Aufrufe in geeigneter Form an den gekapselten JPA-EntityManager delegiert.

Dies ist übrigens keine Prototyp- oder Dummy-Implementierung! Es ist weder notwendig noch gebräuchlich noch sinnvoll, für den  produktiven Einsatz diese Methoden durch eigene Sequenzen zu ersetzen und damit zu “verbessern”.

Genügen die im Basis-Interface definierten Methoden nicht, können diese durch eigene ergänzt werden. Hierbei werden folgende Möglichkeiten bereitgestellt:

  • Namenskonventionen: Eine Methode mit der Signatur findByLastnameOrderedByHeight(String lastname) kann einfach im Interface deklariert werden. Eine Implementierung ist nicht notwendig, da das im Endeffekt zu generierende JQL-Statement wohl offensichtlich ist.
  • Sollen diese Namenskonvention nicht verwendet werden, kann statt dessen eine Methode beliebiger Signatur mit der @Query-Annotation versehen werden.
    @Query("SELECT p FROM Person p WHERE p.lastname = :lastname order by p.height")
    public Collection<Person> getByLastname(@Param("lastname") String lastname);

    Statt der Annotationen können auch Named Queries der orm.xml benutzt werden.

  • Und schließlich kann das Interface in einer abstrakten Klasse selbst programiert werden. Dabei werden aber selbstverständlich nur die besonders zu behandelnden Fälle implementiert.

Seminar zum Thema

Weiterlesen