Перейти к основному содержимому

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

· 8 мин. чтения

1. Обзор

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

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

2. Кассандра

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

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

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

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

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

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

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

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

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

<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. Подключение к Кассандре

Чтобы подключиться к 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 нам нужно определить еще два параметра, 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<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);
}

Убедимся, что новый издатель столбца добавлен:

@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 , используя столбцы, заголовок и идентификатор . Заголовок столбца — это ключ разделения, а столбец идентификатора — ключ кластеризации.

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

Посмотрим, какие данные в данный момент сохранены в нашей таблице:

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

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

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

Мы можем решить эту проблему с помощью пакетного запроса, состоящего из двух операторов вставки, по одному для каждой таблицы. Пакетный запрос выполняет несколько операторов 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 доступна новая функция «Материализированные представления», которую мы можем использовать вместо пакетных запросов. Хорошо документированный пример для «Materialized Views» доступен здесь .

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 .