Eine Anleitung zu Cassandra mit Java

1. Übersicht

Dieses Tutorial ist eine Einführung in die Apache Cassandra-Datenbank mit Java.

Hier finden Sie wichtige Konzepte sowie ein Arbeitsbeispiel, in dem die grundlegenden Schritte zum Herstellen einer Verbindung mit dieser NoSQL-Datenbank und zum Arbeiten mit dieser NoSQL-Datenbank von Java aus beschrieben werden.

2. Cassandra

Cassandra ist eine skalierbare NoSQL-Datenbank, die eine kontinuierliche Verfügbarkeit ohne Single Point of Failure bietet und die Möglichkeit bietet, große Datenmengen mit außergewöhnlicher Leistung zu verarbeiten.

Diese Datenbank verwendet ein Ringdesign anstelle einer Master-Slave-Architektur. Im Ringdesign gibt es keinen Masterknoten - alle teilnehmenden Knoten sind identisch und kommunizieren als Peers miteinander.

Dies macht Cassandra zu einem horizontal skalierbaren System, indem das schrittweise Hinzufügen von Knoten ermöglicht wird, ohne dass eine Neukonfiguration erforderlich ist.

2.1. Schlüssel Konzepte

Beginnen wir mit einem kurzen Überblick über einige der Schlüsselkonzepte von Cassandra:

  • Cluster - Eine Sammlung von Knoten oder Rechenzentren, die in einer Ringarchitektur angeordnet sind. Jedem Cluster muss ein Name zugewiesen werden, der anschließend von den teilnehmenden Knoten verwendet wird
  • Schlüsselraum - Wenn Sie aus einer relationalen Datenbank stammen, ist das Schema der entsprechende Schlüsselraum in Cassandra. Der Schlüsselbereich ist der äußerste Container für Daten in Cassandra. Die wichtigsten Attribute, die pro Schlüsselbereich festgelegt werden müssen, sind der Replikationsfaktor , die Replikatplatzierungsstrategie und die Spaltenfamilien
  • Spaltenfamilie - Spaltenfamilien in Cassandra sind wie Tabellen in relationalen Datenbanken. Jede Spaltenfamilie enthält eine Sammlung von Zeilen, die durch eine Karte dargestellt werden . Der Schlüssel bietet die Möglichkeit, gemeinsam auf verwandte Daten zuzugreifen
  • Spalte - Eine Spalte in Cassandra ist eine Datenstruktur, die einen Spaltennamen, einen Wert und einen Zeitstempel enthält. Die Spalten und die Anzahl der Spalten in jeder Zeile können im Gegensatz zu einer relationalen Datenbank, in der die Daten gut strukturiert sind, variieren

3. Verwenden des Java-Clients

3.1. Maven-Abhängigkeit

Wir müssen die folgende Cassandra-Abhängigkeit in der pom.xml definieren , deren neueste Version hier zu finden ist:

 com.datastax.cassandra cassandra-driver-core 3.1.0 

Um den Code mit einem eingebetteten Datenbankserver zu testen, sollten wir auch die Cassandra-Unit- Abhängigkeit hinzufügen , deren neueste Version hier zu finden ist:

 org.cassandraunit cassandra-unit 3.0.0.1 

3.2. Verbindung zu Cassandra herstellen

Um von Java aus eine Verbindung zu Cassandra herzustellen, müssen wir ein Cluster- Objekt erstellen .

Eine Adresse eines Knotens muss als Kontaktpunkt angegeben werden. Wenn wir keine Portnummer angeben, wird der Standardport (9042) verwendet.

Mit diesen Einstellungen kann der Treiber die aktuelle Topologie eines Clusters ermitteln.

public class CassandraConnector { private Cluster cluster; private Session session; public void connect(String node, Integer port) { Builder b = Cluster.builder().addContactPoint(node); if (port != null) { b.withPort(port); } cluster = b.build(); session = cluster.connect(); } public Session getSession() { return this.session; } public void close() { session.close(); cluster.close(); } }

3.3. Erstellen des Schlüsselraums

Erstellen wir unseren Schlüsselbereich " Bibliothek ":

public void createKeyspace( String keyspaceName, String replicationStrategy, int replicationFactor) { StringBuilder sb = new StringBuilder("CREATE KEYSPACE IF NOT EXISTS ") .append(keyspaceName).append(" WITH replication = {") .append("'class':'").append(replicationStrategy) .append("','replication_factor':").append(replicationFactor) .append("};"); String query = sb.toString(); session.execute(query); }

Mit Ausnahme des Schlüsselbereichsnamens müssen zwei weitere Parameter definiert werden, der replicationFactor und die replicationStrategy . Diese Parameter bestimmen die Anzahl der Replikate und wie die Replikate über den Ring verteilt werden.

Bei der Replikation sorgt Cassandra für Zuverlässigkeit und Fehlertoleranz, indem Kopien von Daten auf mehreren Knoten gespeichert werden.

An dieser Stelle können wir testen, ob unser Schlüsselbereich erfolgreich erstellt wurde:

private KeyspaceRepository schemaRepository; private Session session; @Before public void connect() { CassandraConnector client = new CassandraConnector(); client.connect("127.0.0.1", 9142); this.session = client.getSession(); schemaRepository = new KeyspaceRepository(session); }
@Test public void whenCreatingAKeyspace_thenCreated() { String keyspaceName = "library"; schemaRepository.createKeyspace(keyspaceName, "SimpleStrategy", 1); ResultSet result = session.execute("SELECT * FROM system_schema.keyspaces;"); List matchedKeyspaces = result.all() .stream() .filter(r -> r.getString(0).equals(keyspaceName.toLowerCase())) .map(r -> r.getString(0)) .collect(Collectors.toList()); assertEquals(matchedKeyspaces.size(), 1); assertTrue(matchedKeyspaces.get(0).equals(keyspaceName.toLowerCase())); }

3.4. Erstellen einer Spaltenfamilie

Jetzt können wir die ersten "Bücher" der Spaltenfamilie zum vorhandenen Schlüsselbereich hinzufügen:

private static final String TABLE_NAME = "books"; private Session session; public void createTable() { StringBuilder sb = new StringBuilder("CREATE TABLE IF NOT EXISTS ") .append(TABLE_NAME).append("(") .append("id uuid PRIMARY KEY, ") .append("title text,") .append("subject text);"); String query = sb.toString(); session.execute(query); }

Der Code zum Testen, ob die Spaltenfamilie erstellt wurde, ist unten angegeben:

private BookRepository bookRepository; private Session session; @Before public void connect() { CassandraConnector client = new CassandraConnector(); client.connect("127.0.0.1", 9142); this.session = client.getSession(); bookRepository = new BookRepository(session); }
@Test public void whenCreatingATable_thenCreatedCorrectly() { bookRepository.createTable(); ResultSet result = session.execute( "SELECT * FROM " + KEYSPACE_NAME + ".books;"); List columnNames = result.getColumnDefinitions().asList().stream() .map(cl -> cl.getName()) .collect(Collectors.toList()); assertEquals(columnNames.size(), 3); assertTrue(columnNames.contains("id")); assertTrue(columnNames.contains("title")); assertTrue(columnNames.contains("subject")); }

3.5. Ändern der Spaltenfamilie

Ein Buch hat auch einen Verlag, aber in der erstellten Tabelle befindet sich keine solche Spalte. Wir können den folgenden Code verwenden, um die Tabelle zu ändern und eine neue Spalte hinzuzufügen:

public void alterTablebooks(String columnName, String columnType) { StringBuilder sb = new StringBuilder("ALTER TABLE ") .append(TABLE_NAME).append(" ADD ") .append(columnName).append(" ") .append(columnType).append(";"); String query = sb.toString(); session.execute(query); }

Lassen Sie uns dafür sorgen , dass die neue Spalte Verleger hinzugefügt:

@Test public void whenAlteringTable_thenAddedColumnExists() { bookRepository.createTable(); bookRepository.alterTablebooks("publisher", "text"); ResultSet result = session.execute( "SELECT * FROM " + KEYSPACE_NAME + "." + "books" + ";"); boolean columnExists = result.getColumnDefinitions().asList().stream() .anyMatch(cl -> cl.getName().equals("publisher")); assertTrue(columnExists); }

3.6. Einfügen von Daten in die Spaltenfamilie

Nachdem die Büchertabelle erstellt wurde, können Sie Daten zur Tabelle hinzufügen:

public void insertbookByTitle(Book book) { StringBuilder sb = new StringBuilder("INSERT INTO ") .append(TABLE_NAME_BY_TITLE).append("(id, title) ") .append("VALUES (").append(book.getId()) .append(", '").append(book.getTitle()).append("');"); String query = sb.toString(); session.execute(query); }

In der Tabelle "Bücher" wurde eine neue Zeile hinzugefügt, damit wir testen können, ob die Zeile vorhanden ist:

@Test public void whenAddingANewBook_thenBookExists() { bookRepository.createTableBooksByTitle(); String title = "Effective Java"; Book book = new Book(UUIDs.timeBased(), title, "Programming"); bookRepository.insertbookByTitle(book); Book savedBook = bookRepository.selectByTitle(title); assertEquals(book.getTitle(), savedBook.getTitle()); }

Im obigen Testcode haben wir eine andere Methode verwendet, um eine Tabelle mit dem Namen booksByTitle zu erstellen :

public void createTableBooksByTitle() { StringBuilder sb = new StringBuilder("CREATE TABLE IF NOT EXISTS ") .append("booksByTitle").append("(") .append("id uuid, ") .append("title text,") .append("PRIMARY KEY (title, id));"); String query = sb.toString(); session.execute(query); }

In Cassandra besteht eine der besten Methoden darin, ein Muster mit einer Tabelle pro Abfrage zu verwenden. Dies bedeutet, dass für eine andere Abfrage eine andere Tabelle benötigt wird.

In our example, we have chosen to select a book by its title. In order to satisfy the selectByTitle query, we have created a table with a compound PRIMARY KEY using the columns, title and id. The column title is the partitioning key while the id column is the clustering key.

This way, many of the tables in your data model contain duplicate data. This is not a downside of this database. On the contrary, this practice optimizes the performance of the reads.

Let's see the data that are currently saved in our table:

public List selectAll() { StringBuilder sb = new StringBuilder("SELECT * FROM ").append(TABLE_NAME); String query = sb.toString(); ResultSet rs = session.execute(query); List books = new ArrayList(); rs.forEach(r -> { books.add(new Book( r.getUUID("id"), r.getString("title"), r.getString("subject"))); }); return books; }

A test for query returning expected results:

@Test public void whenSelectingAll_thenReturnAllRecords() { bookRepository.createTable(); Book book = new Book( UUIDs.timeBased(), "Effective Java", "Programming"); bookRepository.insertbook(book); book = new Book( UUIDs.timeBased(), "Clean Code", "Programming"); bookRepository.insertbook(book); List books = bookRepository.selectAll(); assertEquals(2, books.size()); assertTrue(books.stream().anyMatch(b -> b.getTitle() .equals("Effective Java"))); assertTrue(books.stream().anyMatch(b -> b.getTitle() .equals("Clean Code"))); }

Everything is fine till now, but one thing has to be realized. We started working with table books, but in the meantime, in order to satisfy the select query by title column, we had to create another table named booksByTitle.

The two tables are identical containing duplicated columns, but we have only inserted data in the booksByTitle table. As a consequence, data in two tables is currently inconsistent.

We can solve this using a batch query, which comprises two insert statements, one for each table. A batch query executes multiple DML statements as a single operation.

An example of such query is provided:

public void insertBookBatch(Book book) { StringBuilder sb = new StringBuilder("BEGIN BATCH ") .append("INSERT INTO ").append(TABLE_NAME) .append("(id, title, subject) ") .append("VALUES (").append(book.getId()).append(", '") .append(book.getTitle()).append("', '") .append(book.getSubject()).append("');") .append("INSERT INTO ") .append(TABLE_NAME_BY_TITLE).append("(id, title) ") .append("VALUES (").append(book.getId()).append(", '") .append(book.getTitle()).append("');") .append("APPLY BATCH;"); String query = sb.toString(); session.execute(query); }

Again we test the batch query results like so:

@Test public void whenAddingANewBookBatch_ThenBookAddedInAllTables() { bookRepository.createTable(); bookRepository.createTableBooksByTitle(); String title = "Effective Java"; Book book = new Book(UUIDs.timeBased(), title, "Programming"); bookRepository.insertBookBatch(book); List books = bookRepository.selectAll(); assertEquals(1, books.size()); assertTrue( books.stream().anyMatch( b -> b.getTitle().equals("Effective Java"))); List booksByTitle = bookRepository.selectAllBookByTitle(); assertEquals(1, booksByTitle.size()); assertTrue( booksByTitle.stream().anyMatch( b -> b.getTitle().equals("Effective Java"))); }

Hinweis: Ab Version 3.0 ein neues Feature „Materialized Views“ genannt zur Verfügung, die wir statt verwenden können Batch - Abfragen. Ein gut dokumentiertes Beispiel für „Materialisierte Ansichten“ finden Sie hier.

3.7. Löschen der Spaltenfamilie

Der folgende Code zeigt, wie eine Tabelle gelöscht wird:

public void deleteTable() { StringBuilder sb = new StringBuilder("DROP TABLE IF EXISTS ").append(TABLE_NAME); String query = sb.toString(); session.execute(query); }

Die Auswahl einer Tabelle, die nicht im Schlüsselbereich vorhanden ist, führt zu einer InvalidQueryException: Nicht konfigurierte Tabellenbücher :

@Test(expected = InvalidQueryException.class) public void whenDeletingATable_thenUnconfiguredTable() { bookRepository.createTable(); bookRepository.deleteTable("books"); session.execute("SELECT * FROM " + KEYSPACE_NAME + ".books;"); }

3.8. Löschen des Schlüsselraums

Zuletzt löschen wir den Schlüsselraum:

public void deleteKeyspace(String keyspaceName) { StringBuilder sb = new StringBuilder("DROP KEYSPACE ").append(keyspaceName); String query = sb.toString(); session.execute(query); }

Und testen Sie, ob der Schlüsselbereich gelöscht wurde:

@Test public void whenDeletingAKeyspace_thenDoesNotExist() { String keyspaceName = "library"; schemaRepository.deleteKeyspace(keyspaceName); ResultSet result = session.execute("SELECT * FROM system_schema.keyspaces;"); boolean isKeyspaceCreated = result.all().stream() .anyMatch(r -> r.getString(0).equals(keyspaceName.toLowerCase())); assertFalse(isKeyspaceCreated); }

4. Fazit

In diesem Lernprogramm wurden die grundlegenden Schritte zum Herstellen einer Verbindung mit und Verwenden der Cassandra-Datenbank mit Java behandelt. Einige der Schlüsselkonzepte dieser Datenbank wurden ebenfalls besprochen, um Ihnen den Start zu erleichtern.

Die vollständige Implementierung dieses Tutorials finden Sie im Github-Projekt.