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 .