1. 概要

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

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

2. カサンドラ

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

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

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

2.1. 重要な概念

カサンドラの重要な概念のいくつかの簡単な調査から始めましょう:

  • クラスター–リングアーキテクチャに配置されたノードまたはデータセンターのコレクション。 すべてのクラスターに名前を割り当てる必要があります。この名前は、後で参加ノードによって使用されます。
  • キースペース–リレーショナルデータベースからアクセスしている場合、スキーマはCassandraのそれぞれのキースペースです。 キースペースは、Cassandraのデータの最も外側のコンテナです。 キースペースごとに設定する主な属性は、レプリケーションファクターレプリカ配置戦略、および列ファミリーです。
  • 列ファミリー– Cassandraの列ファミリーは、リレーショナルデータベースのテーブルのようなものです。 各列ファミリーには、で表される行のコレクションが含まれています。 地図 >>キーを使用すると、関連データに一緒にアクセスできます
  • Column – Cassandraの列は、列名、値、およびタイムスタンプを含むデータ構造です。 データが適切に構造化されているリレーショナルデータベースとは対照的に、各行の列と列の数は異なる場合があります

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

3.1. Mavenの依存関係

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

<dependency>
    <groupId>com.datastax.cassandra</groupId>
    <artifactId>cassandra-driver-core</artifactId>
    <version>3.1.0</version>
</dependency>

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

<dependency>
    <groupId>org.cassandraunit</groupId>
    <artifactId>cassandra-unit</artifactId>
    <version>3.0.0.1</version>
</dependency>

3.2. カサンドラに接続する

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

ライブラリ」キースペースを作成しましょう。

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

レプリケーションにより、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<String> 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<String> 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 クエリを満たすために、titleおよびidの列を使用して、複合 PRIMARYKEYを含むテーブルを作成しました。 title 列はパーティショニングキーであり、id列はクラスタリングキーです。

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

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

public List<Book> selectAll() {
    StringBuilder sb = 
      new StringBuilder("SELECT * FROM ").append(TABLE_NAME);

    String query = sb.toString();
    ResultSet rs = session.execute(query);

    List<Book> books = new ArrayList<Book>();

    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<Book> 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つのテーブルのデータは現在一貫性がありません。

これは、 batch クエリを使用して解決できます。このクエリは、テーブルごとに1つずつ、2つの挿入ステートメントで構成されます。 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<Book> books = bookRepository.selectAll();
    
    assertEquals(1, books.size());
    assertTrue(
      books.stream().anyMatch(
        b -> b.getTitle().equals("Effective Java")));
        
    List<Book> booksByTitle = bookRepository.selectAllBookByTitle();
    
    assertEquals(1, booksByTitle.size());
    assertTrue(
      booksByTitle.stream().anyMatch(
        b -> b.getTitle().equals("Effective Java")));
}

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

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プロジェクトにあります。