Eine Anleitung zu Cassandra mit Java

Ein Leitfaden für Cassandra mit Java

1. Überblick

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

Es werden wichtige Konzepte erklärt, zusammen mit einem Arbeitsbeispiel, das die grundlegenden Schritte zum Herstellen einer Verbindung mit dieser NoSQL-Datenbank von Java aus und zum Arbeiten mit ihr abdeckt.

2. Kassandra

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 Master-Knoten - alle beteiligten Knoten sind identisch und kommunizieren miteinander als Peers.

Dies macht Cassandra zu einem horizontal skalierbaren System, indem es das schrittweise Hinzufügen von Knoten ermöglicht, 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 beteiligten Knoten verwendet wird

  • Keyspace - Wenn Sie aus einer relationalen Datenbank stammen, ist das Schema der jeweilige Schlüsselbereich in Cassandra. Der Schlüsselbereich ist der äußerste Container für Daten in Cassandra. Die Hauptattribute, die pro Schlüsselbereich festgelegt werden müssen, sindReplication Factor,Replica Placement Strategy undColumn Families

  • Column Family - Spaltenfamilien in Cassandra sind wie Tabellen in relationalen Datenbanken. Jede Spaltenfamilie enthält eine Sammlung von Zeilen, die durchMap<RowKey, SortedMap<ColumnKey, ColumnValue>> dargestellt werden. Der Schlüssel ermöglicht den gemeinsamen Zugriff auf verwandte Daten

  • Column - 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 inpom.xml definieren, deren neueste Versionhere enthält:


    com.datastax.cassandra
    cassandra-driver-core
    3.1.0

Um den Code mit einem eingebetteten Datenbankserver zu testen, sollten wir auch die Abhängigkeit voncassandra-unithinzufügen, deren neueste Versionhere enthält:


    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 einCluster-Objekt erstellen.

Als Kontaktstelle muss eine Adresse eines Knotens 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 "library":

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 vonkeyspaceName müssen zwei weitere Parameter definiert werden,replicationFactor undreplicationStrategy. Diese Parameter bestimmen die Anzahl der Replikate und die Verteilung der Replikate über den Ring.

Bei der Replikation gewährleistet Cassandra 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 dem vorhandenen Schlüsselbereich die ersten Bücher der Spaltenfamilie 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 kann keine solche Spalte gefunden werden. Mit dem folgenden Code können wir die Tabelle ändern und eine neue Spalte hinzufü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);
}

Stellen Sie sicher, dass die neue Spaltepublisher hinzugefügt wurde:

@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 Tabellebookserstellt wurde, können Sie der Tabelle Daten 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 'books'-Tabelle wurde eine neue Zeile hinzugefügt, damit wir testen können, ob die Zeile existiert:

@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 NamenbooksByTitle: 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 ist es eine der bewährten Methoden, ein Muster für eine Tabelle pro Abfrage zu verwenden. Dies bedeutet, dass für eine andere Abfrage eine andere Tabelle benötigt wird.

In unserem Beispiel haben wir uns entschieden, ein Buch nach seinem Titel auszuwählen. Um die AbfrageselectByTitle zu erfüllen, haben wir eine Tabelle mit einer VerbindungPRIMARY KEY unter Verwendung der Spaltentitle undid erstellt. Die Spaltetitle ist der Partitionierungsschlüssel, während die Spalteid der Clustering-Schlüssel ist.

Auf diese Weise enthalten viele Tabellen in Ihrem Datenmodell doppelte Daten. Dies ist kein Nachteil dieser Datenbank. Im Gegenteil, diese Vorgehensweise optimiert die Leistung der Lesevorgänge.

Sehen wir uns die Daten an, die derzeit in unserer Tabelle gespeichert sind:

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;
}

Ein Test für die Abfrage, der die erwarteten Ergebnisse zurückgibt:

@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")));
}

Bis jetzt ist alles in Ordnung, aber eines muss realisiert werden. Wir haben angefangen, mit Tabellebooks, zu arbeiten, aber in der Zwischenzeit mussten wir eine andere Tabelle mit dem NamenbooksByTitle. erstellen, um die Abfrage vonselect durch die Spaltetitle zu erfüllen

Die beiden Tabellen sind identisch und enthalten doppelte Spalten. Wir haben jedoch nur Daten in die TabellebooksByTitleeingefügt. Infolgedessen sind die Daten in zwei Tabellen derzeit inkonsistent.

Wir können dies mit einerbatch-Abfrage lösen, die zwei insert-Anweisungen umfasst, eine für jede Tabelle. Die Abfrage vonbatchführt mehrere DML-Anweisungen als eine einzige Operation aus.

Ein Beispiel für eine solche Abfrage wird bereitgestellt:

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);
}

Wieder testen wir die Batch-Abfrageergebnisse wie folgt:

@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 ist eine neue Funktion namens "Materialized Views" verfügbar, die wir anstelle vonbatch Abfragen verwenden können. Ein gut dokumentiertes Beispiel für „Materialisierte Ansichten“ isthere verfügbar.

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 einemInvalidQueryException: unconfigured table books:

@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 zur und Verwenden der Cassandra-Datenbank mit Java beschrieben. Einige der Schlüsselkonzepte dieser Datenbank wurden ebenfalls besprochen, um Ihnen den Einstieg zu erleichtern.

Die vollständige Implementierung dieses Tutorials finden Sie inGithub project.