Um guia para Cassandra com Java
1. Visão geral
Este tutorial é um guia introdutório ao banco de dadosApache Cassandra usando Java.
Você encontrará os principais conceitos explicados, juntamente com um exemplo prático que cobre as etapas básicas para conectar-se e começar a trabalhar com este banco de dados NoSQL a partir de Java.
2. Cassandra
O Cassandra é um banco de dados NoSQL escalável que fornece disponibilidade contínua sem um único ponto de falha e oferece a capacidade de lidar com grandes quantidades de dados com desempenho excepcional.
Esse banco de dados usa um design de anel em vez de usar uma arquitetura mestre-escravo. No design do anel, não há nó mestre - todos os nós participantes são idênticos e se comunicam como pares.
Isso torna o Cassandra um sistema escalável horizontalmente, permitindo a adição incremental de nós sem a necessidade de reconfiguração.
2.1. Conceitos chave
Vamos começar com uma breve pesquisa de alguns dos principais conceitos do Cassandra:
-
Cluster - uma coleção de nós ou centros de dados organizados em uma arquitetura em anel. Um nome deve ser atribuído a cada cluster, que será subsequentemente usado pelos nós participantes
-
Keyspace - se você estiver vindo de um banco de dados relacional, o esquema é o respectivo keyspace no Cassandra. O espaço de chave é o contêiner mais externo para dados no Cassandra. Os principais atributos a serem definidos por keyspace sãoReplication Factor,Replica Placement StrategyeColumn Families
-
Column Family - famílias de colunas no Cassandra são como tabelas em bancos de dados relacionais. Cada família de colunas contém uma coleção de linhas representadas por umMap<RowKey, SortedMap<ColumnKey, ColumnValue>>. A chave permite acessar dados relacionados juntos
-
Column - Uma coluna no Cassandra é uma estrutura de dados que contém um nome de coluna, um valor e um carimbo de data / hora. As colunas e o número de colunas em cada linha podem variar em contraste com um banco de dados relacional em que os dados estão bem estruturados
3. Usando o cliente Java
3.1. Dependência do Maven
Precisamos definir a seguinte dependência de Cassandra empom.xml, a última versão pode ser encontradahere:
com.datastax.cassandra
cassandra-driver-core
3.1.0
Para testar o código com um servidor de banco de dados embutido, também devemos adicionar a dependênciacassandra-unit, a última versão da qual pode ser encontradahere:
org.cassandraunit
cassandra-unit
3.0.0.1
3.2. Conectando-se a Cassandra
Para conectar ao Cassandra a partir do Java, precisamos construir um objetoCluster.
Um endereço de um nó precisa ser fornecido como um ponto de contato. Se não fornecermos um número de porta, a porta padrão (9042) será usada.
Essas configurações permitem que o driver descubra a topologia atual de um cluster.
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. Criando o Keyspace
Vamos criar nosso keyspace “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);
}
Com exceção dekeyspaceName, precisamos definir mais dois parâmetros, oreplicationFactorereplicationStrategy. Esses parâmetros determinam o número de réplicas e como as réplicas serão distribuídas pelo anel, respectivamente.
Com a replicação, o Cassandra garante confiabilidade e tolerância a falhas, armazenando cópias de dados em vários nós.
Neste ponto, podemos testar se nosso espaço de chaves foi criado com sucesso:
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. Criação de uma família de colunas
Agora, podemos adicionar os primeiros "livros" da Família de Colunas ao espaço de chave existente:
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);
}
O código para testar se a família de colunas foi criada é fornecido abaixo:
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. Alterando a família da coluna
Um livro também tem um editor, mas nenhuma coluna pode ser encontrada na tabela criada. Podemos usar o seguinte código para alterar a tabela e adicionar uma nova coluna:
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);
}
Vamos nos certificar de que a nova colunapublisher foi adicionada:
@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. Inserindo Dados na Família de Colunas
Agora que a tabelabooks foi criada, estamos prontos para começar a adicionar dados à tabela:
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);
}
Uma nova linha foi adicionada na tabela ‘books ', para que possamos testar se a linha existe:
@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());
}
No código de teste acima, usamos um método diferente para criar uma tabela chamadabooksByTitle:
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);
}
No Cassandra, uma das práticas recomendadas é usar o padrão de uma tabela por consulta. Isso significa que, para uma consulta diferente, é necessária uma tabela diferente.
No nosso exemplo, optamos por selecionar um livro pelo título. Para satisfazer a consultaselectByTitle, criamos uma tabela com um compostoPRIMARY KEY usando as colunastitleeid. A colunatitle é a chave de particionamento, enquanto a colunaid é a chave de cluster.
Dessa forma, muitas das tabelas no seu modelo de dados contêm dados duplicados. Esta não é uma desvantagem deste banco de dados. Pelo contrário, essa prática otimiza o desempenho das leituras.
Vamos ver os dados que estão salvos atualmente em nossa tabela:
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;
}
Um teste para consulta que retorna os resultados esperados:
@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")));
}
Está tudo bem até agora, mas uma coisa precisa ser realizada. Começamos a trabalhar com a tabelabooks,, mas enquanto isso, para satisfazer a consultaselect pela colunatitle, tivemos que criar outra tabela chamadabooksByTitle.
As duas tabelas são idênticas, contendo colunas duplicadas, mas inserimos apenas dados na tabelabooksByTitle. Como conseqüência, os dados em duas tabelas são atualmente inconsistentes.
Podemos resolver isso usando uma consultabatch, que compreende duas instruções de inserção, uma para cada tabela. Uma consultabatch executa várias instruções DML como uma única operação.
Um exemplo dessa consulta é fornecido:
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);
}
Novamente, testamos os resultados da consulta em lote da seguinte maneira:
@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")));
}
Nota: A partir da versão 3.0, um novo recurso chamado “Visões Materializadas” está disponível, que podemos usar no lugar debatch consultas. Um exemplo bem documentado de “Visões Materializadas” está disponívelhere.
3.7. Excluindo a Família de Colunas
O código abaixo mostra como excluir uma tabela:
public void deleteTable() {
StringBuilder sb =
new StringBuilder("DROP TABLE IF EXISTS ").append(TABLE_NAME);
String query = sb.toString();
session.execute(query);
}
Selecionar uma tabela que não existe no keyspace resulta emInvalidQueryException: 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. Excluindo o Keyspace
Finalmente, vamos excluir o keyspace:
public void deleteKeyspace(String keyspaceName) {
StringBuilder sb =
new StringBuilder("DROP KEYSPACE ").append(keyspaceName);
String query = sb.toString();
session.execute(query);
}
E teste se o espaço de chave foi excluído:
@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. Conclusão
Este tutorial abordou as etapas básicas de conexão e uso do banco de dados Cassandra com Java. Alguns dos principais conceitos desse banco de dados também foram discutidos para ajudá-lo a iniciar.
A implementação completa deste tutorial pode ser encontrada emGithub project.