Рубрики
Без рубрики

Руководство по Cassandra с Java

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

Автор оригинала: baeldung.

1. Обзор

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

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

2. Кассандра

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

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

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

2.1. Ключевые Понятия

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

  • Кластер – совокупность узлов или центров обработки данных, расположенных в кольцевой архитектуре. Каждому кластеру должно быть присвоено имя, которое впоследствии будет использоваться участвующими узлами
  • Пространство ключей – Если вы исходите из реляционной базы данных, то схема является соответствующим пространством ключей в Cassandra. Пространство ключей-это самый внешний контейнер для данных в Cassandra. Основными атрибутами , устанавливаемыми для каждого пространства ключей, являются Коэффициент репликации , Стратегия размещения реплик и Семейства столбцов
  • Семейство столбцов – Семейства столбцов в Cassandra похожи на таблицы в реляционных базах данных. Каждое семейство столбцов содержит коллекцию строк, которые представлены Map SortedMap ColumnValue>> . Ключ дает возможность совместно получать доступ к связанным данным SortedMap ColumnValue>>
  • . Ключ дает возможность совместно получать доступ к связанным данным ColumnValue>>

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

3.1. Зависимость Maven

Нам нужно определить следующую зависимость Cassandra в pom.xml , последнюю версию которого можно найти здесь :


    com.datastax.cassandra
    cassandra-driver-core
    3.1.0

Чтобы протестировать код со встроенным сервером баз данных, мы также должны добавить зависимость cassandra-unit , последнюю версию которой можно найти здесь :


    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. Создание пространства ключей

Давайте создадим наше ” библиотека ” пространство ключей:

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 нам нужно определить еще два параметра: фактор репликации и 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);
}

В таблицу “книги” добавлена новая строка, поэтому мы можем проверить, существует ли эта строка:

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

В приведенном выше тестовом коде мы использовали другой метод для создания таблицы с именем книги по названию:

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

В нашем примере мы решили выбрать книгу по ее названию. Чтобы удовлетворить запрос select By Title , мы создали таблицу с составным ПЕРВИЧНЫМ КЛЮЧОМ , используя столбцы 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.

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

Мы можем решить эту проблему с помощью запроса batch , который содержит два оператора insert, по одному для каждой таблицы. Запрос 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, доступна новая функция под названием “Материализованные представления”, которую мы можем использовать вместо пакетных запросов. Хорошо документированный пример “материализованных представлений” доступен здесь .

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 .