JavaによるCassandraのガイド

Javaを使用したCassandraのガイド

1. 概要

このチュートリアルは、Javaを使用したApache Cassandraデータベースの入門ガイドです。

JavaからこのNoSQLデータベースに接続して作業を開始するための基本的な手順をカバーする実例とともに、重要な概念が説明されています。

2. カサンドラ

CassandraはスケーラブルなNoSQLデータベースであり、単一障害点のない継続的な可用性を提供し、並外れたパフォーマンスで大量のデータを処理する機能を提供します。

このデータベースは、マスタースレーブアーキテクチャを使用する代わりにリング設計を使用します。 リング設計では、マスターノードはありません。参加しているすべてのノードは同一であり、ピアとして相互に通信します。

これにより、Cassandraは、再構成を必要とせずにノードを段階的に追加できるため、水平方向にスケーラブルなシステムになります。

2.1. キーコンセプト

まず、Cassandraの重要な概念のいくつかの簡単な調査から始めましょう。

  • Cluster –リングアーキテクチャに配置されたノードまたはデータセンターのコレクション。 すべてのクラスターに名前を割り当てる必要があり、その後、参加するノードによって使用されます

  • Keyspace –リレーショナルデータベースからアクセスしている場合、スキーマはCassandraのそれぞれのキースペースです。 キースペースは、Cassandraのデータの最も外側のコンテナーです。 キースペースごとに設定する主な属性は、Replication FactorReplica Placement Strategy、およびColumn Familiesです。

  • Column Family – Cassandraの列ファミリーは、リレーショナルデータベースのテーブルのようなものです。 各列ファミリーには、Map<RowKey, SortedMap<ColumnKey, ColumnValue>>で表される行のコレクションが含まれています。 キーは、関連するデータに一緒にアクセスする機能を提供します

  • Column – Cassandraの列は、列名、値、およびタイムスタンプを含むデータ構造です。 データが適切に構造化されているリレーショナルデータベースとは対照的に、各行の列と列の数は異なる場合があります

3. Javaクライアントの使用

3.1. メーベン依存

pom.xmlで次のCassandra依存関係を定義する必要があります。その最新バージョンはhereにあります。


    com.datastax.cassandra
    cassandra-driver-core
    3.1.0

組み込みデータベースサーバーでコードをテストするには、cassandra-unit依存関係も追加する必要があります。最新バージョンはhereにあります。


    org.cassandraunit
    cassandra-unit
    3.0.0.1

3.2. Cassandraへの接続

JavaからCassandraに接続するには、Clusterオブジェクトを作成する必要があります。

ノードのアドレスを連絡先として提供する必要があります。 ポート番号を指定しない場合は、デフォルトのポート(9042)が使用されます。

これらの設定により、ドライバーはクラスターの現在のトポロジーを検出できます。

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. キースペースの作成

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

keyspaceNameを除いて、さらに2つのパラメーター、replicationFactorreplicationStrategyを定義する必要があります。 これらのパラメータは、レプリカの数と、レプリカがリング全体にどのように分散されるかをそれぞれ決定します。

レプリケーションにより、Cassandraはデータのコピーを複数のノードに保存することにより、信頼性と耐障害性を確保します。

この時点で、キースペースが正常に作成されたことをテストできます。

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. 列ファミリーの作成

これで、既存のキースペースに最初の列ファミリー「本」を追加できます。

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

列ファミリが作成されたことをテストするコードを以下に示します。

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. 列ファミリーの変更

書籍には出版社もいますが、作成されたテーブルにそのような列は見つかりません。 次のコードを使用して、テーブルを変更し、新しい列を追加できます。

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

新しい列publisherが追加されていることを確認しましょう。

@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. 列ファミリーへのデータの挿入

booksテーブルが作成されたので、テーブルへのデータの追加を開始する準備が整いました。

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

「books」テーブルに新しい行が追加されたため、行が存在するかどうかをテストできます。

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

上記のテストコードでは、別の方法を使用してbooksByTitle:という名前のテーブルを作成しました。

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

Cassandraでのベストプラクティスの1つは、クエリごとに1つのテーブルを使用するパターンです。 つまり、異なるクエリには異なるテーブルが必要です。

この例では、タイトルで本を選択しました。 selectByTitleクエリを満たすために、列titleidを使用して複合PRIMARY KEYでテーブルを作成しました。 列titleはパーティショニングキーであり、id列はクラスタリングキーです。

このようにして、データモデル内のテーブルの多くに重複データが含まれます。 これは、このデータベースの欠点ではありません。 それどころか、この方法は読み取りのパフォーマンスを最適化します。

現在テーブルに保存されているデータを見てみましょう。

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

期待される結果を返すクエリのテスト:

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

これまではすべて問題ありませんが、1つのことを理解する必要があります。 テーブルbooks,の操作を開始しましたが、その間に、title列によるselectクエリを満たすために、booksByTitle.という名前の別のテーブルを作成する必要がありました。

2つのテーブルは同一であり、重複する列が含まれていますが、booksByTitleテーブルにデータを挿入しただけです。 結果として、2つのテーブルのデータは現在一貫性がありません。

これは、テーブルごとに1つずつ、2つの挿入ステートメントで構成されるbatchクエリを使用して解決できます。 batchクエリは、複数のDMLステートメントを単一の操作として実行します。

このようなクエリの例は次のとおりです。

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

ここでも、バッチクエリの結果を次のようにテストします。

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

注:バージョン3.0以降、「マテリアライズドビュー」と呼ばれる新機能が利用可能になりました。これは、batchクエリの代わりに使用できます。 「マテリアライズドビュー」の十分に文書化された例は、hereで利用できます。

3.7. 列ファミリーの削除

以下のコードは、テーブルを削除する方法を示しています。

public void deleteTable() {
    StringBuilder sb =
      new StringBuilder("DROP TABLE IF EXISTS ").append(TABLE_NAME);

    String query = sb.toString();
    session.execute(query);
}

キースペースに存在しないテーブルを選択すると、InvalidQueryException: 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. キースペースの削除

最後に、キースペースを削除しましょう。

public void deleteKeyspace(String keyspaceName) {
    StringBuilder sb =
      new StringBuilder("DROP KEYSPACE ").append(keyspaceName);

    String query = sb.toString();
    session.execute(query);
}

キースペースが削除されたことをテストします。

@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. 結論

このチュートリアルでは、JavaでCassandraデータベースに接続して使用する基本的な手順について説明しました。 このデータベースの重要な概念のいくつかは、キックスタートを支援するために議論されています。

このチュートリアルの完全な実装は、Github projectにあります。