Руководство по Кассандре с Java

Путеводитель по Кассандре с Явой

1. обзор

Это руководство является вводным руководством к базе данныхApache Cassandra с использованием Java.

Вы найдете объяснения основных понятий, а также рабочий пример, который охватывает основные шаги для подключения и начала работы с этой базой данных NoSQL из Java.

2. Cassandra

Cassandra - это масштабируемая база данных NoSQL, которая обеспечивает непрерывную доступность без единой точки отказа и дает возможность обрабатывать большие объемы данных с исключительной производительностью.

В этой базе данных используется кольцевой дизайн, а не архитектура ведущий-ведомый. В схеме кольца нет главного узла - все участвующие узлы идентичны и взаимодействуют друг с другом как одноранговые узлы.

Это делает Cassandra горизонтально масштабируемой системой, допуская постепенное добавление узлов без необходимости перенастройки.

2.1. Ключевые идеи

Давайте начнем с краткого обзора некоторых ключевых понятий Кассандры:

  • Cluster - совокупность узлов или центров обработки данных, объединенных в кольцевую архитектуру. Каждому кластеру должно быть присвоено имя, которое впоследствии будет использоваться участвующими узлами

  • Keyspace - если вы работаете с реляционной базой данных, то схема является соответствующим пространством ключей в Cassandra. Пространство ключей является самым внешним контейнером для данных в Cassandra. Основными атрибутами для каждого пространства ключей являютсяReplication Factor,Replica Placement Strategy иColumn Families.

  • Column Family - Семейства столбцов в Cassandra похожи на таблицы в реляционных базах данных. Каждое семейство столбцов содержит набор строк, которые представленыMap<RowKey, SortedMap<ColumnKey, ColumnValue>>. Ключ дает возможность доступа к связанным данным вместе

  • Column - столбец в Cassandra - это структура данных, которая содержит имя столбца, значение и метку времени. Столбцы и количество столбцов в каждой строке могут различаться в отличие от реляционной базы данных, где данные хорошо структурированы.

3. Использование клиента Java

3.1. Maven Dependency

Нам нужно определить следующую зависимость Cassandra вpom.xml, последнюю версию которой можно найти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, нам нужно построить объект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, нам нужно определить еще два параметра:replicationFactor иreplicationStrategy. Эти параметры определяют количество реплик и то, как реплики будут распределены по кольцу соответственно.

Благодаря репликации 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 одним из лучших методов является использование шаблона «одна таблица на запрос». Это означает, что для другого запроса нужна другая таблица.

В нашем примере мы выбрали книгу по названию. Чтобы удовлетворить запросselectByTitle, мы создали таблицу с составнымPRIMARY KEY, используя столбцы,title иid. Столбец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")));
}

До сих пор все хорошо, но нужно понять одну вещь. Мы начали работать с таблицейbooks,, но тем временем, чтобы удовлетворить запросselect по столбцуtitle, нам пришлось создать другую таблицу с именемbooksByTitle.

Две таблицы идентичны и содержат повторяющиеся столбцы, но мы только вставили данные в таблицуbooksByTitle. Как следствие, данные в двух таблицах в настоящее время противоречивы.

Мы можем решить эту проблему с помощью запроса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. Заключение

В этом руководстве рассматриваются основные этапы подключения и использования базы данных Cassandra с Java. Некоторые ключевые концепции этой базы данных также обсуждались, чтобы помочь вам начать работу.

Полную реализацию этого руководства можно найти вGithub project.