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;"); ListmatchedKeyspaces = 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;"); ListcolumnNames = 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 ListselectAll() { 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); Listbooks = 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); Listbooks = 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 .