Você está na página 1de 133

Bancos NoSQL têm ganhado cada vez mais popularidade nos últimos

tempos, podemos ver no ranking da Figura 1 que entre os dez SGBDs


mais populares, temos bancos como o MongoDB e o Cassandra, que
estão subindo cada vez mais na lista.

Figura 1. Top 10 SGBDs em popularidade

Porque esses bancos estão se tornando mais populares e outros, como


o Oracle, tem perdido um grande volume de adesão? Cada vez mais
temos sistemas que precisam trabalhar com volumes altíssimos de
transações por segundo e os bancos de dados precisam estar
preparados para aguentar a demanda. Um exemplo de um cenário de
alta demanda são as Black-Fridays, que todo ano tem batido recordes
de vendas no Brasil. Montar uma estrutura de banco de dados
relacional que atenda à demanda de evento desse tipo exige um alto
investimento, contudo, estamos falando de um único dia em que essa
demanda é muito acima do normal. Seria conveniente que, passado o
momento da Black-Friday, encolher a estrutura de banco de dados de
forma que o custo de manutenção seja muito menor, porém isso não é
muito simples de ser feito em uma estrutura de banco relacional.

É nesse tipo de cenário que alguns bancos NoSQL tem ganhado


popularidade. Eles possuem uma estrutura de dados muito mais
flexível e são, por concepção, feitos para trabalhar de forma
distribuída. Essas características permitem que eles lidem melhor
com altas demandas de processamento além de terem uma grande
capacidade de elasticidade, ou seja, em momentos que exigem alta
demanda, pode-se alocar um número maior de servidores, após esse
período, basta removê-los da estrutura e economizar na
infraestrutura.
Neste artigo será feita uma introdução a um dos bancos NoSQL mais
populares, o Cassandra, cobrindo suas características e casos de uso
mais comuns, além de alguns exemplos de como criar e consultar
estruturas de dados básicas dentro dele.

Introdução ao Cassandra

O Cassandra foi inicialmente desenvolvido pelo Facebook para ser


utilizado no motor de busca de sua caixa de entrada de mensagens.
Em 2008 ele se tornou open-source e em 2009 passou a ser mantido pela
Apache Foundation. Seu modelo de distribuição do sistema é baseado
no Dynamo (desenvolvido pela Amazon) enquanto a forma de
organização dos dados é baseado no BigTable (desenvolvido pelo
Google).

O Dynamo nasceu a partir da necessidade de se ter um banco de dados


simples, altamente escalável e confiável para lidar com grandes
demandas de leitura/escrita. Essa motivação veio de uma série de
momentos de indisponibilidade do site da Amazon durante a Black-
Friday de 2004 nos Estados Unidos causados em boa parte por
sobrecargas nos bancos relacionais utilizados na época, resultando em
prejuízo financeiro para o site de e-commerce. O Dynamo então foi
desenvolvido e empregado para tratar os sistemas que sofriam com
maior demanda como o de carrinho de compras e sessões de usuário.
Em 2007, a Amazon disponibilizou um documento descrevendo como
a arquitetura do Dynamo funcionava, esse documento serviu de base
para a criação de vários outros bancos NoSQL.

O BigTable também começou a ser desenvolvido em 2004 pelo Google,


também como uma solução altamente escalável e distribuída. O
desafio nesse caso era armazenar o volume imenso de dados de
indexação de todas as páginas web mapeadas pelo Google que então
era utilizado para alimentar o seu motor de buscas.

A distribuição oficial do Cassandra é compatível com todas as


distribuições do Linux e com o Mac OS. Existe também uma versão
compatível com o Windows distribuída pela DataStax. O Cassandra é,
por concepção, feito para trabalhar de forma distribuída, sendo que
não há grandes vantagens em trabalhar com ele utilizando apenas
uma máquina. Ao utilizar várias máquinas (também chamadas de
nós), vemos o verdadeiro potencial da solução.

Outra característica fundamental da arquitetura é o fato dela ser


descentralizada, ou seja, diferente da arquitetura master-
slave encontrada em outros sistemas de banco de dados, dessa forma,
todos os nós da rede possuem as mesmas funções e capacidades, não
há um ponto único de falha. Isso também facilita a manutenção da
solução já que não é necessário realizar configurações específicas
para cada nó. Os nós também não compartilham entre si nenhum tipo
de recurso de hardware como disco, processamento ou memória, esse
tipo de arquitetura é conhecido como shared-nothing. Isso evita
gargalos no sistema e permite que os nós sejam heterogêneos entre si.

Por ter uma arquitetura distribuída e descentralizada, uma solução


com Cassandra é altamente escalável, principalmente para escalar de
forma horizontal (conhecida também como escalabilidade elástica).
Caso seja necessária mais performance, basta adicionar mais nós na
rede, não é necessário a priori substituir as máquinas existentes por
servidores mais poderosos (ou escalar verticalmente). Da mesma
forma, se a demanda diminuir, pode-se remover alguns nós e
economizar na manutenção do sistema. Alie isso a uma infraestrutura
baseada em nuvem e é possível atingir um alto grau de eficiência de
uso de recursos de hardware.

Os dados das tabelas do Cassandra passam automaticamente por um


processo de particionamento utilizando a partition key da tabela que,
por padrão, é baseada na chave primária da tabela. As partições são
então distribuídas entre os nós do cluster utilizando uma técnica
conhecida como data sharding. As partições são replicadas em
múltiplos nós para prover alta disponibilidade e tolerância a falhas, o
número de réplicas é configurado na definição de um keyspace (o
conceito de keyspace será abordado mais adiante neste artigo).

Para determinar em que nó do cluster cada partição será gravada, são


definidas faixas baseadas no hash da chave utilizada para
particionamento, essas faixas são chamadas de tokens e são
representadas por um inteiro de 64 bits (ou seja, números que podem
variar de –263 até 263–1). Cada nó assume responsabilidade por um ou
mais tokens até cobrirem todas as faixas possíveis. Esse tipo de
arquitetura é conhecido como Arquitetura em Anel, como
representado pela Figura 2.
Figura 2. Representação
da arquitetura em anel

Para detectar nós com indícios de falha, o Cassandra emprega o gossip


protocol: de tempos em tempos os nós do cluster enviam pequenas
mensagens entre si, caso algum nó falhe sucessivamente ao responder
a essas mensagens, ele é marcado como um nó defeituoso e ações
corretivas são disparadas em background como replicar os dados que
estavam nesse servidor para outros nós. O Cassandra também é capaz
de detectar se um dado retornado por um dos nós está desatualizado,
nesse caso, outro nó será consultado em busca da versão mais recente
do dado e é disparado um processo secundário para correção do dado
desatualizado.

Por se tratar de um sistema distribuído, é importante entender os


níveis de consistência ao ler e gravar dados no Cassandra. Ao fazer a
inserção de um registro, pode-se informar que os dados devem ser
imediatamente replicados entre os nós responsáveis (o que é chamado
de consistência forte ou consistência imediata), ou, se a informação
não é crítica, pode-se instruir que os dados sejam replicados aos
poucos, conforme a disponibilidade do cluster (modo chamado de
consistência fraca ou consistência eventual). Esses níveis de
consistência também são aplicáveis ao efetuar comandos de leitura. É
importante ter em mente que quanto mais forte o nível de
consistência, mais oneroso é para o sistema e maior a latência ao
executar os comandos.

Estrutura dos dados no Cassandra


O Cassandra é um banco de dados NoSQL. Um banco NoSQL não
emprega os conceitos tradicionais de banco de dados relacional. Não
há definição de um schema, as tabelas normalmente não possuem
definições precisas de colunas e não existe o conceito
de constraints como chaves estrangeiras. Porém, isso não impede que
sejam utilizadas linguagens parecidas com o SQL para interagir com
sistemas NoSQL (às vezes referidos como Not Only SQL).

Guia relacionado: Guia de Referência NoSQL

Existem tipos diferentes de bancos de dados NoSQL, podemos listar os


seguintes:

• Orientado à grafos: bancos baseados na teoria dos grafos, são


úteis em cenários como geração de rotas ou redes de elementos
com várias interconexões, como redes sociais. O Neo4j é um
banco de dados que emprega esse conceito;
• Armazenamento de chave-valor: um dos tipos mais simples, os
valores são armazenados e indexados por meio de uma chave.
O Cassandra faz parte dessa categoria;
• Armazenamento colunar: neste tipo de banco os dados as
colunas das tabelas são armazenadas como estruturas de dados
separadas atrelados a uma chave primária. Um exemplo dessa
categoria é o HBase;
• Orientado à documentos: similar ao conceito de chave-valor,
mas o valor nesse caso é um documento que representa um
objeto complexo semiestruturado (normalmente na forma de
um JSON). O MongoDB é um banco de dados desse tipo.

Como citado anteriormente, o Cassandra é um banco NoSQL do tipo


chave-valor. Cada conjunto de chave-valor é equivalente ao que seria
uma coluna e um conjunto de colunas associadas a uma chave
primária compõem o que seria uma linha de uma tabela do Cassandra,
como representado na Figura 3.

Figura 3. Estrutura de uma linha de uma tabela no Cassandra


As tabelas no Cassandra têm um propósito muito similar às tabelas que
encontramos em bancos relacionais. Elas têm a função de agrupar
dados correspondentes à uma determinada entidade. Podemos, por
exemplo, ter uma tabela chamada usuário, que por sua vez pode ter as
colunas nome, sobrenome, telefone e email.

Agora, diferente de uma tabela de um banco relacional,


no Cassandra não é necessário popular todas as colunas ao criar uma
nova linha, de forma que podemos ter “larguras” diferentes para cada
uma delas (uma coluna de um banco relacional sempre precisa ser
preenchida com algum valor, mesmo que seja um valor nulo).
A Figura 4 representa esse conceito.

Figura 4. Representação de uma tabela do Cassandra

Ao gravar um valor em uma coluna, junto a ele é associado


um timestamp indicando o horário exato em que o valor foi gravado.
Isso é importante para detectar versões desatualizadas de valores
quando o Cassandra comparar o resultado de uma consulta feita em
vários nós diferentes.

Recapitulando as estruturas de dados básicas do Cassandra, de baixo


para cima temos:

• Colunas: representam um conjunto de chave/valor;


• Linhas: conjunto de colunas relacionados à uma chave primária;
• Tabelas: contêiner para um conjunto de linhas;
• Keyspace: um conjunto de tabelas.

A linguagem CQL

A linguagem CQL (Cassandra Query Language) é utilizada para interagir


com o Cassandra. A vantagem te se trabalhar com o CQL é que ele é
próximo do mais tradicional SQL, sendo familiar para quem vem do
mundo dos bancos relacionais.

Como no SQL, os comandos no CQL são divididos em três categorias


básicas, sendo comandos para:

• Definição das estruturas de dados;


• Manipulação dos dados;
• Consultas.

Entretanto, é importante lembrar que o CQL tem suas características


próprias que o diferem do SQL, além de outros tipos de dados. Mais
detalhes da linguagem serão apresentados na parte prática deste
artigo.

Criando um banco de dados no Cassandra

A seguir será apresentado como criar um banco de dados simples


dentro do Cassandra. Para isso, será utilizada a versão 3.7 que é a
versão estável mais recente disponível no momento da escrita deste
artigo. A primeira coisa a ser feita é criar um keyspace. Pode-se criar
um utilizando o comando CREATE KEYSPACE:

CREATE KEYSPACE IF NOT EXISTS "my_keyspace"


WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 1};

Nesse comando, estamos criando um


novo keyspace chamado my_keyspace. Estamos especificando
também como deverá ser tratada a replicação dos dados por meio do
parâmetro replication, que, por sua vez, possui duas propriedades,
sendo:
• class: indica qual o método de replicação será utilizando para
distribuir as réplicas das partições, podendo ser:
o SimpleStrategy: é o método padrão. Assume que o cluster é
composto de um único data center e irá criar um número de
réplicas de cada partição das tabelas desse keyspace igual ao
valor especificado no parâmetro replication_factor;
o NetworkTopologyStrategy: esse método permite que seja
definido um fator de replicação para cada data center que faz
parte do cluster. Nesse caso, o
parâmetro replication_factor não é utilizado, sendo necessário
passar um conjunto de chaves e valores em que a chave é o
nome do data center e o valor é a quantidade de réplicas para o
mesmo.
• replication_factor: indica quantos nós do cluster devem conter
uma cópia de cada partição das tabelas deste keyspace caso o
método de replicação seja o SimpleStrategy.

Tendo o keyspace criado, o próximo passo é adicionar tabelas a ele.


A Listagem 1 cria uma tabela chamada contacts.

Listagem 1. Comando CREATE TABLE


CREATE TABLE "my_keyspace"."contacts" (
contact_id uuid,
first_name text,
last_name text,
phone_number text,
PRIMARY KEY(contact_id)
);

O comando informa que seja criada uma tabela


chamada contacts dentro do keyspace my_keyspace que foi criado
anteriormente. Definimos também quatro colunas para essa
tabela, contact_id, que é a chave-primária e é do
tipo uuid (Universally Unique Identifier), assegura que os valores dessa
coluna serão únicos. As outras colunas
são first_name, last_name e phone_number, todas do tipo text.
Nesse caso, como a chave-primária é composta por apenas uma
coluna, ela também é a partition-key dessa tabela, o hash da partition-
key é o que define em que partição da tabela as linhas serão
armazenadas. No caso de uma chave-primária composta, apenas a
primeira coluna da chave vai ser utilizada como partition-key, as outras
colunas são chamadas de clustering columns.

Outra observação importante é que uma vez definida a composição da


chave-primária, ela não pode ser mais alterada, isso se deve ao fato de
todo o particionamento ser definido por meio da chave. Criada a
tabela, o próximo passo é inserir novas linhas nela, o que pode ser
feito utilizando o comando INSERT, conforme mostra a Listagem 2.

Listagem 2. Comando INSERT


INSERT INTO
"my_keyspace"."contacts" (contact_id, first_name, last_name, phone_number)
VALUES (uuid(), 'John', 'Doe', '559912341234');
INSERT INTO
"my_keyspace"."contacts" (contact_id, first_name, last_name)
VALUES (uuid(), 'Mary', 'Jane');
INSERT INTO
"my_keyspace"."contacts" (contact_id, first_name, phone_number)
VALUES (uuid(), 'Will', '441239876543');

Executando os comandos, inserimos três novas linhas na


tabela contacts. Propositalmente, nem todas as linhas são inseridas
com todas as colunas preenchidas, no Cassandra, as únicas colunas
que requerem um valor preenchido são as que compõem uma chave-
primária. Utilizamos também a função uuid() para gerar um
identificador único para a linha que será inserida. Por fim, vamos
exibir o conteúdo da tabela utilizando o comando SELECT,
conforme Listagem 3.

Listagem 3. Comando SELECT


SELECT * FROM "my_keyspace"."contacts";
contact_id | first_name | last_name | phone_number
--------------------------------------+------------+-----------+-------------
-
910ac22b-538e-42d2-a5cc-7163cdd5a0c3 | Will | null | 441239876543
e7beb210-b9b6-4b08-9afb-950d5918ba2e | John | Doe | 559912341234
7ef9a9af-1332-46bb-abb4-2deb9b335588 | Mary | Jane | null

Como esperado, podemos ver os três registros que foram inseridos


anteriormente. Um ponto importante é que não foi colocado nenhum
filtro na consulta, nesse caso, o Cassandra sempre retornará por
padrão as primeiras 1000 linhas da tabela. Ao adicionar filtros com
uma cláusula WHERE, é necessário sempre incluir uma chave no
filtro, isso também vale para os comandos UPDATE e DELETE.

Até então, salvo algumas particularidades, tudo é muito parecido com


o que existe na linguagem SQL, o que ajuda muito a quem vem do
mundo dos bancos relacionais. Entretanto, podemos ver as maiores
diferenças quando o assunto é a modelagem de dados.

Supondo que seja necessário alterar a tabela contacts para permitir


que seja gravado mais de um telefone por contato. O caminho natural
em um banco de dados relacional seria aplicar a primeira forma
normal criando uma segunda tabela em que seriam armazenados os
telefones relacionados ao contact_id da tabela original. Porém, isso
não é encorajado ao trabalharmos com o Cassandra, realizar um JOIN
entre tabelas, embora possível, é custoso pelo fato da possibilidade de
os dados estarem distribuídos entre vários nós diferentes do cluster.
Em sistemas com essas características, é importante fazer o menor
número de operações de I/O possível para garantir uma alta
performance. Nesse cenário, é recomendado o uso de uma coluna do
tipo Collection. Uma coleção pode ser uma lista de valores ou ainda
um outro conjunto de chaves e valores. Podemos exemplificar isso
criando um novo campo phone_numbers sendo uma lista de valores
do tipo texto com os comandos apresentados na Listagem 4.

Listagem 4. Criação de uma coluna do tipo coleção


ALTER TABLE my_keyspace.contacts ADD phone_numbers LIST<text>;
ALTER TABLE my_keyspace.contacts DROP phone_number;
UPDATE my_keyspace.contacts
SET phone_numbers = [ '441239876543', '441239876543' ]
WHERE contact_id = 910ac22b-538e-42d2-a5cc-7163cdd5a0c3;
SELECT phone_numbers FROM "my_keyspace"."contacts"
WHERE contact_id = 910ac22b-538e-42d2-a5cc-7163cdd5a0c3;
phone_numbers
----------------------------------
['441239876543', '441239876543']

Nesse caso, foi criada a nova coluna utilizando o tipo LIST<text>, com
isso, estamos informando uma coluna que é uma lista ordenada de
valores do tipo texto. Outros tipos de coleção disponíveis são SET (lista
não ordenada) e MAP (lista de chave-valor). Não é possível alterar a
coluna phone_number, que é do tipo texto, para um
tipo LIST<text>. O Cassandra não permite esse tipo de operação,
então o comando seguinte exclui essa coluna da tabela.

A seguir, é atualizado um dos registros agora com uma lista de


telefones e, por fim, um comando SELECT para exibir o resultado. No
caso da Listagem 4, foi atribuída uma lista fixa de números de
telefone na coleção, mas há diversas outras maneiras de manipular os
dados, conforme exemplifica a Listagem 5.

Listagem 5. Operações com coleções


UPDATE my_keyspace.contacts
SET phone_numbers = phone_numbers + [ '5511987654321' ]
WHERE contact_id = 910ac22b-538e-42d2-a5cc-7163cdd5a0c3;
UPDATE my_keyspace.contacts
SET phone_numbers = phone_numbers - [ '5511987654321' ]
WHERE contact_id = 910ac22b-538e-42d2-a5cc-7163cdd5a0c3;
UPDATE my_keyspace.contacts
SET phone_numbers[0] = '5511987654321'
WHERE contact_id = 910ac22b-538e-42d2-a5cc-7163cdd5a0c3;
UPDATE my_keyspace.contacts
SET phone_numbers = []
WHERE contact_id = 910ac22b-538e-42d2-a5cc-7163cdd5a0c3;

No primeiro caso, estamos adicionando um valor no final da lista


(igualmente, fazendo a declaração invertida como SET
phone_numbers = [ '5511987654321' ] + phone_numbers, irá inserir
os valores no início da lista). No segundo exemplo, estamos
removendo o valor 5511987654321 da lista independentemente da
posição que ele esteja. Podemos também especificar um valor para
uma posição específica da lista como no terceiro exemplo, nesse caso,
o valor anterior será substituído. Por fim, para limpar todos os valores
da lista, basta atribuir à coluna um valor como uma lista vazia como
demonstrado no quarto comando.

Uma coleção pode guardar até 64.000 elementos dentro dela, porém, é
importante salientar que elas são projetadas para guardar pequenos
conjuntos de dados, coleções muito extensas e com elementos muito
complexos impactam a performance de leitura dos dados. Em um
cenário desse tipo é mais indicado criar uma tabela à parte.

Outro recurso interessante no Cassandra é a criação de tipos


customizados. Tipos customizados também são tratados como
coleções e são úteis quando é necessário definir uma estrutura com
campos pré-definidos. Digamos que é necessário criar um tipo para
guardar o endereço de um contato, em que serão guardados o
logradouro, a cidade, o estado e o CEP. Podemos fazer isso utilizando
o comando CREATE TYPE como exemplificado na Listagem 6.

Listagem 6. Criação de um tipo customizado


CREATE TYPE my_keyspace.address (
street text,
city text,
state text,
zip_code int);

Ao definir uma coluna com esse tipo, é garantido que o campo não
terá nenhuma chave a mais do que as que estão descritas
(street, city, state e zip_code), contudo, não é obrigatório preencher
todos os valores.

Agora vamos adicionar uma nova coluna addresses na


tabela contacts que será uma coleção do tipo MAP<text, address>,
com isso, teremos uma coluna podendo conter vários endereços
indexados por um nome (Listagem 7).

Listagem 7. Adicionando o novo tipo address à tabela contacts


ALTER TABLE my_keyspace.contacts
ADD addresses MAP<text, FROZEN<address>>;
UPDATE my_keyspace.contacts
SET addresses = addresses + {'home':
{ street: 'Av. Paulista, 1', city: 'Sao Paulo', state: 'SP', zip_code:
1311000} }
WHERE contact_id = 910ac22b-538e-42d2-a5cc-7163cdd5a0c3;
SELECT phone_numbers, addresses FROM "my_keyspace"."contacts"
WHERE contact_id = 910ac22b-538e-42d2-a5cc-7163cdd5a0c3;
phone_numbers | addresses
----------------------------------+-----------------------------------------
['441239876543', '441239876543'] | {'home': {street: 'Av. Paulista, 1',
city: 'Sao Paulo', state: 'SP',
zip_code: 1311000}}

Podemos ver que para adicionar a nova coluna foi necessário definir o
tipo address como FROZEN, sem isso, o Cassandra não vai permitir
que a coluna seja criada. Isso se deve ao fato do Cassandra não
suportar por completo coleções aninhadas (tipos customizados são
tratados como coleções) devido à forma como os dados da coluna são
serializados. Ao definir uma coleção como FROZEN não é possível
alterar atributos individuais dela (como alterar somente o valor do
atributo street da coleção address de uma linha já existente da
tabela), é necessário substituir a coleção por completo. Após criar a
nova coluna, inserimos um novo endereço com a chave ‘home’ para
um dos contatos. Por fim, podemos ver o resultado com o comando
SELECT.

Como explicado anteriormente, o Cassandra não permite fazer


consultas com filtros sem que haja uma chave entre as cláusulas. Se
tentarmos fazer uma consulta para trazer todos os contatos com o
nome ‘Mary’, o Cassandra retornará o erro apresentado na Listagem
8.

Listagem 8. SELECT sem chave na cláusula WHERE


SELECT first_name, last_name FROM my_keyspace.contacts
WHERE first_name = 'Mary';
InvalidRequest: code=2200 [Invalid query] message="Cannot execute this query
as it
might involve data filtering and thus may have unpredictable performance.
If you want to execute this query despite the performance unpredictability,
use ALLOW FILTERING"

Ele indica que se for executada uma consulta desse tipo, a


performance é imprevisível. Isso acontece pois se essa tabela tivesse
bilhões de linhas, possivelmente vários nós do cluster precisariam ser
consultados para conseguir trazer o resultado desejado, seria
basicamente o equivalente a um full table scan em um banco
relacional, mas em uma escala muito maior. Podemos forçar a
execução da consulta adicionando a instrução ALLOW FILTERING no
final do comando como explicado na mensagem, mas isso não é
recomendado a não ser que a tabela seja muito pequena.
Contudo, há uma forma melhor de resolver isso criando um índice
secundário. Assim como em um banco relacional, no Cassandra é
possível criar índices para colunas que não fazem parte da chave
primária. Para isso, é utilizado o comando CREATE INDEX
apresentado na Listagem 9.

Listagem 9. Criação de um índice secundário


CREATE INDEX ON my_keyspace.contacts ( first_name );
SELECT first_name, last_name FROM my_keyspace.contacts
WHERE first_name = 'Mary';
first_name | last_name
------------+-----------
Mary | Jane

Com o índice criado, os dados são retornados normalmente. No


entanto, o uso de índices secundários não é eficiente quando:

• A coluna tem uma cardinalidade muito alta ou muito baixa (seus


valores variam bastaste ou muito pouco). Nesses casos a
consulta pode acabar envolvendo muitos nós do cluster;
• A coluna tem valores frequentemente alterados ou apagados.
Isso torna a manutenção do índice onerosa, já que ele também
precisa passar por constantes alterações e também ser replicado
entre as máquinas.

TL (Time to Live)

Um recurso interessante e muito útil do Cassandra é a possibilidade


de atribuir a um valor de uma coluna um tempo de vida, um recurso
chamado TTL (abreviação de Time to Live). Passado esse tempo, o
valor será automaticamente removido da tabela. O tempo é descrito
em segundos e é definido utilizando o comando USING TTL como no
exemplo da Listagem 10.

Listagem 10. Definição do tempo de vida de um valor


INSERT INTO my_keyspace.contacts (contact_id, first_name, last_name)
VALUES (uuid(), 'Temporary', 'Temporary') USING TTL 600;
SELECT ttl(first_name), ttl(last_name) from my_keyspace.contacts
WHERE contact_id = deec4e61-1aac-442c-bd4e-d5038f900710;
ttl(first_name) | ttl(last_name)
-----------------+----------------
430 | 430

No exemplo, foi definido um TTL de 600 segundos (ou 10 minutos).


Também é possível consultar o tempo de vida restante utilizando a
função ttl(). Podemos ver que restam 430 segundos nesse caso para
que os valores sejam apagados da tabela. A função ttl() irá retornar
nulo caso não haja um tempo de vida definido para o valor. É possível
definir tempo de vida no comando UPDATE, a diferença é que o
comando USINT TTL deve estar antes da palavra-chave SET.

Níveis de consistência

Outra característica muito importante do Cassandra é a possibilidade


de definir o nível de consistência para as operações de leitura e escrita
no banco. Como descrito no início do artigo, os dados ficam
distribuídos entre os nós do cluster por meio das partições que por sua
vez podem ter réplicas de acordo com o fator de replicação definido
na criação do keyspace.

No momento em que um dado é escrito, é possível definir um nível de


consistência que irá dizer quantos nós precisarão ser acionados para
gravar os dados. Quanto menor o nível de consistência, mais rápida e
menos onerosa é a operação, porém nem todas as réplicas da partição
onde foi inserido o novo dado terão a última versão do mesmo. Em
contrapartida, quanto mais alto o nível de consistência definido, mais
lenta e onerosa é a operação, pois ela só é considerada como
concluída quando a quantidade de réplicas necessárias for atualizada.
Os possíveis níveis de consistência que podem ser definidos estão
descritos na Tabela 1. Da mesma forma, é possível definir níveis de
consistência de leitura (Tabela 2).

Tabela 1. Níveis de consistência de escrita


Nível Descrição

Garante que o dado seja escrito em pelo menos uma


réplica antes de retornar a resposta para o
ANY
cliente. Hints (BOX 1) são consideradas como uma
escrita bem-sucedida.

Garante que o dado seja escrito em pelo menos um,


ONE, TWO,
dois ou três nós antes de retornar uma resposta para
THREE
o cliente.

Similar ao ONE, mas com a premissa de que o dado


LOCAL_ONE seja escrito em pelo menos uma réplica no
mesmo data center.

Garante que o valor foi escrito com sucesso na


QUORUM maioria das réplicas (calculado por: (fator de
replicação / 2) + 1).

Similar ao QUORUM, mas com a premissa de que


LOCAL_QUORUM
todas as réplicas sejam do mesmo data center.
Nível Descrição

Também similar ao QUORUM, mas garante que o


EACH_QUORUM valor seja escrito na maioria das réplicas em todos
os data centers.

Garante que o valor seja escrito em um número de


réplicas igual ao especificado no fator de
ALL replicação. A operação é considerada como falha
caso não seja possível escrever em uma ou mais
réplicas.

BOX 1. HINT

Uma hint é gerada dentro do Cassandra quando um nó falha em responder ao

tentar executar uma operação de escrita nele, funcionando como uma espécie

de lembrete. As hints são gravadas em uma tabela chamada system.hints e só

são geradas caso o nível de consistência exigido no momento da escrita já foi

atingido. Eventualmente, quando o nó volta a responder, o servidor que

armazenou a hint vai tentar escrever novamente a informação e remover a hint

caso bem-sucedido.

Tabela 2. Níveis de consistência de leitura


Nível Descrição

Retorna o valor mais atualizado encontrado em um,


dois ou três nós (de acordo com o valor
ONE, TWO, especificado). Após retornar a informação para o
THREE cliente, um processo é criado em background para
atualizar um valor que esteja desatualizado em uma
ou mais réplicas, se necessário.

Similar ao ONE, mas com a premissa de que o dado


LOCAL_ONE
seja lido em um nó no mesmo data center.

Consulta todos os nós até que a maioria das réplicas


tenha respondido (fator de replicação / 2) + 1)
retornando a versão mais atualizada. Após retornar
QUORUM a informação para o cliente, um processo é criado
em background para atualizar um valor que esteja
desatualizado em uma ou mais réplicas, se
necessário.

Similar ao QUORUM, mas com a premissa de que


LOCAL_QUORUM os nós que responderem sejam todos do
mesmo data center.
Nível Descrição

Também similar ao QUORUM, mas com a


EACH_QUORUM premissa de haja a resposta da maioria dos nós em
todos os data centers.

Consulta todos os nós e aguarda a resposta de todos


eles retornando a versão mais atualizada. Após
retornar a informação para o cliente, um processo é
ALL
criado em background para atualizar um valor que
esteja desatualizado em uma ou mais réplicas, se
necessário.

O nível de consistência é definido por conexão. Podemos consultar ou


definir o nível de consistência utilizando o comando CONSISTENCY
conforme apresentado na Listagem 11.

Listagem 11. Definição de consulta do nível de consistência de leitura

e escrita
CONSISTENCY;
Current consistency level is ONE.
CONSISTENCY LOCAL_ONE;
Consistency level set to LOCAL_ONE.

Entender o nível de consistência exigido para cada caso de uso da


aplicação é importante para não onerar demais o banco sempre
exigindo altos níveis de consistência. Onde não é estritamente
necessário sempre ter o dado mais atualizado, é recomendado utilizar
níveis de consistência mais baixos.
O Cassandra é um ótimo banco de dados para começar a entender
soluções NoSQL para quem sempre teve contato com a linguagem SQL
e bancos relacionais, tornando a curva de aprendizado mais suave em
comparação ao MongoDB, por exemplo. No entanto, é importante se
atentar às suas particularidades da modelagem de dados, já que
empregar a normalização de tabelas pode trazer um grande impacto
negativo na performance do banco.

Ele é uma ótima solução de alta disponibilidade e escalabilidade


quando é preciso trabalhar com sistemas que requerem volumes de
leitura e escrita extremamente alto e em que sempre há a chave-
primária em mãos. É ótimo também para soluções em nuvem graças
ao formato de sua arquitetura derivada do Dynamo, permitindo uma
alta elasticidade, sendo assim, pode-se adequar a complexidade da
infraestrutura de acordo com a demanda de leitura/escrita necessária.

Por outro lado, o Cassandra não é interessante para ser usado em


cenários com baixa demanda de leitura e escrita (a não ser que seja
esperado que essa demanda cresça rapidamente com o tempo).
Também não é o melhor candidato para ser utilizado como banco de
dados analítico, por ter muitas limitações quando não são feitas
consultas utilizando a chave-primária, existem outras soluções que
podem desempenhar melhor esse papel, como por exemplo, o HBase.

Atualmente, grande parte dos sistemas já opera através da Internet.


Como consequência disso, eleva-se a quantidade potencial de usuários
e passa-se a expor as limitações das tecnologias tradicionais.

Em muitos casos essa exposição se deu pelo fato dos sistemas terem
apresentado um crescimento bastante elevado do número de acessos,
o que culminou em um aumento exponencial do volume de dados a tal
ponto que os bancos relacionais passaram a ter dificuldades em
processar as requisições com um tempo de resposta satisfatório.

A solução, então, seria escalar o banco de dados verticalmente,


adicionando mais recursos de hardware numa mesma máquina, de
forma a garantir um desempenho aceitável para o sistema.

Entretanto, os custos com isso podem se tornar proibitivos, assim


como em algum momento o limite dessa escalabilidade pode ser
alcançado. Diante disso, os bancos de dados relacionais se tornaram
um gargalo na arquitetura desses sistemas.

Essas limitações fizeram com que os pesquisadores buscassem


alternativas para melhorar o desempenho e, a partir daí, criaram
opções de replicação de dados dos bancos relacionais em vários nós
(mestre-escravo, mestre-mestre) e os particionamentos vertical e
horizontal.

Dessa forma, foi criada a possibilidade de se escalar horizontalmente


um banco de dados relacional. Entretanto, os pesquisadores notaram
que para grandes volumes de dados, o custo de se manter uma
estrutura de hardware para escalar horizontalmente de forma
satisfatória era proibitivo.

Esse elevado custo se dava por conta das características ACID


(Atomicidade, Consistência, Integridade e Disponibilidade) do banco
de dados. Para garantir essas propriedades, o banco terminava por
fazer pesquisas em todos os nós do cluster de dados a fim de realizar
as operações de JOIN, precisava fazer leituras (muitas vezes em vários
nós) antes de escrever ou atualizar os dados, entre outros detalhes.

Todo esse comportamento levou a um custo muito alto para se realizar


consultas, aumentando o tempo das mesmas de tal forma que se
tornaram inviáveis. Nesse momento, negócios que necessitavam de
respostas rápidas, principalmente os de operações críticas,
começaram a sofrer com essas dificuldades e tiveram que buscar
alternativas.

A opção que se encontrou foi baseada no teorema de CAP, o qual


conceitua que é impossível, para um sistema distribuído, garantir as
características de consistência (só existe um único valor em todo o
cluster para um mesmo registro), disponibilidade (é possível executar
operações com sucesso a qualquer momento/tempo razoável) e
tolerância a falhas (sistema continua a operar mesmo se um nó tiver
falha de rede).

Portanto, só é possível construir sistemas distribuídos que atendam a


no máximo duas das características do teorema de CAP, sendo
necessário, portanto, flexibilizar as regras de armazenamento de
dados em troca de maior escalabilidade e performance.
Esse conceito é válido até mesmo para os bancos de dados relacionais,
quando se opta por escalá-los horizontalmente, pois se houver falha
de rede em algum nó, algumas consultas podem não ser executadas.

Obviamente, com o aumento do volume de dados, se faz necessário


escalonar horizontalmente, a fim de aumentar a capacidade de
processamento e armazenamento, diminuir custos (nós simples em
vez de máquinas poderosas) e aumentar a disponibilidade.

Essa necessidade fez com que surgissem os bancos de dados NoSQL,


como o Cassandra, o qual faz uso do conceito de consistência
eventual, garantindo apenas a disponibilidade e tolerância a falhas.

A consistência eventual significa que se nenhuma atualização for feita


a um registro a partir de determinado momento, eventualmente, ou
seja, quando todas as atualizações anteriores tiverem sido replicadas
em todos os nós do cluster, todos os acessos àquele registro retornarão
o valor mais atualizado.

O problema é que antes que o dado mais atualizado esteja disponível


em todos os nós do cluster de dados, não necessariamente o valor
retornado a uma consulta será o mais atualizado, pois a replicação dos
dados demora um certo tempo para ocorrer, criando uma janela de
inconsistência.

Por exemplo, se um cluster Cassandra possui três nós e houver uma


atualização em um registro trocando a letra “A” por “B”, enquanto o
valor B não for atualizado em todos os nós, uma consulta pode
consultar um nó em que o valor ainda seja “A” (a escolha do nó é
aleatória), pois a replicação não terminou, ocasionando uma
inconsistência.

O Cassandra possui formas de contornar essa limitação ao possibilitar


a configuração do nível de consistência. Entretanto, não chega a
garantir a consistência, como nos sistemas relacionais.

Dessa forma, esta solução NoSQL é a melhor opção para situações em


que se tenha um grande volume de dados, necessite de alta
disponibilidade, tolerância a falhas e que não seja necessário
trabalhar sempre com o dado mais recente.
Um bom exemplo de aplicação que pode tirar proveito dessa
característica é o carrinho de compras da Amazon. O modelo de
negócio da empresa assume que é melhor pedir desculpa ao usuário
por um eventual erro, no caso de qualquer inconsistência (nos dados,
compra efetuada de forma errada, falha na compra, etc.), do que arcar
com os custos de garantir a consistência com um banco de dados
relacional.

Quando ocorre esse tipo de situação, a empresa pede desculpas e


oferece uma série de vantagens ao cliente, como bons descontos em
produtos ou créditos para serem gastos na loja. Dessa forma, além de
incentivar uma nova compra, a empresa tem o custo de operação
diminuído significativamente ao adotar essa estratégia. O “custo” dos
incentivos aos clientes que passaram pelo problema é diluído pelo
maior volume de compras que o sistema pode lidar.

Além disso, existem vários casos de sucesso da utilização do


Cassandra, em vários ramos de negócio, como, por exemplo: catálogo
de produtos, redes sociais, detecção de fraudes e aplicações analíticas
no geral. Devido a isso, tem sido adotado por milhares de empresas de
diferentes áreas, como a Amazon (comércio eletrônico), eBay
(comércio eletrônico), Netflix (serviço de assinatura de filmes e séries
de TV), Facebook (rede social), CERN (centro de pesquisa nuclear),
FedEx (logística), Globo.com (portal de notícias), Microsoft (software),
Credit Suisse (banco de investimento) e até mesmo a NASA (agência
espacial estadunidense).

Como era de se esperar, o Cassandra não foi escolhido por acaso. Ele é
altamente escalável, possui uma arquitetura P2P tolerante a falhas,
um modelo de dados versátil e flexível e uma linguagem de consulta
com baixa curva de aprendizado. Todas essas características fazem
com que o Cassandra seja o repositório perfeito para aplicações que
precisam estar sempre disponíveis e que operam com grandes
volumes de escrita e leitura de dados. Com esta solução NoSQL é
possível atender a milhões de transações por segundo, em grandes
volumes de dados, fazendo uso de milhares de servidores.

No entanto, um dos grandes desafios que novos projetos encontram


ao adotar o Apache Cassandra é que a modelagem de dados é bem
diferente. As abordagens tradicionais se baseiam em bancos
relacionais e já possuem uma metodologia bem estabelecida, fruto de
décadas de pesquisas.
Por sua vez, por ter uma abordagem diferente e recente, existem
poucas metodologias para a modelagem de dados não-relacionais. A
primeira tentativa e a mais utilizada até o momento, foi criada por três
pesquisadores da Wayne State University, entre os quais se destaca
Artem Chebotko, arquiteto de soluções da DataStax, uma empresa de
software que fornece uma versão comercial do Apache Cassandra.
Essa abordagem que será demonstrada ao longo deste artigo.

O processo de modelagem relacional é bastante focado nos dados,


pois procura entender e organizar os dados de forma relacionada, de
tal forma a minimizar a redundância e duplicação dos mesmos. Além
disso, as consultas feitas ao banco de dados, a princípio, não
interferem no processo de modelagem, ou seja, não se modela
pensando nas consultas que a aplicação deseja fazer.

Durante esse processo, a análise e otimização de consultas é uma


atividade muitas vezes não executada, uma vez que se tem uma
linguagem poderosa como a SQL que ajuda na obtenção dos dados.
Tudo isso resulta em um projeto que ajuda a evitar a duplicação,
impondo regras bastante restritas sobre os dados, mas que não é
otimizado para realizar consultas que exigem processamento em
grandes de volumes de dados e curto tempo de resposta.

Em contrapartida, a forma de estruturação e armazenamento de


dados do Cassandra foi projetada com o intuito de prover uma
performance superior para as consultas que a aplicação precisa
executar.

Dito isso, a modelagem de dados para o Cassandra se inicia com as


consultas, pois o que se mais objetiva é a agilidade na realização das
operações em detrimento da consistência dos dados. Ao contrário do
que prega a cartilha da modelagem relacional, para o Cassandra a
regra é encadear os dados pertencentes a uma mesma consulta e
desnormalizar as entidades relacionais, com o objetivo de que
consultas complexas possam ser executadas acessando uma única
tabela.

Por conta disso, é bastante comum que o mesmo dado seja


armazenado em múltiplas tabelas, a fim de suportar várias formas de
consultas, o que resulta na duplicação de dados.
Portanto, é preciso entender como modelar a estrutura de
armazenamento dos dados da aplicação no Cassandra, levando em
consideração as consultas que serão executadas.

Para isso, existe um processo de modelagem padrão bem definido, o


qual será demonstrado ao longo do artigo, partindo das etapas de
identificação do fluxo da aplicação, de forma simultânea com a
modelagem do modelo conceitual, culminando no modelo físico, o
qual será utilizado para criar o esquema de banco de dados desejado.

O Modelo de Dados do Cassandra


Um esquema de banco de dados no Cassandra é denominado keyspace.
Este nada mais é do que uma estrutura de dados na qual todos os
outros objetos do banco de dados residem, além de uma série de
atributos de configuração que não são o foco deste artigo. Conforme
a Figura 1, dentro do keyspace são definidas uma série de tabelas para
armazenar os dados de uma aplicação, e mais adentro, pode-se
perceber que cada tabela possui linhas e colunas. A essa estrutura dá-
se o nome de modelo de dados, o qual representa os mecanismos
internos que o Cassandra faz uso, no intuito de armazenar os dados.

Figura 1. Modelo de dados do Cassandra.

Tabelas (Famílias de Colunas)


Uma tabela, ou família de colunas no Cassandra, é a estrutura de
dados onde são armazenados os dados da aplicação. Neste modelo,
cada tabela é formada por um conjunto de linhas, e cada linha, por
um conjunto de colunas.

Ainda no Cassandra, cada tabela pode ou não ser subdividida em


vários subconjuntos de linhas, chamados de partições, os quais podem
ter apenas uma única linha ou mais. Além disso, podem existir três
tipos de chaves ou identificadores em cada linha: chaves de partição
(partition keys), chaves de ordenação (clustering keys) e chaves
primárias (primary keys). Entretanto, as chaves de partição e primária
são obrigatórias (sempre existem, ainda que as duas sejam a mesma),
enquanto as de ordenação são opcionais.

A chave de partição é um conjunto de uma (chave simples) ou mais


colunas (chave composta) de uma linha que identifica de qual partição
(subconjunto) da tabela a linha faz parte. No entanto, não basta
apenas identificar a partição à qual a linha pertence, pois, o Cassandra
não saberia em que ordem as linhas devem ser armazenadas em cada
partição.

Desse modo, esse é o propósito da chave de ordenação, a qual também


é definida por um conjunto de uma ou mais colunas cujos valores
servem como critério de ordenação. Por sua vez, a junção entre as
chaves de partição e de ordenação identificam unicamente uma linha
em uma partição, sendo assim chamada de chave primária.

Devido a essa característica, existem partições que só podem ter uma


linha, porque enquanto a chave da partição é obrigatória, a de
ordenação é opcional. Assim, na ausência da chave de ordenação, o
Cassandra assume que a chave de partição também é a chave primária
da linha, e nestes casos, como não haveria como identificar mais de
uma linha, a partição poderá ter no máximo uma linha.

A definição da estrutura da tabela, na linguagem utilizada para operar


o Cassandra, a CQL (Cassandra Query Language), especifica um
conjunto de colunas e uma chave primária, ou seja, para se criar uma
tabela no Cassandra, é necessário apenas informar o nome e o tipo de
dado (primitivos: int, text ou compostos/coleções: set, list ou map) de
cada coluna e quais são as colunas que compõem a chave primária.
Dessa forma, todas as linhas da tabela terão a mesma estrutura, ainda
que em partições diferentes.
Além disso, também é possível definir uma coluna com o tipo de
dados counter, o qual é um tipo especial, usado para manter um
contador distribuído, a fim de realizar operações de agregação em um
cluster de máquinas como, por exemplo, contar o número de visitas
realizadas a um site.

No entanto, se houver uma coluna do tipo counter, todas as outras


que não são desse tipo devem fazer parte da chave primária, pois só
assim o Cassandra consegue realizar as operações de agregação.
Ademais, similarmente à coluna de tipo counter, existem outros tipos
de coluna, como a opção estática (static), a qual apresenta o mesmo
valor em todas as linhas de uma partição. Obviamente, esse tipo de
coluna só faz sentido numa tabela com partições com múltiplas
linhas.

Para demonstrar esses conceitos, imagine a tabela Usuário conforme


a Tabela 1.

email (Chave) nome senha dataCriacao


patrick@gmail.com Patrick a78vJY& 1444085502
diogo@hotmail.com Diogo Gw!6-89 1222085502
eduardo@uol.com Eduardo KD4Uq& 1333085502

Tabela 1. Exemplo de tabela Usuário.

Sendo email a chave primária, ela também é a chave da partição, pois


como mencionado anteriormente, na ausência de uma chave de
ordenação, a chave primária e a chave da partição serão iguais. Logo,
cada linha que representa um usuário nessa tabela está alocada em
uma partição (partição de única linha).
Figura 2. Tabela com partições de única linha, conforme estrutura
da Tabela 1.

A Figura 2 demonstra, de forma gráfica, como fica a estrutura de


partições de uma única linha, tendo como base a Tabela 1. Com o
objetivo de recuperar os dados com mais agilidade o Cassandra
distribui cada partição de uma tabela em um nó do cluster de dados.

Assim, supondo que tenhamos um cluster composto por três


máquinas, cada máquina vai conter uma partição, ou seja, uma
máquina armazenará a partição cuja chave é patrick@gmail.com, outra
com diogo@hotmail.com e a terceira com eduardo@uol.com. Portanto,
quando for realizada uma pesquisa pelo usuário cujo e-mail
é patrick@gmail.com, o Cassandra irá consultar apenas uma máquina,
a qual possui aquele dado específico, consultando pela chave da
partição/linha. Por outro lado, quando existem partições com várias
linhas (vide Figura 3), a abordagem é um pouco diferente.
Figura 3. Tabela de partições de múltiplas linhas.

Uma tabela com partições com múltiplas linhas tem a chave primária
composta pela coluna que representa a chave da partição e pela
coluna que representa a chave de ordenação. Essa figura, em conjunto
com a Tabela 2, ilustram exatamente esse conceito, onde a chave da
partição é a coluna year e a chave de ordenação/identificador da linha
é a coluna name. Dessa forma, se uma pesquisa filtrar por year = 2014,
consultará um único nó do cluster, o qual contém a partição cujo
identificador é 2014, retornando duas linhas.

No entanto, é preciso ter cuidado com o projeto de partição de


múltiplas linhas, pois cada partição no Cassandra deve estar
inteiramente armazenada em um único disco rígido, uma vez que não
é possível dividir uma mesma partição em vários nós do cluster, a fim
de evitar queda na performance das consultas.

year (Chave partição) name (Chave ordenação) id runtime


2014 Interestellar 2 98
2014 Mockingjay 3 113

Tabela 2. Demonstração das chaves de partição e ordenação da tabela


da Figura 3.

Nota: Para um entendimento mais aprofundado acerca da estrutura interna do

Cassandra, a forma de utilização dos tipos de dados, entre outras informações

que estão fora do escopo deste artigo, recomenda-se a leitura da documentação

oficial da DATASTAX, bem como a leitura do artigo de Otávio Santana no site


da DevMedia. Os endereços para acessá-los estão disponíveis na seção Links e

estão intitulados “DataStax - Documentação do Cassandra” e “DevMedia -

Artigo sobre Cassandra em Java”, respectivamente.

Fluxo de Modelagem de Dados


Após uma breve introdução ao modelo de dados do Cassandra, a partir
deste tópico será apresentado o processo de modelagem seguindo
uma metodologia escolhida pelo autor. Entretanto, para iniciar a
modelagem é preciso conhecer os requisitos de negócio que devem
ser traduzidos em funcionalidades da aplicação. Para tanto, observe o
cenário a seguir.

“Uma empresa deseja desenvolver uma aplicação para exibição de


vídeos de forma gratuita, por meio da internet. A aplicação será
disponibilizada globalmente na Internet fazendo uso de centros de
processamento distribuídos em três países: China, Estados Unidos e
Brasil. A expectativa é que a aplicação que irá funcionar nessa
infraestrutura tenha milhões de usuários por todo o mundo.

Cada usuário deve ser capaz de se cadastrar informando alguns dados


(e-mail, senha, nome), logar na aplicação usando e-mail e senha,
pesquisar vídeos, enviar vídeos, fazer comentários em vídeos, bem
como visualizar vídeos de forma anônima ou logado.

O sistema deve armazenar a data de cadastro do usuário no ato de seu


cadastro para relatórios posteriores. Os usuários podem adicionar
quantos vídeos quiserem e devido à quantidade de dados esperada, a
empresa está preocupada em como lidar com esse volume em
tamanha escala, como também com o desempenho da aplicação.
Entretanto, ela afirmou que a eventual perda de alguns dados não é
crítica para o negócio, tendo em vista que é uma plataforma de
entretenimento. ”

Este breve enunciado expõe, de forma sucinta, os requisitos


necessários para a aplicação solicitada pelo cliente. Ao analisá-los,
nota-se claramente que eles podem ser atendidos fazendo uso do
Cassandra, pois temos:
· Necessidade de bancos de dados em múltiplos centros de
processamento;

· Grande volume de dados e transações (milhões de usuários, número


ilimitado de vídeos);

· Perda de informação é tolerável;

· Preocupação com escalabilidade e alto desempenho.

Subjetivamente, pode-se entender também que não é necessária a


recuperação dos dados mais recentes em todas as ocasiões como, por
exemplo, na listagem de comentários de um vídeo e na listagem de
vídeos, pois nessas ocasiões o que importa é mostrar algum resultado
para o usuário, ainda que não seja o melhor possível. Dito isso,
iniciemos o processo de modelagem de dados conforme os passos
definidos no diagrama da Figura 4.

Nesta imagem estão demonstrados os passos ou tarefas que devem ser


seguidos com o intuito de criar um modelo de banco de dados para
uma aplicação que deseja fazer uso do Cassandra, conforme uma
metodologia escolhida pelo autor.

Como são muitos passos no processo, por questões didáticas, optou-se


por dividi-los em três grandes passos, a saber: Modelagem
Conceitual/Fluxo da Aplicação, Modelagem Lógica e Modelagem
Física.

abrir imagem em nova janela

Figura 4. Processo de modelagem de dados para o banco de dados


Cassandra.

Passo 1: Modelagem conceitual e fluxo da


aplicação
O princípio de toda modelagem de dados é conceituar ideias ou
contextos de negócio, transformando-os em uma linguagem ou
modelo (modo de descrever algo, como um diagrama, por exemplo)
que possa ser entendido da forma mais clara possível, sem espaço
para ambiguidades. Por isso essa atividade é importante, pois em
geral, várias pessoas de áreas diferentes precisam entender, ainda que
de forma superficial, como os dados estão estruturados na aplicação.

Em paralelo à modelagem conceitual, quando se modela os dados


para bancos de dados não relacionais, como é o caso do Cassandra,
existe outra tarefa igualmente importante a ser executada, a análise
do fluxo da aplicação. Essa tarefa se refere a identificar as consultas
que devem ser realizadas pela aplicação, de acordo com os requisitos
de negócio, sintetizando-as em um diagrama.

Como o primeiro passo do processo é executar as atividades


supracitadas, esta seção as aborda de forma teórica, com o intuito de
auxiliar na compreensão das mesmas.

Modelo Conceitual
Para fazer a modelagem conceitual será utilizada a técnica de
modelagem entidade-relacionamento (ERM – Entity-Relationship
Model), a qual visa construir um modelo que descreve dados,
informações, domínios de negócio e até mesmo requisitos da
aplicação de forma abstrata. Os principais componentes desse modelo
são as entidades e os relacionamentos que existem entre elas.

O intuito desse modelo é dar uma visão geral de como as entidades de


uma aplicação se relacionam e que dados elas possuem, se abstendo
de especificar detalhes técnicos de implementação. Ao conhecer o
escopo dos dados da aplicação através desse modelo é possível
transformá-lo em outros mais detalhados, que serão a base para a
implementação do repositório de dados.

Por isso, para este artigo é interessante que se tenha algum


conhecimento em modelagem entidade-relacionamento, a fim de
melhor compreender os diagramas que aqui serão apresentados.

O primeiro conceito que se pode abstrair dos requisitos do software é


a ideia de usuário da aplicação, ou simplesmente usuário. Esse
conceito se obtém da percepção de que um dos elementos-chave para
o funcionamento da aplicação é a presença do usuário, o qual é uma
pessoa que faz uso do sistema que se quer desenvolver. Então, a
primeira entidade a ser criada é a entidade Usuário (vide Figura 5).

Figura 5. Modelagem da entidade Usuário com seus respectivos


atributos.

No enunciado com os requisitos estão explícitos os dados pertinentes


ao usuário que devem ser considerados. Esses dados
são email, nome, senha e dataCadastro. Como no modelo entidade-
relacionamento toda entidade deve ter um atributo que seja um
identificador, foi escolhido o email, o qual está sublinhado na imagem,
destacando-o como tal.

A segunda entidade que deve ser criada é a que representa o vídeo.


Logo, denominaremos essa entidade como Vídeo. Esta entidade possui
uma série de atributos, como data de publicação, título, duração e
sequencial (identificador numérico que faz parte de uma sequência, o
qual incrementa 1 ao último valor inserido).

Conforme os requisitos de negócio, a modelagem deve ser de feita tal


forma que cada vídeo no modelo de dados só pode ser publicado por
um único usuário e um usuário pode publicar vários vídeos. Dito isso,
seguindo os conceitos de ERM, a relação entre as
entidades Usuário e Vídeo deve ser modelada através de um
relacionamento identificador (vide BOX 1) entre vídeo e usuário, no
qual o vídeo é identificado pela chave primária do usuário (email) e
um sequencial, pois um vídeo não existe sem um usuário. Portanto,
cada vídeo será identificado unicamente por duas chaves: a
chave email da entidade forte e o atributo sequencial da própria
entidade.
BOX 1. Relacionamento identificador

Um relacionamento identificador ocorre quando uma entidade não faz sentido

se separada de outra. Nestes casos temos uma "entidade fraca", por exemplo:

Empresa possui Filial. Filial é uma entidade fraca de empresa, possuindo,

portanto, um relacionamento identificador. Assim, a chave primária de Filial

provavelmente será composta pela chave primária de Empresa (a qual também

será uma chave estrangeira) e um número que a identifica, como no código a

seguir:

· Empresa(idEmpresa, Nome, ...);

· Filial(idEmpresa, idFilial, Endereço, ...).

O enunciado dos requisitos também informa que deve existir uma


funcionalidade pela qual o usuário pode fazer comentários em
qualquer vídeo. Por consequência, existe uma relação entre um vídeo
e seu respectivo comentário. Logo, o modelo de dados deve possuir
uma terceira entidade denominada Comentário.

Os comentários, conforme a Figura 6, possuem atributos como texto


(conteúdo digitado pelo usuário), data de publicação e sequencial
(identificador numérico que faz parte de uma sequência). Entretanto,
também é preciso levar em consideração que cada usuário logado
pode publicar um ou mais comentários sobre determinado vídeo e,
obviamente, cada vídeo pode receber vários comentários de diversos
usuários diferentes. Ademais, como não existe comentário sem
usuário e sem vídeo, Comentário é uma entidade fraca, com
relacionamento identificador com Usuário e com Vídeo.

Desta forma, cada comentário é unicamente identificado pelo usuário


que o publicou e pelo vídeo ao qual aquele comentário se refere e
mais um sequencial, para possibilitar que haja mais de um comentário
por usuário e por vídeo.
Figura 6. Modelo de dados conceitual da aplicação em diagrama ERM.

Fluxo da aplicação
Com o modelo conceitual preparado é preciso definir o fluxo de
funcionamento da aplicação, de forma a identificar os pontos de
otimização do modelo a fim de atender às requisições feitas à base de
dados.

No fluxo da aplicação se especifica quais as tarefas/operações a


aplicação executa (interações que os usuários realizam), como
também as respectivas dependências (interações anteriores
obrigatórias para se realizar a atual) e em que ordem.

Além disso, são definidos os padrões de acesso (consultas que o


sistema realiza para conseguir executar determinada tarefa) no fluxo
da aplicação, os quais ajudam a determinar como os dados são
acessados e a ordem das consultas realizadas. Logo, cada tarefa ou
interação do usuário identificada no fluxo deve possuir ao menos uma
consulta a banco de dados associada.

Um bom exemplo da relação entre uma tarefa e um padrão de acesso


(consulta) é a ação/tarefa de logar na aplicação e a consulta à
tabela Usuário no banco de dados, informando o e-mail do usuário que
deseja logar, a fim de validar a existência do mesmo no sistema.

Toda vez que um usuário tentar logar na aplicação, sempre haverá


uma consulta ao banco de dados pelo e-mail fornecido, gerando um
vínculo entre a atividade executada pelo usuário (tarefa) e a respectiva
consulta. Assim, se constrói um diagrama com correlações entre
tarefas e consultas.
Portanto, para identificar o fluxo da aplicação é necessário observar
os requisitos desejados (no cenário apresentado no início do tópico
“Fluxo de Modelagem de Dados”) a fim de deduzir algumas tarefas que
a aplicação proposta necessita, e depois disso, identificar as consultas
que podem ser associadas a cada uma delas. Dito isso, para este artigo
serão consideradas as seguintes tarefas:

· Usuário logar na aplicação (site);

· Mostrar informações básicas do usuário;

· Mostrar os vídeos adicionados recentemente no site;

· Visualizar vídeo no site;

· Listar comentários de um usuário;

· Listar comentários de um vídeo;

· Pesquisar vídeo por data ou título;

· Listar vídeos de um usuário.

É importante salientar que as tarefas aqui mencionadas são aquelas


que exigem consultas (padrões de acesso) ao banco de dados.
Obviamente, existem outras inúmeras possibilidades de tarefas a
serem executadas como, por exemplo, enviar um vídeo para a
aplicação.

No entanto, tarefas que são de escrita serão desconsideradas para o


mapeamento do fluxo, pois para projetar o banco segundo a
metodologia abordada, devemos considerar apenas as consultas, a fim
de otimizar o planejamento do banco para realizar a leitura de dados
da forma mais rápida possível.

Dito isso, ao modelar o fluxo da aplicação, o diagrama resultante é


apresentado na Figura 7.
abrir imagem em nova janela

Figura 7. Fluxo da aplicação conforme os padrões de acesso.

Observando essa imagem, nota-se que há uma correlação entre as


tarefas (retângulos) e os padrões de acesso ou consultas (linhas).
Percebe-se também que há uma ordem definida pela numeração de
cada padrão de acesso (Q1 a Q8) e pelo fluxo das setas. A letra “Q”,
disposta nas setas e no início de cada linha de padrão de acesso, se
refere a uma query, ou consulta, ao banco de dados.

Outro ponto importante a se notar no diagrama é que existem dois


retângulos no mesmo nível, no topo do diagrama, os quais não
possuem nenhuma seta proveniente de outro retângulo. Eles estão
modelados dessa forma porque não guardam relação de dependência
com uma tarefa anterior, visto que o usuário tanto pode logar na
aplicação como pode visualizar os vídeos adicionados recentemente
de forma anônima.

Passo 2: Modelagem Lógica


A criação do modelo lógico segue uma abordagem top-down, ou seja,
se modela tendo em mente os conceitos mais abstratos até os mais
concretos e detalhados. Dessa forma, em primeiro lugar se divide a
aplicação em partes, cada uma representando um tipo de conceito,
para posteriormente detalhar cada uma, analisando um problema por
vez.
Esse tipo de abordagem pode ser definido de forma algorítmica ou
programática, ou seja, é possível fazer uso de uma linguagem de
programação e devido a isso, existem ferramentas que automatizam
esse processo.

Para o Cassandra uma das ferramentas é a KDM, a qual, inclusive,


pode ser acessada de forma gratuita, exclusivamente pelo site.
Entretanto, o escopo deste artigo não inclui a utilização de
ferramentas de automação, pois o foco é na aprendizagem do
processo.

Para realizar o mapeamento do modelo conceitual para o lógico, faz-


se uso do modelo conceitual e do fluxo da aplicação, definidos
anteriormente. Ademais, também existe uma série de regras de
mapeamento e padrões que podem ser utilizados para auxiliar na
criação do esquema lógico e assegurar que o mesmo esteja correto e
funcione conforme esperado.

Figura 8. Processo de criação do modelo lógico.


Como pode ser observado na Figura 8, o modelo conceitual é
representado pelo diagrama entidade-relacionamento, também
chamado de ERD, demonstrado na figura como um artefato de saída
do retângulo “Modelo Conceitual” e os padrões de acesso são as
consultas identificadas no fluxo da aplicação, estando representada
como “Consultas”, numa linha de saída do retângulo “Padrões de
Acesso”.

Portanto, as consultas e o diagrama ERD servem de insumo para a


aplicação de regras e padrões de mapeamento (retângulo amarelo), a
qual é uma tarefa intermediária para a tarefa de criação do modelo
lógico. Como derivação desse processo de mapeamento tem-se o
modelo lógico, o qual é diagramado utilizando-se a notação de
Chebotko, que representa, de forma visual, a correlação de tabelas
(em vez de tarefas, como no diagrama do fluxo da aplicação) e
consultas (padrões de acesso).

A notação de Chebotko é utilizada tanto para documentar o modelo


lógico quanto o modelo físico. A diferença é que enquanto no modelo
lógico se tem apenas as tabelas com suas respectivas colunas e
propriedades, no modelo físico é apresentado, além disso, os tipos de
dados da coluna e as otimizações realizadas para se instanciar uma
tabela no banco de dados com a linguagem CQL.

É importante mencionar que o processo demonstrado na Figura


8 ficará mais claro ao longo do artigo e o objetivo, nesse momento, é
apenas fazer uma breve descrição geral.

Para criar um modelo consistente é preciso seguir alguns princípios


de modelagem de dados, analisados a seguir:

· Conheça os dados: Ao olhar para os componentes de um modelo


conceitual (entidades, relacionamentos, atributos,
chaves/identificadores e cardinalidades) é preciso compreender os
tipos de dados que são capturados pelo mesmo.

Com os tipos de dados em mãos, pode-se definir o que deve ser


armazenado no banco de dados, lembrando sempre de preservar suas
propriedades, ou seja, se devem ser únicos, multivalorados, de
tamanho específico (no caso de text), entre outras, de tal forma que os
dados sejam organizados a fim de obter um bom desempenho nas
consultas.
Além do mais, as chaves/identificadores afetam diretamente a
maneira como o modelo lógico deve ser projetado, uma vez que as
chaves de entidades e relacionamentos se tornam chave primária
(simples ou composta) em uma tabela do modelo lógico. Devido a isso,
a definição da chave primária é extremamente importante,
principalmente por causa das possíveis consultas, as quais só podem
ser realizadas por itens da chave primária e também devido à forma
de ordenação dos dados na tabela que se deseja, uma vez que caso
haja uma chave de ordenação, ela faz parte da chave primária;

· Conheça as consultas: As consultas também afetam diretamente o


modelo como um todo, pois as tabelas são projetadas com base nelas,
e devido a isso, o modelo deve passar por ajustes se as consultas
mudarem.

Dito isso, existem algumas estratégias de se modelar para atender as


consultas, como utilizar uma partição por consulta (ideal) e utilizar
mais de uma partição por consulta (aceitável). Entretanto, não se deve
fazer consultas em todas as partições da tabela ou fazer consultas em
mais de uma tabela para obter o resultado final, visto que esse tipo de
operação ocasiona um grande custo de processamento, pois será
necessário consultar em mais de um nó do cluster e,
consequentemente, o tempo de resposta será maior.

O Cassandra consegue rodar várias consultas em paralelo e uma vez


que cada partição está distribuída em um nó diferente do cluster,
fazer uso de uma partição por consulta é o modo mais eficiente de se
recuperar os dados. Nesse momento é importante lembrar que as
tabelas podem ser projetadas para cada partição conter uma ou mais
linhas, adequando as partições de acordo com a quantidade de
registros a guardar.

Assim, se existe uma consulta da qual se espera um único resultado


como, por exemplo, buscar o usuário por e-mail (no ato do login),
então deve-se modelar uma linha por partição; do contrário, modela-
se uma partição com várias linhas, pois será esperado mais de um
resultado, como em uma listagem de vídeos publicados pelo usuário;

· Encadeie os dados: O encadeamento de dados é considerado a


principal técnica de modelagem para o Cassandra. Encadear os dados
significa organizar múltiplas entidades correlacionadas em uma
mesma partição, colocando atributos de mais de uma entidade numa
mesma tabela, de tal forma a suportar a estratégia de uma partição
por consulta.

Deste modo, com uma única consulta é possível obter resultados mais
completos, que podem servir para mais de uma tarefa do fluxo da
aplicação, diminuindo assim a quantidade de tabelas e consultas
necessárias no banco de dados. Por exemplo, uma mesma consulta
poderia servir para as tarefas “Usuário loga na aplicação” e “Mostrar
vídeos adicionados pelo usuário”, bastando encadear as entidades
Usuário e Vídeo numa mesma tabela.

Basicamente, existem três formas de encadeamento de dados no


Cassandra: chaves de ordenação (gerando partições com várias
linhas), colunas com coleções de dados (set, list, etc.) ou colunas de
tipos de dados definidos pelo usuário (UDTs – User Defined Types).

A adoção de chaves de ordenação, identificadas pela letra C e uma seta


para cima ou para baixo, conforme a Figura 9, é o mecanismo mais
utilizado para o encadeamento de dados, pois possui uma série de
características interessantes.

Nesta mesma imagem, a chave da partição, a qual possui a letra K do


lado direito, especifica uma entidade/tabela na qual outras entidades
serão encadeadas. Os valores das chaves de ordenação, por sua vez,
identificam unicamente as entidades encadeadas. Ademais, se houver
mais de uma chave de ordenação, significa que há vários níveis de
encadeamento.

Logo, na Figura 9 é possível observar uma tabela actors_by_video cujo


objetivo é listar os atores de acordo com o respectivo vídeo
(tabela Vídeos da figura) em que atua. Note ainda que essa tabela
possui múltiplos níveis de encadeamento, pois apresenta duas chaves
de ordenação: actor_name e character_name.
Figura 9. Encadeamento de múltiplos níveis.

O segundo mecanismo de encadeamento de dados mais utilizado é a


criação de um UDT (User Defined Type), ou tipo de dado definido pelo
usuário, o qual é um tipo de dados que possui uma estrutura
customizada.

Ao contrário dos tipos de dados primitivos e complexos, o UDT é um


tipo de dado especial que pode ser utilizado para representar uma
entidade com o intuito de colocá-la numa tabela que possui colunas de
outra entidade, sinalizando um relacionamento do tipo um para um
(1-1).

Por exemplo, na Figura 10 há uma tabela com uma coluna


chamada user_id, a qual pertence à entidade Usuário, e uma
coluna videos, cujo tipo de dado é video_type.

Este tipo é um UDT que representa uma entidade hipotética de mesmo


nome, para referenciar os tipos de vídeo. Logo, numa única consulta
pelo user_id é possível atender à tarefa de “Listar todos os vídeos
publicados por um usuário”, uma vez que será retornado o usuário
consultado com sua respectiva coleção de video_types, os quais
possuem os dados relacionados a cada vídeo.
Figura 10. Encadeamento com tipo de dado definido pelo usuário
(UDT).

· Duplicar dados. A duplicação de dados é o último dos princípios de


modelagem para o Cassandra, mas não menos importante.

Neste ponto é válido salientar que a utilização dos dois princípios


anteriores pode resultar em duplicação de dados, uma vez que tabelas
diferentes respondem a consultas diferentes e, portanto, se possuírem
colunas iguais, haverá duplicação de um mesmo dado em mais de
uma tabela.

Com a duplicação dos dados as consultas são pré-executadas e


materializadas (o resultado esperado da consulta já é gravado numa
tabela especificada), não sendo necessário, portanto, realizar a
filtragem de dados ou qualquer cálculo, pois tudo já terá sido feito
anteriormente, diminuindo assim o tempo de resposta e o custo de
processamento.

Saiba também que existem várias formas de se duplicar os dados,


como em tabelas, partições e/ou linhas, ou seja, dados iguais podem
estar em tabelas, partições e até mesmo em linhas de uma mesma
tabela. No exemplo da Figura 11, o título do vídeo está duplicado por
tabela.
Figura 11. Duplicação de dados por tabela.

Regras de mapeamento
As regras de mapeamento são orientações a serem seguidas a fim de
mapear as entidades do modelo conceitual para o modelo lógico.

Essas regras, categorizadas em cinco etapas, protegem o modelo


contra eventuais erros ao projetar cada tabela para atender a uma
única consulta, retornando o resultado na ordem desejada. Vamos a
elas:

1. Mapeamento de Entidade e Relacionamento;

2. Mapeamento de Atributos de Busca Igualitária (filtro de igualdade


=);

3. Mapeamento de Atributos de Busca Desigual (filtros de


desigualdade >=, <=, >, <);

4. Mapeamento de Atributos de Ordenação;

5. Mapeamento de Atributos Identificadores.

Nota: Saiba que as regras de mapeamento não são passos para a construção de

um Modelo Entidade Relacionamento (MER), pois apesar de existir um MER

como modelo conceitual, as regras de mapeamento apenas fazem uso do

mesmo para criar um modelo lógico sem relações entre entidades, no qual

cada tabela atende a uma consulta identificada no fluxo da aplicação.

Regra 1 – Mapeamento de Entidade e


Relacionamento
As entidades e relacionamentos devem ser mapeados para tabelas, nas
quais cada linha representa uma instância ou unidade da entidade a
qual ela representa e seus respectivos relacionamentos.

Ao fazer isso, a intenção da regra é criar um modelo de dados com


partições de múltiplas linhas, nas quais se armazenam dados relativos
a uma ou mais entidades, diminuindo a fragmentação das linhas em
vários nós diferentes, pois cada partição fica armazenada em um
único nó, evitando assim uma queda na performance.

Nessa regra, os atributos das entidades provenientes do modelo


conceitual devem se tornar colunas nas tabelas do modelo lógico.
Além disso, os dados do modelo conceitual devem ser preservados no
modelo lógico, o que não impede que o mesmo dado possa ser
duplicado por várias tabelas, partições e linhas. A Figura 12 mostra
um exemplo de modelagem da tabela usuario, seguindo as diretrizes
do mapeamento.

Figura 12. Modelagem da tabela Usuario conforme a regra 1.

Continuando a abordagem da regra de mapeamento 1, os


relacionamentos devem se transformar em tabelas, de tal forma que
os atributos das entidades relacionadas sejam representados por
colunas nas mesmas. Neste ponto é válido destacar que a
cardinalidade do relacionamento afeta diretamente o projeto da chave
primária e eventuais escolhas erradas na composição da mesma
podem levar a tabelas que não conseguem realizar pesquisas por
determinados atributos, pois no Cassandra só é possível realizar
pesquisas através dessa chave.

Além disso, saiba que todo relacionamento entre duas entidades tem o
potencial de gerar duas tabelas, cada uma representando uma direção
do relacionamento, uma vez que todo relacionamento é, por natureza,
bidirecional. Logo, como demonstrado na Figura 13, o
relacionamento 1-N entre as entidades Usuario e Video pode (ou não, a
depender da necessidade do negócio) gerar as
tabelas videos_do_usuario, a qual tem o intuito de listar os vídeos de
determinado usuário, obtendo os usuários com seus respectivos
vídeos, e usuarios_do_video, que, por sua vez, tem a intenção oposta,
de listar os usuários de um determinado vídeo.

Ao observar a figura, enquanto videos_do_usuario suporta consultas


pelo identificador do usuário (a coluna usuario_id é chave primária -
K) e opcionalmente pelo identificador do vídeo (a coluna video_id é
chave de ordenação - C), usuarios_do_video suporta apenas consultas
pelo identificador do vídeo, uma vez que no Cassandra só é possível
realizar consultas utilizando chaves primárias (obrigatório) e chaves
de ordenação (opcional). Logo, não é possível consultar
pelo usuario_id em usuarios_do_video.

abrir imagem em nova janela

Figura 13. Mapeamento do relacionamento entre as entidades


Usuário e Vídeo.

Nota: Nada impede que se escolha colocar a coluna usuario_id como chave de

ordenação na tabela usuarios_do_video, a fim de possibilitar a consulta por ela.

Entretanto, para o caso de uso do negócio que seria obter o usuário de um

determinado vídeo, a utilização do identificador do vídeo como chave primária

é suficiente.

Ademais, não faria sentido pesquisar informando além do video_id, o

identificador do usuário, pois já saberíamos, de antemão, de qual usuário se

trata. Portanto, a escolha de qual coluna vai ser que tipo de chave é meramente
objetiva, levando em conta apenas os tipos de consultas que a tabela deve

suportar.

Regra 2 – Mapeamento de Atributos de


Busca Igualitária
Quando se pensa em consultas no Cassandra, deve-se analisar quais
delas precisam de buscas igualitárias (utilizando o sinal de igual, =) e
quais precisam de buscas desiguais (isto é, que fazem uso de sinais
como maior que, menor que, menor ou igual, maior ou igual).

Para atender à consulta, os atributos das entidades que precisam do


critério de igualdade devem estar presentes na chave primária da
tabela, do contrário a pesquisa não pode ser realizada.

Por conseguinte, as consultas suportadas pela tabela devem,


obrigatoriamente, incluir todas as chaves de partição na mesma, pois
sem elas não é possível encontrar a partição em que o dado se
encontra, impossibilitando a busca do mesmo. A Figura 14 demonstra
os componentes da chave primária que podem ser utilizados para a
realização de buscas igualitárias.

Figura 14. Componentes da chave primária que podem ser filtrados


por busca igualitária.

Ao aplicar essa regra para a entidade Vídeo, pegam-se os atributos da


consulta que precisam ser filtrados pelo sinal de = e transforma-os em
chaves primárias em uma tabela. A Figura 15 demonstra algumas
possibilidades de modelagem, caso fosse necessário realizar filtragens
pelo título e pela data de publicação de um vídeo, por exemplo.
Figura 15. Exemplo de aplicação da regra 2 para a entidade Vídeo.

Outro exemplo de aplicação da regra 2 é apresentado na Figura 16,


agora para tabelas que representam relacionamentos entre entidades
do modelo conceitual. Neste caso, a relação entre as
entidades Usuário e Vídeo poderia ser traduzida em duas tabelas
(videos_do_usuario_1 e videos_do_usuario_2), as quais visam atender à
consulta “Listar vídeos de um determinado usuário informando o
nome e a data de criação da conta do mesmo no sistema”. Logo, os
atributos dataCriacao e nome da entidade Usuário devem ser filtrados
por igualdade (nome = X e dataCriacao = Y), transformando-os em
chaves de partição (especificado como K na legenda), no caso
de videos_do_usuario_1, e em chave de partição e ordenação
(especificado como C), no caso de videos_do_usuario_2.

Note que essas tabelas apresentam maneiras diferentes de responder


à mesma consulta, sendo que na primeira os resultados serão
ordenados pelo sequencial do vídeo de forma ascendente, enquanto
na segunda serão ordenados primeiro pelo nome e em seguida pelo
sequencial, ambos de forma ascendente.

abrir imagem em nova janela


Figura 16. Mapeando relacionamento entre Usuario e Video conforme
a regra 2.

Regra 3 – Mapeamento de Atributos de


Busca Desigual
As buscas desiguais, como mencionado anteriormente, se referem a
filtros nas consultas que fazem uso de sinais de desigualdade. Todos
os atributos que possuem a necessidade de serem filtrados dessa
maneira se tornam chaves de ordenação (clustering columns) na chave
primária da tabela. Por conseguinte, as colunas que se tornam chaves
de ordenação devem ser definidas na chave primária, após as colunas
que participam de buscas igualitárias, como expõe a Figura 17. É
importante mencionar que as chaves de partição não podem ser
utilizadas em filtros de desigualdade e não podem ser utilizadas mais
de uma chave de ordenação em buscas desiguais.

Figura 17. Colunas da chave primária e tipos de buscas.

Aplicando a regra 3 à entidade Usuário, levando em consideração o


atributo de busca igualitária o nome e o atributo de busca
desigual dataCriacao, tem-se o diagrama apresentado na Figura 18.
Note que foi colocado o nome como chave da partição e o e-mail
passou a ser chave de ordenação, a fim de também permitir a busca
pelo mesmo.

Figura 18. Mapeamento da entidade Usuário com a regra 3.

A regra 3 também pode ser aplicada a relacionamentos, como no caso


da Figura 19, que demonstra a aplicação dessa regra ao
relacionamento entre as entidades Usuário e Vídeo. Neste caso, temos
uma consulta que informa o e-mail do usuário (busca de igualdade) e
uma data de publicação do vídeo maior que (busca desigual) uma data
especificada, ou seja, algo como “e-mail = algumemail@xxx.com AND
dataPublicacao > XXXXX”.

abrir imagem em nova janela

Figura 19. Mapeamento do relacionamento entre Usuario e Video


conforme a regra 3.

Regra 4 – Mapeamento de Atributos de


Ordenação
Nesta regra, define-se que os atributos de ordenação das entidades
especificadas no modelo lógico passam a ser chaves de ordenação
(clustering columns) numa tabela. Feito isso, as linhas da tabela serão
ordenadas de acordo com os valores das chaves de ordenação, de
forma ascendente ou descendente, conforme pode ser observado
na Figura 20.

Figura 20. Chaves/colunas de ordenação na chave primária.

Ao aplicar a regra 4, supondo que se deseja fazer uma consulta pelo


nome e pela data de criação do usuário maior que uma determinada
data, ordenada de forma ascendente pela data de criação, a
modelagem fica de acordo com a Figura 21.
Figura 21. Modelagem da tabela usuarios_por_data_criacao

Agora, ao aplicar a regra 4 para relacionamentos, analisando o


exemplo da relação entre usuário e vídeo, a fim de atender uma
consulta que busca uma lista de vídeos publicados pelo usuário após
uma determinada data, ordenando o resultado da data mais antiga
para mais recente, teremos como resultado a tabela exposta na Figura
22.

abrir imagem em nova janela

Figura 22. Modelagem da tabela videos_do_usuario a partir de uma


consulta especificada.

Regra 5 – Mapeamento de Atributos Chave


ou Identificadores
A chave primária da tabela, derivada da aplicação da regra 5, deve
conter as colunas identificadoras da entidade no modelo conceitual,
sem que seja necessária uma preocupação com a posição (acima ou
abaixo) e a ordenação destas.

Com o intuito de permitir consultas específicas, no entanto, essa


chave pode conter algumas colunas adicionais. Dito isso, para uma
melhor compreensão da aplicação da regra 5, a Figura 23 demonstra
duas modelagens possíveis para a entidade Usuário.
Figura 23. Modelagem da entidade usuário, conforme a regra 5.

A primeira tabela modelada, denominada usuarios, passou a ter


o email como único representante da chave primária, sendo também
chave de partição.

Essa modelagem é perfeita para uma consulta em que se deseja


apenas obter os dados do usuário informando o e-mail. Por outro lado,
se fosse necessário pesquisar pelo nome do usuário, não seria possível
nesta tabela, pois o atributo nome não compõe a chave primária.

A fim de solucionar esse problema, pode-se definir também a


tabela usuarios_por_nome, demonstrada na mesma figura, na qual o
nome é a chave da partição e o e-mail a chave de ordenação, ambos
compondo a chave primária da tabela.

Aplicando as regras de mapeamento ao


modelo do exemplo
A primeira relação que será modelada é entre as entidades Usuário e
Vídeo. Por ser do tipo 1-N, esse relacionamento sinaliza que cada
Usuário pode publicar um ou mais Vídeos. Logo, uma das possíveis
consultas que se pode deduzir para essa relação é a “listar vídeos do
usuário de forma ordenada (mais antigo para o mais novo ou vice-
versa) ”.

Assim, de acordo com a regra 1 as entidades e seus relacionamentos


devem se tornar uma tabela.

Para uma modelagem otimizada, recomenda-se que cada


relacionamento entre duas entidades seja modelado como uma tabela.
Assim, devemos criar uma apenas para a relação entre as entidades
Usuário e Vídeo. Essa restrição é necessária para viabilizar uma
economia de espaço na tabela por conta do limite de dados por
partição (quanto mais encadeamento de dados, mais dados na
partição) e direcionar o modelo para consultas específicas, o que por
consequência otimizará o tempo necessário para retornar o resultado
desejado.

Seguindo, então, a regra 1, tem-se videos_do_usuario, a qual uma


consulta pelo usuário deve retornar todos os vídeos publicados pelo
mesmo.

A segunda regra se refere ao mapeamento de atributos de buscas


igualitárias, ou seja, aqueles atributos que devem fazer parte do filtro
de igualdade da consulta de forma a retornar o resultado desejado.
Pensando nisso, nota-se claramente que em videos_do_usuario é
preciso filtrar por igualdade os dados do identificador do usuário.

A regra informa ainda que os atributos que necessitam participar


dessa filtragem devem compor a chave primária. Dessa forma, o
segundo passo é definir a coluna email como chave primária,
identificando-a com a letra K.

O terceiro passo do mapeamento é a aplicação da terceira regra, a


qual especifica que atributos de buscas desiguais devem se tornar
chaves de ordenação e, portanto, devem fazer parte da chave
primária.

Como em nosso caso é preciso realizar uma filtragem por


desigualdade pelo atributo dataPublicacao da entidade Vídeo,
permitindo ordenar o resultado de forma ascendente (mais antigos
primeiro) ou descendente (mais recentes primeiro) – a fim de atender
à consulta mencionada no primeiro parágrafo deste tópico – define-se
a coluna data_publicacao com a letra C (alusão à clustering column) e
com a seta ↑, resultando em C↑.

A seta para cima significa ordem ascendente. Se quiser modelar com a


ordem descendente, utiliza-se a seta apontando para baixo. É válido
ressaltar que toda chave de ordenação precisa ter especificada a sua
forma de ordenação.

A quarta regra é um pouco parecida com a terceira, no que diz


respeito à chave de ordenação. A ideia dessa regra é definir na tabela
os atributos que precisam ser ordenados para a consulta,
transformando-os em chaves de ordenação.

Visto que em nosso exemplo há um atributo que é desejável que seja


ordenado para o retorno da consulta, o identificador do Vídeo,
especifica-se a coluna sequencial_video como uma chave de ordenação.

A quinta regra se refere à composição da chave primária, a qual deve


conter o identificador de cada entidade no relacionamento.

Conforme a aplicação das regras anteriores, até o momento a chave


primária a ser gerada já possui as seguintes
colunas: email, sequencial_video e data_publicacao. Logo, os requisitos
para a regra 5 já foram cumpridos com a coluna email, pois é o
atributo identificador de uma entidade, e sequencial_video, como o
identificador da outra.

A Figura 24 ilustra a tabela resultante da aplicação das regras de


mapeamento. A partir dela, uma consulta pelo identificador do
usuário vai retornar todos os vídeos já publicados pelo mesmo, de
forma ascendente.

Figura 24. Tabela resultante da aplicação das regras de mapeamento.

Passo 3: Modelagem Física


O último passo se refere a dois blocos do diagrama do processo de
modelagem: otimização do modelo e modelagem do modelo físico
(vide Figura 4).

Esses dois blocos fazem parte de uma grande atividade, que é a


criação do modelo físico. O primeiro objetiva encontrar problemas no
modelo ao realizar análises, validações e otimizações necessárias para
um melhor funcionamento do esquema como um todo.

O segundo, visa o retoque final, transformando o modelo lógico num


modelo físico ao adicionar tipos de dados às colunas das tabelas
definidas, bem como ao especificar as rotinas de criação das tabelas
com a linguagem CQL.

O modelo lógico que foi gerado seguindo as técnicas demonstradas até


aqui está correto, funciona de forma apropriada e com uma
performance eficiente, afinal, foram empregadas todas as regras e
boas práticas. Na realidade, no entanto, a eficiência do modelo lógico
não pode ser constatada sem a execução de testes de desempenho
com ferramentas adequadas, pois o banco de dados possui limitações,
os recursos são finitos (nós, espaço em disco, memória, etc.).

Dessa forma, é preciso realizar uma análise do modelo a fim de


encontrar potencias problemas, ao levar em conta todas as limitações
do ambiente de hardware e software. Entre as coisas que devem ser
avaliadas estão o tamanho da partição de uma tabela, a redundância e
consistência dos dados, operações de junções realizadas pela
aplicação, regras de integridade referencial, transações e agregações
de dados. Essa análise é primordial para otimizações mais detalhadas,
de forma que o modelo possua um desempenho ainda melhor.

Existem basicamente três formas de otimizar o modelo: melhorando as chaves

(de partição, primária e de ordenação), melhorando as tabelas e melhorando a

concorrência de acesso a dados.

Para finalizar o modelo lógico criado, transformando-o em modelo


físico, é preciso identificar os tipos de dados de cada coluna na tabela
especificada, de tal forma que seja possível utilizar a linguagem CQL
para criar as tabelas. A partir disso, como exemplo fica a modelagem
física da tabela videos_do_usuario, apresentada na Figura 25.
Figura 25. Representação da tabela videos_do_usuario no modelo
físico.

Este artigo teve como principal objetivo prover uma visão geral da
modelagem de dados para o banco de dados Apache Cassandra,
através de uma abordagem introdutória. Dessa forma, ainda existe
muito assunto a ser explorado e que pode ser o objeto de estudo de
outros artigos no futuro. O importante, neste momento, é entender os
conceitos básicos e começar a colocá-los em prática em seus projetos.

Por fim, saiba que o Cassandra pode fazer muito mais do que o
demonstrado aqui, sendo, portanto, de suma importância que os
leitores consultem a bibliografia disposta na seção Links. Dito isso,
explorem esse banco de dados, conheçam sua arquitetura,
frameworks relacionados, configurações avançadas e também casos
de uso, pois é importante saber quando e como fazer uso desta
diferenciada opção.

Links

Site oficial do Apache Cassandra.

http://cassandra.apache.org/

DataStax - Site oficial

http://www.datastax.com/

DataStax - Documentação do Cassandra

http://docs.datastax.com/en/cql/3.1/cql/cql_reference/cqlReferenceTOC.html
Site oficial da ferramenta KDM, para automação da modelagem de dados do

Cassandra.

http://kdm.dataview.org/

Tese sobre a metodologia utilizada neste artigo.

http://www.cs.wayne.edu/andrey/papers/TR-BIGDATA-05-2015-CKL.pdf

Ao longo das últimas décadas muitas coisas aconteceram no mundo


da tecnologia: mudanças em linguagens de programação, novas
arquiteturas, diferentes metodologias de desenvolvimento, entre
outros. No entanto, uma coisa permanecia intacta: bancos de dados
relacionais eram a escolha padrão para armazenar dados. Com o
crescimento acelerado da Internet e a necessidade cada vez mais
comum de manipular altos volumes de dados, isso mudou um pouco.
Uma nova tecnologia emergiu e vem se consolidando nos últimos
anos: são os chamados bancos de dados NoSQL.

Um dos principais problemas dos bancos de dados relacionais para


lidar com grandes massas de dados é o fato de que sua arquitetura cria
dificuldades para que esses bancos rodem em cluster. Dessa forma,
quando surge a necessidade de escalar as alternativas normalmente
são:

· Escalabilidade Vertical: consiste em aumentar os recursos do


servidor (memória, CPU, disco e etc.). Além de ter um limite máximo
real, normalmente tem custos proibitivos;

· Sharding: essa técnica divide os dados da aplicação em mais de um


servidor, distribuindo melhor a carga. O problema é que traz uma
enorme complexidade para a aplicação, perde as melhores vantagens
dos bancos relacionais, como integridade referencial, e continua
tendo um ponto único de falha;

· Master-Slave: um servidor (Master) recebe todas as escritas e replica


para as demais instâncias (Slaves), as quais podem atender apenas
requisições de leitura. Apesar de poder distribuir melhor a carga entre
vários servidores, continua tendo um ponto único de falha e não
consegue ter escalabilidade nas operações de escrita por ter apenas
um servidor atendendo esse tipo de requisição. Ademais, pode
acarretar em custos que inviabilizam sua adoção.

Por esse motivo os bancos de dados NoSQL vêm se popularizando


cada vez mais. Executar em cluster com naturalidade, ter alta
disponibilidade, facilidade de rodar na nuvem são aspectos comuns
nesses novos bancos, pois nasceram justamente para resolver esse
tipo de problema. Nesse contexto, o Apache Cassandra se destaca por
possuir um modelo arquitetural que proporciona todas essas
funcionalidades de uma maneira que minimiza a complexidade
existente nesse tipo de ambiente.

Assim, nas próximas seções este artigo irá apresentar o Apache


Cassandra de maneira mais detalhada, com o intuito de proporcionar
ao leitor um embasamento teórico para o melhor entendimento da
tecnologia, bem como irá demonstrar por meio de um exemplo
prático algumas das funcionalidades explicadas. Além disso, também
será exposta uma abordagem para usá-lo em conjunto com a
plataforma Java EE. E para tornar o exemplo mais próximo do nosso
dia a dia, outras tecnologias serão usadas, como o PrimeFaces e seus
novos recursos de responsividade, CDI e DeltaSpike.

Conhecendo o Apache Cassandra


O Cassandra é um banco de dados NoSQL orientado a colunas
desenvolvido em Java. Criado pelo Facebook e depois doado para a
Fundação Apache, hoje é reconhecido na indústria de software como
um banco de dados massivamente escalável, de alta disponibilidade,
distribuído, dentre outras características essenciais para suportar
volumes de dados colossais, com crescimento exponencial e carga
excessiva de requisições.

Antes de analisar outros detalhes, a seguir serão apresentados alguns


conceitos importantes para facilitar o entendimento do restante do
artigo:

· Cluster: consiste num grupo de máquinas (nós) onde os dados são


distribuídos e armazenados. Pode ser composto de um único nó
(single-node cluster) ou vários nós em diversos data centers. A Figura
1 apresenta um exemplo;
· Data center: uma subdivisão dos nós do cluster, os quais estão
ligados para propósitos de segregação de replicação e carga. Por
exemplo, é possível configurar o Cassandra para replicar dados
apenas entre nós do mesmo data center, o que normalmente envolve
menos latência do que replicar através de múltiplos data centers. Não
se trata necessariamente de um data center físico;

· Nó: uma máquina que faz parte do cluster e que consequentemente


armazena dados da base;

· Keyspace: tem o conceito similar a um database no PostgreSQL,


onde tabelas são agrupadas para uma finalidade específica.
Normalmente para separar dados de aplicações diferentes;

· Família de colunas (Column-Family): nas versões mais atuais do


Cassandra esse termo foi substituído por Tabela. Trata-se de um
conjunto de pares chave/valor (nome da coluna/valor da coluna) onde
são armazenadas as informações da base. Pode-se dizer que com o
CQL3 (Cassandra Query Language) esse termo ficou obsoleto.

Figura 1. Representação de um cluster multi-data center – Adaptado


de DataStax.

CQL – O SQL do Cassandra


O CQL (Cassandra Query Language), como o próprio nome já diz, é a
linguagem de consulta para o Cassandra. Atualmente na versão 3.3, é
a interface primária para estabelecer comunicação com essa base de
dados. Além disso, por possuir muitos aspectos similares ao SQL, não
se restringindo apenas ao nome, o CQL3 facilita bastante o
aprendizado de profissionais que estão habituados a trabalhar com
bancos de dados relacionais. Antes do CQL a interface padrão do
Cassandra era a Thrift API (vide BOX 1).

BOX 1. Thrift API

Nos primórdios do Cassandra a única opção disponível para consulta era a

Thrift API, uma interface baseada no protocolo RPC e que era bastante

burocrática e difícil de entender à primeira vista. Em seguida houve algumas

melhoras com o advento do CQL, mas ainda assim muitas características da

Thrift API estavam presentes. Somente com o CQL3 o Cassandra pôde ter uma

forma de comunicação mais intuitiva, simples e produtiva.

O CQL3 também impactou a forma de modelar dados para o


Cassandra. Assim, caso você esteja interessado em se aprofundar no
assunto é importante entender que existe uma fase “pré-CQL3” e outra
“pós-CQL3”. Isso irá facilitar os estudos e evitará confusão ao
aprender dicas e boas práticas diferentes para cada uma dessas fases.
Neste artigo, iremos focar no CQL3.

Acessando o CQL
A maneira mais comum de acessar o CQL é através da
ferramenta cqlsh, como pode ser observado na Figura 2. Trata-se de
um cliente de linha de comando que vem junto com a instalação do
Cassandra (CASSANDRA_HOME/bin/cqlsh).
Figura 2. Executando comandos CQL através do cqlsh.

Caso queira optar por uma ferramenta gráfica, o DataStax DevCenter é


uma ótima opção (vide Figura 3). Esta ferramenta é baseada no
Eclipse e traz algumas views especializadas para o Cassandra:

· View Connections: Espaço onde você pode gerenciar todas as


conexões que criou para algum cluster do Cassandra.

· View Schema: Aqui são listados todos os objetos de uma determinada


conexão, o que permite uma visualização hierárquica da estrutura do
banco (keyspace > tabela > coluna);

· View CQL Scripts: Através dessa view é possível gerenciar scripts


CQL: criar, editar, deletar;

· View Results: Exibe o resultado da última consulta executada; e

· Editor CQL: Editor que possibilita escrever e executar comandos


CQL, faz destaque de palavras reservadas, tem code completion e ainda
possibilita escolher a conexão onde o comando será executado para
cada arquivo aberto.

Escolher entre uma ferramenta e outra normalmente é uma questão


de preferência. O DevCenter é mais indicado para quem está iniciando
devido a diversas facilidades que uma IDE pode proporcionar, como:
wizards para criação de conexões, keyspaces e tabelas, abas para se
trabalhar com múltiplos servidores, gerenciamento de scripts,
destaque de palavras reservadas, entre outros. O cqlsh, por sua vez, é
mais utilizado por quem tem familiaridade com a linha de comando e
não está muito a fim de abrir uma IDE pesada na sua máquina. Como
facilidade, o cqlsh tem o recurso de tab completion, bastante útil a
quem está acostumado com a “telinha preta”.
abrir imagem em nova janela

Figura 3. Executando comandos CQL através do DevCenter.

Modelagem – Desnormalizar é preciso


Quando se fala de modelagem de dados em bancos NoSQL,
normalmente temos que deixar de lado quase tudo que é considerado
boa prática no mundo relacional. No Cassandra não é diferente, e
mais, várias das boas práticas da modelagem relacional são
consideradas anti-padrões.

Nesse banco a normalização dos dados é considerada um destruidor


de performance. Portanto, quase sempre é melhor desnormalizar
para escalar a base. Por isso, não se preocupe tanto com a repetição
dos dados ou com a quantidade maior de escritas que isso provoca. O
Cassandra sabe lidar muito bem com essa situação.

Outra diferença é que nos bancos relacionais o foco da modelagem


são as tabelas (entidades), onde a partir das mesmas diversos
relacionamentos são obtidos através de joins e chaves estrangeiras. Já
as boas práticas do Cassandra instruem a guiar sua modelagem
baseado nas consultas. Assim, um padrão recomendado é ter uma
tabela por consulta. Por exemplo, se sua aplicação precisa consultar
Usuários por nome e por login, serão criadas duas
tabelas: usuario_por_nome e usuario_por_login, ambas com os mesmos
dados (desnormalização).
O problema da normalização e do foco em entidades é que essas
técnicas acabam distribuindo os dados de forma inadequada, e para
um banco como o Cassandra, isso pode significar ter que consultar
vários nós do cluster para encontrar a informação, o que pode
acarretar um grande problema de desempenho. As recomendações
apresentadas visam minimizar ao máximo o acesso a múltiplos nós.

Partition Key
Todas as tabelas do Cassandra precisam definir uma chave
denominada Partition Key. Esta tem como principal utilidade
determinar em qual nó do cluster um dado será armazenado e trata-se
de um conceito fundamental a todos que lidam com essa base de
dados.

No que se refere à modelagem, a partition key tem relação direta com


os filtros de uma consulta. Isso porque no Cassandra qualquer query
precisa filtrar a tabela no mínimo pelas colunas que compõem sua
partition key. A lógica dessa restrição é que sem a partition key o
Cassandra não tem como saber em qual nó a informação está
armazenada.

Neste ponto vale ressaltar que não se deve confundir partition key
com primary key. Por exemplo, digamos que uma tabela definiu sua
chave primária da seguinte forma:

PRIMARY KEY (user_login, status, book_isbn)

Nesse cenário, a primary key é composta pelas


colunas user_login, status e book_isbn, enquanto a partition key se
resume à primeira coluna, que no caso é user_login. As demais são
conhecidas como clustering columns.

Clustering Column
Outro importante aspecto do Cassandra são as Clustering Columns.
Essas colunas fazem parte da primary key, mas não da partition key.
No exemplo apresentado anteriormente, as
colunas status e book_isbn seriam as clustering columns.

A função dessas colunas é determinar a ordenação pela qual os dados


serão organizados no disco para uma determinada partition key. É
como uma ordenação padrão. Assim, no exemplo supracitado, uma
vez que a tabela foi filtrada pela coluna user_login (partition key), os
dados apresentados estarão ordenados pelas
colunas status e book_isbn, mesmo que não se use um ORDER BY. Essa
ordenação padrão ainda pode ser definida na criação da tabela como
ASC ou DESC para cada uma das clustering columns. Como essa
ordenação já é garantida no momento de armazenar a informação no
disco, existe um enorme ganho de desempenho, pois o banco de
dados não precisa fazer isso em memória para cada consulta.

Vale informar que o Cassandra só aceita ordenação (ORDER BY)


baseado nas clustering columns. Deste modo, a ordenação dos dados
para uma consulta pode modificar a forma como modelamos as
tabelas. Isto porque o nosso objetivo deve ser executar o SELECT sem
precisar especificar uma cláusula ORDER BY e mesmo assim sempre
obter os dados na ordem desejada.

Outra maneira que as clustering columns podem afetar a modelagem


é que essa ordenação padrão também irá trazer ótima performance
nos filtros por intervalos, por exemplo, um período de data. Assim, se
você perceber que sua consulta irá precisar desse tipo de filtragem, é
fundamental escolher bem as clustering columns.

Distribuição – A chave para a escalabilidade


horizontal
Um dos pontos fortes do Cassandra é a sua arquitetura distribuída
com suporte a diversas configurações e tamanhos de cluster, desde
um único nó a até centenas de nós, como acontece em algumas
empresas como eBay, Netflix e Apple.

Essa distribuição é feita de forma automática, não necessitando que


desenvolvedores e arquitetos se preocupem em implementar algum
tipo de sharding via aplicação. Um componente conhecido como
partitioner é o responsável por essa tarefa.

Um partitioner basicamente é uma função que gera um token (hash) a


partir da partition key e então distribui os dados entre os nós do
cluster baseado nesse token de maneira uniforme e transparente para
o desenvolvedor. Em outras palavras, cada nó é responsável por um
range compreendido pelo token.
O Cassandra oferece algumas opções de particionadores, sendo o

Murmur3Partitioner a opção padrão e mais recomendada. Esse particionador

gera tokens de 64 bits que englobam um range de -263 a 263-1. Para mais

detalhes, acesse Apache Cassandra Product Guide (veja o endereço na

seção Links).

Exemplo de distribuição de dados


Para exemplificar o funcionamento do mecanismo de distribuição de
dados, vamos supor que exista uma tabela chamada pessoas_por_nome,
que tem como partition key o campo Nome. Dessa forma, essa coluna
será usada pelo particionador para gerar os hashes sempre que uma
nova linha for inserida na tabela. Para ilustrar melhor essa situação,
apresentamos na Tabela 1 alguns valores para a coluna Nome e os
seus respectivos hashes, que foram gerados por um partitioner
hipotético.

Partition Key Hash


José -2245462676723223822
Maria 7723358927203680754
João -6723372854036780875
Isabel 1168604627387940318

Tabela 1. Partition keys e seus respectivos hashes.

Agora, imagine que o cluster dessa aplicação é composto por quatro


máquinas, como exemplificado na Figura 4, e que cada um dos nós é
responsável por armazenar os dados de um determinado range do
hash gerado pelo partitioner. Por exemplo, nesse cluster o nó C
armazenará as linhas que tenham um hash de 0 a
46116860118427387903.
Figura 4. Nós do cluster e seus respectivos ranges - Adaptado de:
DataStax.

Considerando o conjunto de dados da Tabela 1 sendo inseridos num


cluster com a configuração da Figura 4, teríamos uma distribuição dos
dados conforme a Tabela 2. Assim, a linha que tem a
coluna Nome igual a João seria armazenada pelo nó A, pois o
partitioner gerou um hash para essa partition key o qual fica dentro do
intervalo da máquina A.

Nó Início range Fim range Partition Hash


Key
- - -
A João
9223372036854775808 4611686018427387903 672337285403678087
- -
B -1 José
4611686018427387904 224546267672322382
C 0 4611686018427387903 Isabel 116860462738794031
D 4611686018427387904 9223372036854775807 Maria 772335892720368075

Tabela 2. Distribuição dos dados no cluster.


Replicação – A mágica por trás da alta
disponibilidade
No Cassandra a replicação de dados é algo tão natural como
transações são para bancos de dados relacionais. Ela pode acontecer
num mesmo data center, em múltiplos data centers ou em múltiplas
zonas de cloud. Isso vai depender de sua estratégia de replicação.

A configuração mais importante quando se fala de replicação é o


replication factor. Ele é configurado na criação de cada keyspace
(similar a um database no PostgreSQL) e sua função é definir o total de
cópias de uma linha no cluster, fazendo com que cada cópia resida em
um nó diferente. Se o replication factor é 1, então só haverá uma cópia
por linha no cluster. Se o replication factor for 3, então quer dizer que
para cada linha haverá três cópias no cluster.

Uma vez que o número de cópias é definido através do replication


factor, ainda é possível parametrizar o processo de replicação para
indicar como essas cópias serão distribuídas em diferentes data
centers. Por exemplo, para um replication factor 4, pode-se
armazenar duas cópias no data center 1, uma cópia no data center 2 e
uma cópia no data center 3.

Dentre os principais benefícios que a replicação proporciona destaca-


se a tolerância a falhas e o aumento da confiabilidade da aplicação.

Consistência – Nem forte, nem fraca:


Tunável
Um tema quase certo em todas as discussões sobre NoSQL é o
Teorema CAP. No próprio site da DevMedia existem diversos artigos
que abordam o assunto. Por isso, apresentaremos aqui apenas um
pequeno resumo, mostrado no BOX 2.

Box 2. Teorema CAP

Resumidamente, esse teorema explica que num sistema distribuído é

impossível ter, ao mesmo tempo, consistência, disponibilidade e tolerância à


partição. A seguir, apresentamos uma breve explanação sobre cada uma dessas

propriedades:

Consistência: implica que todos os clientes sempre visualizarão os mesmos

dados;

Disponibilidade: se o cliente pode se conectar a um nó, então este nó deve ser

capaz de ler e escrever dados;

Tolerância à Partição: significa que o cluster continuará trabalhando mesmo

que ele seja dividido por quebras na comunicação da rede. Por exemplo, um

cluster composto de dois data centers deverá funcionar mesmo que esses data

centers não consigam se comunicar.

No que se refere ao Teorema CAP, o Cassandra é um sistema AP,


proporcionando alta disponibilidade e tolerância à partição. Isso
significa que os dados eventualmente ficarão consistentes em todas as
réplicas, mas também podem, em determinadas janelas de tempo,
ficar inconsistentes. Para minimizar essa situação, o Cassandra
estende o conceito de consistência eventual, oferecendo o que chama
de consistência tunável.

Tunable Consistency
Consistência tunável nada mais é do que a capacidade de configurar o
grau de consistência desejado, seja num nível mais global ou em nível
de operação (select, insert, delete, update). Dessa forma, numa
operação mais crítica para a aplicação o desenvolvedor pode setar a
consistência para um nível forte, e numa operação menos importante,
pode usar consistência fraca. Isso faz com que o Cassandra haja tanto
como um sistema AP (Availability e Partition Tolerant) quanto como um
sistema CP (Consistency e Partition Tolerant).

Alguns exemplos de nível de consistência disponíveis são:

● ONE: basta um único nó responder à requisição que a resposta é


retornada para o cliente;
● QUORUM: para o cliente obter a resposta será necessário que um
quórum de nós retorne, onde, o quórum é igual a FATOR
REPLICACAO/2 + 1;

● ALL: antes de retornar ao cliente, todas as réplicas deverão


responder. Vale ressaltar que “todas as réplicas” equivale ao fator de
replicação, o que não quer dizer todas as máquinas do cluster.

Além destes existem vários outros níveis de consistência que podem


resultar numa diversidade de maneiras de controlar esse aspecto de
uma determinada operação, o que traz grande flexibilidade para os
usuários do Cassandra. Para conhecer todas as opções disponíveis
acesse Apache Cassandra Product Guide (veja a seção Links).

No que se refere à operação de leitura, o dado mais recente de todos


os nós consultados será retornado ao cliente. Em seguida, caso haja
algum nó desatualizado, ele será sincronizado em background (read
repair). A Figura 5 demonstra um exemplo de leitura com consistência
QUORUM. Note que o nó 10 é o coordenador da operação, o nó que se
comunica diretamente com o cliente. Os nós 1 e 6 possuem a mesma
informação, sendo que o nó 6 foi escolhido para responder à
requisição. Já o nó 3 está com a informação desatualizada e por isso
receberá um read repair. O BOX 3 explica como o Cassandra verifica
qual dado é mais atual que outro.

Nas operações de escrita, a consistência determina quantos nós


devem gravar a informação até que um retorno de sucesso seja dado
ao cliente, no entanto, as réplicas ainda serão determinadas de acordo
com o replication factor (RF). Por exemplo, digamos que o RF seja 3 e
a operação tenha consistência ONE. O que irá acontecer é que assim
que o primeiro nó responder com sucesso o cliente já será notificado
de que a operação foi bem-sucedida, mas o Cassandra continuará a
operação nas outras duas máquinas para completar a quantidade de
cópias definidas no RF.

Box 3. Last Write Wins

Para cada coluna de uma tabela no Cassandra, além do seu valor também é

armazenado um timestamp correspondente à última atualização do campo.

Assim, caso haja uma atualização concorrente numa coluna, a mais recente é a
que prevalecerá. Essa é uma técnica de resolução de conflito conhecida

como Last Write Wins. É através desse timestamp também que o Cassandra

checa se a informação de um nó é mais recente do que a que está em outro

para retorná-la nas consultas, bem como para disparar um read repair aos nós

desatualizados.

Figura 5. Funcionamento de uma operação de leitura com


consistência QUORUM - Fonte: DataStax.

Gerenciamento de transações - Sai ACID


entra lightweight
O Cassandra não suporta transações ACID, mais especificamente, não
suporta o “C” (Consistência) já que não contempla joins, integridade
referencial ou mecanismos como commit e rollback. A ausência
dessas features, no entanto, é compensada pela alta disponibilidade e
escrita extremamente rápida que esse banco oferece.
Com relação às demais características, embora sejam um pouco
diferentes do que acontece em bancos relacionais, elas existem: as
escritas são duráveis (armazenadas num dispositivo não volátil como
um disco rígido ou SSD), o isolamento é suportado em nível de linha
(uma operação numa linha só é visível a outros usuários quando a
mesma é completada) e a atomicidade também é oferecida em nível
de linha (a inserção ou atualização de colunas de uma mesma linha
são tratadas como uma única operação de escrita).

Lightweight Transactions
Em bancos de dados relacionais uma arquitetura master-slave é uma
estratégia comum para se implementar um cluster. Por outro lado, o
Cassandra usa uma arquitetura do tipo peer-to-peer, na qual qualquer
nó do cluster tem o mesmo papel, ou seja, todos podem receber
escritas e leituras.

Por conta dessa abordagem, as operações de escrita no Cassandra


podem ter problemas com requisições concorrentes que atualizam a
mesma informação. Apesar da consistência tunável poder resolver
muitos desses problemas, ainda há casos críticos em que se precisa ter
uma maior “regulação” entre essas requisições concorrentes.

Figura 6. Exemplo de race condition – Fonte: FinishJUG.

A Figura 6 relata uma operação concorrente para criar um usuário.


Nesse exemplo o usuário só deve ser criado se não houver nenhum
outro com o mesmo login (id). No entanto, o que acontece é que as
duas operações criam o usuário com o mesmo login e a segunda
requisição acaba sobrescrevendo a informação da primeira. Esse
cenário é conhecido como race condition.

É com intuito de resolver esse problema que o Cassandra desenvolveu


o que chama de lightweight transaction. Uma lightweight transaction
permite garantir que uma sequência de operações será executada sem
interferência de outras. Algo semelhante ao nível de isolamento
serializável oferecido por bancos de dados relacionais. Veja alguns
exemplos na Figura 7.

Figura 7. Exemplo de uso de lightweight transaction.

Tenha em mente que lightweight transactions podem impactar


bastante na performance do Cassandra, visto que checar se um
registro já existe em diversas réplicas distribuídas pela rede é uma
operação custosa. Por isso, lightweight transactions devem ser
utilizadas com cuidado e em poucos casos.

Cassandra na prática
Agora que você tem uma visão geral do que é o Cassandra e como
funcionam vários aspectos importantes da arquitetura, vamos ao que
interessa: a parte prática!

Ao longo deste tutorial vamos construir uma aplicação web


(WebShelf) que imita o Amazon Shelfari, o qual permite aos usuários
criar uma prateleira online de livros identificando o que já foi lido, o
que se está lendo no momento e o que se pretende ler (vide Figura 8).

Para isso, vamos usar algumas tecnologias, onde destacam-se: Apache


Cassandra 2.2.3, WildFly 9, PrimeFaces 5.3, Cassandra DataStax Driver
2.1.8, DataStax DevCenter 1.4.1 e Java EE 7.
Figura 8. Prateleira de livros do WebShelf.

Montando o ambiente
O tutorial foi desenvolvido utilizando o Eclipse Mars, mas você pode
optar por qualquer IDE que achar melhor. Com a IDE definida, para
colocar o Cassandra para rodar basta fazer o download, descompactar
o arquivo, entrar na pasta bin e executar o comando cassandra -f para
Linux ou cassandra.bat -f para Windows.

Para conectar no Cassandra a ferramenta escolhida foi o DevCenter.


Baseado no Eclipse, para iniciá-lo basta fazer o download da versão
mais adequada para o seu sistema operacional, descompactar e
executá-lo através do arquivo DevCenter dentro da pasta raiz. No
decorrer do tutorial será mostrado como criar uma conexão.

Já o ambiente de execução da aplicação será o WildFly 9. Para quem


não conhece esse servidor, ele é a evolução do JBoss AS com uma nova
nomenclatura adotada pela Red Hat a partir da versão 8 e que
implementa o Java EE 7. Como não faremos nenhuma configuração
extra para este tutorial, basta realizar o download do mesmo e
descompactá-lo. Para iniciar o servidor você pode usar a linha de
comando. Assim, é só entrar na pasta bin e executar o
script standalone.sh (Linux) ou standalone.bat (Windows). No entanto,
durante o desenvolvimento recomenda-se configurar a IDE para
poder gerenciá-lo de um ambiente centralizado. No Eclipse Mars o
adapter do WildFly 9 já vem instalado por padrão. Portanto, basta
acessar a view Servers e criar uma nova instância.

Configurando o projeto
Neste tópico iremos preparar a maior parte das configurações da
aplicação de modo a nos concentrar posteriormente no código que
mais interessa a este artigo. Portanto, vamos às configurações.

Criando o projeto Maven


O primeiro passo é criar o projeto no Eclipse, momento no qual
informaremos que vamos utilizar o Maven como ferramenta de build
e gerenciador de dependências. O projeto se chamará webshelf e terá
dois módulos: webshelf-business e webshelf-web. A Figura 9 apresenta
essa estrutura.

O módulo webshelf-business terá, basicamente, a camada de negócio e a


camada de acesso a dados. Já o módulo webshelf-web conterá o código
relacionado à camada web da aplicação. Assim, ao rodar o build do
projeto, o módulo webshelf-web irá gerar um arquivo WAR que
possuirá, entre outras libs, o JAR referente ao módulo webshelf-
business.
Figura 9. Estrutura do projeto.

Configurando os poms
O pom do projeto principal, demonstrado na Listagem 1, terá as
configurações que serão compartilhadas entre os dois módulos, por
exemplo, dependências comuns ao módulo business e ao módulo
web, como é o caso do CDI e Hibernate Validator. Além disso, para
garantir que utilizaremos as mesmas APIs e versões contidas no
WildFly, adicionamos uma dependência ao BOM (Bill of Materials) do
WildFly 9. Assim você só precisa declarar as APIs do Java EE 7 que irá
usar, sem se preocupar com versões ou escopo, pois estas
configurações já estão definidas no BOM.

Listagem 1. Configuração do arquivo webshelf/pom.xml.

<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>br.com.devmedia</groupId>
<artifactId>webshelf</artifactId>
<version>1.0.0</version>
<packaging>pom</packaging>
<modules>
<module>webshelf-business</module>
<module>webshelf-web</module>
</modules>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<version.jboss.bom>9.0.1.Final</version.jboss.bom>
</properties>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.wildfly.bom</groupId>
<artifactId>jboss-javaee-7.0-wildfly</artifactId>
<version>${version.jboss.bom}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<dependencies>
<dependency>
<groupId>javax.enterprise</groupId>
<artifactId>cdi-api</artifactId>
</dependency>

<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
</dependency>
</dependencies>
</project>

Já as configurações e dependências dos módulos business e web, por


sua vez, dizem respeito apenas a eles mesmos. Por exemplo, uma
dependência declarada no módulo web não estará disponível no
módulo business (vide Listagens 2 e 3). Isso é bastante útil para evitar
que as camadas lógicas da aplicação se misturem, afinal, para que
você precisa de classes do JSF na sua camada de negócio?

Portanto, os dois poms são simples, contendo basicamente as


declarações de dependências que serão explicadas no decorrer do
tutorial. Uma observação importante é que no webshelf-
web/pom.xml existe uma declaração de dependência ao módulo
business. O intuito disso é fazer com que o WAR do módulo web
contenha na sua pasta libs o JAR do módulo business.

Listagem 2. Configuração do arquivo webshelf-business/pom.xml.

<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>br.com.devmedia</groupId>
<artifactId>webshelf</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>webshelf-business</artifactId>
<packaging>ejb</packaging>

<build>
<plugins>
<plugin>
<artifactId>maven-ejb-plugin</artifactId>
<version>2.5.1</version>
<configuration>
<ejbVersion>3.2</ejbVersion>
</configuration>
</plugin>
</plugins>
</build>

<dependencies>
<dependency>
<groupId>org.jboss.spec.javax.ejb</groupId>
<artifactId>jboss-ejb-api_3.2_spec</artifactId>
</dependency>

<!-- Driver do Cassandra -->


<dependency>
<groupId>com.datastax.cassandra</groupId>
<artifactId>cassandra-driver-core</artifactId>
<version>2.1.8</version>
</dependency>

<!-- API de object-mapping para o driver Cassandra -->


<dependency>
<groupId>com.datastax.cassandra</groupId>
<artifactId>cassandra-driver-mapping</artifactId>
<version>2.1.8</version>
</dependency>
</dependencies>
</project>

Listagem 3. Configuração do arquivo webshelf-web/pom.xml.

<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>br.com.devmedia</groupId>
<artifactId>webshelf</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>webshelf-web</artifactId>
<packaging>war</packaging>

<build>
<plugins>
<plugin>
<artifactId>maven-war-plugin</artifactId>
<version>2.6</version>
<configuration>
<warName>webshelf</warName>
</configuration>
</plugin>
</plugins>
</build>

<repositories>
<repository>
<id>prime-repo</id>
<name>PrimeFaces Maven Repository</name>
<url>http://repository.primefaces.org</url>
<layout>default</layout>
</repository>
</repositories>

<dependencies>
<dependency>
<groupId>br.com.devmedia</groupId>
<artifactId>webshelf-business</artifactId>
<version>1.0.0</version>
<type>ejb</type>
</dependency>

<dependency>
<groupId>org.jboss.spec.javax.servlet</groupId>
<artifactId>jboss-servlet-api_3.1_spec</artifactId>
</dependency>

<dependency>
<groupId>org.jboss.spec.javax.faces</groupId>
<artifactId>jboss-jsf-api_2.2_spec</artifactId>
</dependency>

<dependency>
<groupId>org.apache.deltaspike.modules</groupId>
<artifactId>deltaspike-jsf-module-api</artifactId>
<version>1.5.0</version>
</dependency>

<dependency>
<groupId>org.apache.deltaspike.modules</groupId>
<artifactId>deltaspike-jsf-module-impl</artifactId>
<version>1.5.0</version>
<scope>runtime</scope>
</dependency>

<dependency>
<groupId>org.primefaces</groupId>
<artifactId>primefaces</artifactId>
<version>5.3</version>
</dependency>

<dependency>
<groupId>org.primefaces.themes</groupId>
<artifactId>blitzer</artifactId>
<version>1.0.10</version>
</dependency>
</dependencies>
</project>

Ativando o CDI
Para que seja possível utilizar o CDI no projeto, será necessário criar o
arquivo beans.xml nos dois módulos. No módulo business, o arquivo
deve ficar localizado na pasta src/main/resources/META-INF, enquanto
no módulo web deve ficar na pasta src/main/webapp/WEB-INF.
A Listagem 4 apresenta o conteúdo desse arquivo, que deve ser o
mesmo nos dois módulos.

Listagem 4. Configuração do arquivo beans.xml.

<?xml version="1.0" encoding="UTF-8"?>


<beans xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/beans_1_1.xsd" bean-discovery-
mode="all">
</beans>

Preparando o web.xml
Como se trata de uma aplicação web, não poderia faltar o web.xml.
Esse arquivo deve ficar no módulo web em src/main/webapp/WEB-
INF e é apresentado na Listagem 5. Nele temos algumas configurações
que merecem atenção. A primeira delas é o mapeamento do servlet
JSF (o Faces Servlet). O intuito dessa configuração é mapear o servlet
para qualquer URL terminada com *.xhtml, que é a extensão usada
nas páginas JSF do projeto.
Outra configuração é a definição do tema do PrimeFaces através do
parâmetro primefaces.THEME. No nosso exemplo optamos por
utilizar o blitzer. Caso você prefira outro, basta alterar esse parâmetro
com o nome do tema escolhido e incluir a dependência no webshelf-
web/pom.xml. Para saber os temas que o PrimeFaces disponibiliza,
basta acessar PrimeFaces Web Site (Links).

Além do tema, outra configuração relacionada ao PrimeFaces é a


ativação da API de validação client-side, disponível desde a versão 4
(primefaces.CLIENT_SIDE_VALIDATION). Através dessa feature é
possível realizar diversas validações via JavaScript, evitando que seja
feita uma requisição desnecessária ao servidor. O melhor é que para
os casos padrões de validação, como required, length e range, você
não precisará escrever nenhum código, o que é aplicável também às
validações padrões da API Bean Validation. Para mais detalhes,
veja PrimeFaces Web Site na seção Links.

Listagem 5. Código do arquivo web.xml.

<?xml version="1.0" encoding="UTF-8"?>


<web-app version="3.0" xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd">

<servlet>
<servlet-name>Faces Servlet</servlet-name>
<servlet-class>javax.faces.webapp.FacesServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>Faces Servlet</servlet-name>
<url-pattern>*.xhtml</url-pattern>
</servlet-mapping>

<welcome-file-list>
<welcome-file>index.html</welcome-file>
</welcome-file-list>
<session-config>
<session-timeout>30</session-timeout>
</session-config>
<context-param>
<param-name>primefaces.THEME</param-name>
<param-value>blitzer</param-value>
</context-param>
<context-param>
<param-name>primefaces.CLIENT_SIDE_VALIDATION</param-name>
<param-value>true</param-value>
</context-param>
<context-param>
<param-name>javax.faces.FACELETS_SKIP_COMMENTS</param-name>
<param-value>true</param-value>
</context-param>
<context-param>
<param-
name>javax.faces.INTERPRET_EMPTY_STRING_SUBMITTED_VALUES_AS_NULL</param-name>
<param-value>true</param-value>
</context-param>
<p align="left"></web-app>

Acessando o Cassandra via Java


Para estabelecer a conexão com o Cassandra será utilizado o driver da
DataStax, empresa que presta um serviço de suporte para o Cassandra
e tem sido uma das principais mantenedoras desse banco. Além desta,
existem diversas outras opções para se conectar com o Cassandra via
Java, como o Hector ou Astyanax, mas essas APIs foram construídas
inicialmente em cima da interface Thrift, a qual nas versões mais
atuais do Cassandra está em desuso. Portanto, é preferível o uso de
uma API que trabalhe em cima do CQL e seu protocolo nativo.

O driver DataStax também oferece diversas features que o tornam


uma forma extremamente recomendada de se comunicar com o
Cassandra, a saber: chamadas assíncronas, balanceamento de carga
configurável, failover transparente, entre outras.

Para começar, vamos criar a classe CassandraCluster, conforme


a Listagem 6. Essa classe será responsável por gerenciar a
comunicação com o Cassandra encapsulando os objetos do driver de
modo a centralizar qualquer configuração relacionada ao banco de
dados.
Listagem 6. Classe para comunicação com o Cassandra (módulo
webshelf-business).

package br.com.devmedia.webshelf.data;

import java.util.HashMap;
import java.util.Map;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.ejb.Singleton;
import javax.ejb.TransactionAttribute;
import javax.ejb.TransactionAttributeType;

import com.datastax.driver.core.Cluster;
import com.datastax.driver.core.PreparedStatement;
import com.datastax.driver.core.Session;
import com.datastax.driver.mapping.MappingManager;

@Singleton
@TransactionAttribute(TransactionAttributeType.NOT_SUPPORTED)
public class CassandraCluster {

private Cluster cluster;


private Session session;
private MappingManager mappingManager;
private Map<String, PreparedStatement> preparedStatementCache = new
HashMap<>();

@PostConstruct
private void init() {
cluster = Cluster.builder().addContactPoint("localhost").build();
session = cluster.connect();
mappingManager = new MappingManager(session);
}

@PreDestroy
private void destroy() {
session.close();
cluster.close();
}
}

Neste código, a anotação @Singleton define a classe como um EJB


Singleton. Já a anotação @TransactionAttribute foi colocada para
sinalizar que esse EJB não suporta controle de transação. Isso porque
não existe uma maneira padronizada de se criar um datasource num
servidor de aplicação Java EE que gerencie conexões do Cassandra e,
portanto, não há como tirar proveito dessa feature da especificação
EJB. Além disso, o Cassandra não suporta transações ACID, tendo seu
próprio conceito e protocolo de gerenciamento de transações que não
se encaixam no modelo oferecido pelo EJB.

Regras de utilização do driver DataStax


Aqui é necessário observar quatro regras básicas ao se trabalhar com
o driver:

● Deve haver apenas uma instância da classe Cluster para cada cluster
que sua aplicação precisar conectar. Essa instância precisará existir
durante todo o ciclo de vida da aplicação;

● Deve haver apenas uma instância da classe Session por keyspace,


ou, uma instância única para toda a aplicação, onde os comandos
terão que especificar o keyspace. Essa instância precisa existir
durante todo o ciclo de vida da aplicação;

● Caso seja necessário executar um mesmo statement repetidas vezes,


é fortemente recomendado que se use PreparedStatement;

● Onde for adequado, faça uso de batches para reduzir round-trips na


rede e também obter operações atômicas.

Para implementar os pontos 1 e 2 optamos por fazer


de CassandraCluster um EJB Singleton. Dessa forma, vamos garantir
que apenas uma instância dessa classe irá existir durante todo o ciclo
de vida da aplicação. Consequentemente, acontecerá o mesmo com os
campos cluster e session, que são inicializados e fechados através dos
callbacks @PostConstruct e @PreDestroy, os quais serão executados
apenas uma vez seguindo o ciclo de vida de um EJB Singleton.
O atributo preparedStatementCache está relacionado com o item 3.
Como o WebShelf irá executar determinados statements de maneira
repetida, a aplicação irá usar PreparedStatement em vários pontos.
No entanto, para otimizar o uso de PreparedStatements é
recomendado fazer um cache desse tipo de objeto para evitar que o
Cassandra tenha que “re-preparar” statements repetitivos. Se você não
fizer isso, verá alguns warnings como esse:

Re-preparing already prepared query "SELECT..." . Please note that preparing


the same query more than once is generally an anti-pattern and will likely
affect performance. Consider preparing the statement only once.

O atributo mappingManager, por sua vez, é responsável por prover a


funcionalidade de object-mapping, algo como um JPA bem mais
simplificado. A função da classe MappingManager é criar Mappers
para as entidades da aplicação e esses Mappers serão encarregados
diretos por executar comandos como insert e select baseados em
objetos, ao invés de comandos CQL.

Cada Mapper criado é guardado num cache interno da


classe MappingManager e cada um tem também um cache interno de
PreparedStatements para as operações que já executou. Dessa forma,
esses caches evitam re-preparar statements que serão executados
várias vezes e, portanto, nos ajudam a implementar também o item 3
das regras de utilização do driver. Para que esse esquema de caches
funcione, é recomendado que haja apenas uma instância do
tipo MappingManager para cada instância de Session.

>

Nota: As classes Cluster, Session, MappingManager,


Mapper e PreparedStatements, todas do driver DataStax, são thread-safe e assim
podem ser compartilhadas por múltiplas threads.

Cadastro de Usuário
A primeira funcionalidade do projeto WebShelf será o Cadastro de
Usuário, o qual permitirá que qualquer pessoa se inscreva no site para
usufruir de seus serviços.
Esta funcionalidade consistirá de uma tela simples, feita em JSF 2.2
com apoio do PrimeFaces 5.3. A página usará alguns recursos dessa
biblioteca para prover melhor responsividade de seus componentes.
Vale ressaltar que alguns desses recursos de responsividade podem
ser usados desde a versão 5.1, quando o time do PrimeFaces
aumentou os esforços para melhor gradativamente essa característica
da biblioteca.

Essa tela irá invocar um método no controller, que por sua vez irá
delegar para um método de negócio o qual será responsável por fazer
algumas validações e então inserir o registro no Cassandra.

Mapeando a entidade de Usuário


No WebShelf um usuário terá login, nome e senha, sendo que o login
deve ser único. Usaremos essa entidade para exemplificar a API de
object-mapping (API OM) do driver Cassandra, como pode ser
visualizado na Listagem 7. Essa API permite que o desenvolvedor
tenha algumas features similares, embora bem menos poderosa, a um
ORM como o Hibernate. É importante esclarecer que a maior parte do
desenvolvimento com esse driver será utilizando comandos CQL
diretamente, mas em alguns casos o object-mapping pode facilitar a
implementação de cenários mais simples e por isso decidimos
demonstrá-lo neste tutorial.

Listagem 7. Código da classe User (módulo webshelf-business).

package br.com.devmedia.webshelf.model;

import java.io.Serializable;
import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

import javax.validation.constraints.Size;

import org.hibernate.validator.constraints.NotBlank;

import com.datastax.driver.mapping.annotations.PartitionKey;
import com.datastax.driver.mapping.annotations.Table;
@Table(keyspace = "webshelf", name = "user")
public class User implements Serializable {

private static final long serialVersionUID = 1L;

@PartitionKey
@NotBlank(message="Login: não pode está em branco.")
private String login;

@NotBlank(message="Nome: não pode está em branco.")


private String name;

@NotBlank(message="Senha: não pode está em branco.")


private String password;

//constructors

//getters and setters

public void setClearPassword(String password) {


this.password = encryptPassword(password);
}

public static String encryptPassword(String clearPassword){


try {
MessageDigest md = MessageDigest.getInstance("MD5");
md.update(clearPassword.getBytes());
return new BigInteger(1, md.digest()).toString(16);
} catch (NoSuchAlgorithmException exception) {
throw new RuntimeException("Ocorreu um erro ao tentar criptografar a
senha.",exception);
}
}
}

Para usar a API OM é preciso anotar a classe com @Table informando


o nome do keyspace e da tabela. Além disso, a
anotação @PartitionKey deve ser colocada no campo que representa
a partition key da tabela (no caso de User, o campo login). Outras
anotações úteis também estão disponíveis como, por
exemplo, @Column, para informar o nome da coluna caso não seja
igual ao nome do atributo na classe, e @Transient, para ignorar um
campo no mapeamento. Como essa API usa métodos acessores para
recuperar e gravar valores nos campos, também é necessário ter os
respectivos gets/sets.

Por fim, temos também a anotação @NotBlank, a qual faz parte do


Hibernate Validator (uma implementação da especificação Bean
Validation) e como o nome diz, não permite que o campo fique em
branco (nulo ou vazio).

Criando a tabela de usuário no Cassandra


Como visto anteriormente, podemos executar comandos CQL no
Cassandra pelo cliente de linha de comando cqlsh, mas, para este
tutorial iremos utilizar a ferramenta gráfica DevCenter.

A partir deste ambiente, na view Connections é possível criar uma nova


conexão informando um nome qualquer para identificar a conexão e
um ou mais hosts que fazem parte do cluster. Esse wizard pode ser
visto na Figura 10. Como a configuração padrão do Cassandra não
solicita senha, não é necessário fornecer nenhuma credencial.
Figura 10. Criação de conexão no DevCenter.

O script disponibilizado na Listagem 8 cria o keyspace da aplicação e


também a tabela de usuário. Para executar esses comandos no
DevCenter basta usar a tecla de atalho Alt+F11 ou clicar no botão de
execução no editor CQL. Também é possível criar esses dois objetos
através de wizards que o próprio DevCenter oferece. Para isso, acesse
o menu File > New e escolha a opção desejada.

O keyspace é como um database em bancos de dados como o


PostgreSQL. É nele que serão definidas as tabelas e demais objetos do
banco que porventura sejam necessários. No CQL de criação do
keyspace existem dois pontos importantes que podem ser observados.

O primeiro é o replication_factor, que, como já informado em seções


anteriores, define em quantos nós diferentes será armazenada a
mesma informação. Como o nosso exemplo usará apenas um nó, o
replication factor será de 1, pois não é uma boa prática ter esse valor
maior do que o número de nós, além do que, pode gerar diversos
erros.
O segundo ponto é a classe que representa a estratégia de replicação.
No nosso exemplo será usada SimpleStrategy, que é indicada apenas
para cenários onde existe um único data center. Caso haja mais de um
DC deve-se usar NetworkTopologyStrategy. Para mais detalhes,
consulte Apache Cassandra Product Guide na seção Links.

No que se refere à criação da tabela, não há muito o que explicar, visto


a sintaxe ser bem semelhante ao SQL. O script simplesmente cria a
tabela user dentro do keyspace webshelf, caso essa tabela já não exista,
e define o campo login como sendo a primary key da tabela. E como o
campo login é o primeiro campo da primary key, ele também será
usado pelo Cassandra como partition key da tabela.

Listagem 8. Código para criação da tabela de usuário.

CREATE KEYSPACE IF NOT EXISTS webshelf WITH replication =


{'class':'SimpleStrategy', 'replication_factor':1};

CREATE TABLE IF NOT EXISTS webshelf.user (


login text,
name text,
password text,
PRIMARY KEY (login)
);

Implementando o template padrão para as


páginas JSF
Antes de criar a tela do cadastro de usuário, a primeira coisa a fazer é
criar um template padrão para as telas do WebShelf, conforme
a Listagem 9. O objetivo com isso é colocar o código que é comum a
todas as páginas nesse template e fazer uso do mesmo nos demais
XHTMLs. Assim, evita-se duplicação de código, aumenta-se o reuso,
melhora-se a padronização do layout da aplicação, entre outros
benefícios.

Listagem 9. Template padrão para telas do WebShelf (WEB-


INF/templates/default.xhtml).

<?xml version="1.0" encoding="UTF-8"?>


<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:p="http://primefaces.org/ui">

<h:head>
<title>WebShelf</title>
<meta name="viewport" content="user-scalable=no, width=device-width,
initial-scale=1.0, maximum-scale=1.0"/>
</h:head>

<h:body>
<f:view encoding="UTF-8" contentType="text/html" locale="pt_BR">
<p:layout fullPage="true">
<p:layoutUnit id="layoutWest" position="west" header="Menu"
collapsible="true" collapsed="true" rendered="#{renderedMenuBar}">
<h:form prependId="false">
<p:menubar>
<p:menuitem value="Home" outcome="home"/>
<p:menuitem value="Logout" action="#{loginController.logout}" />
</p:menubar>
</h:form>
</p:layoutUnit>

<p:layoutUnit id="layoutCenter" position="center"


header="#{mainContentTitle}">
<p:messages autoUpdate="true" globalOnly="true"/>
<ui:insert name="mainContent" />
</p:layoutUnit>
</p:layout>
</f:view>
</h:body>
</html>

O que esse template faz é basicamente dividir a tela em duas partes:


uma área para o menu e outra que ocupa a maior parte da tela para o
conteúdo das páginas. Isso é feito através dos
componentes p:layout e p:layoutUnit. Caso seja necessário, esses
componentes podem dividir a tela em cinco
partes: header (north), footer (south), left (west), right (east)
e center.

A meta tag viewport serve para informar ao browser como controlar


as dimensões e escalas da página. Em outras palavras, é através dessa
tag que o browser pode gerar uma visualização responsiva baseado no
tamanho da tela do dispositivo.

Note ainda que a área do menu irá iniciar sempre ocultada


(<p:layoutUnit collapsed="true”>) e poderá ser expandida pelo
usuário (<p:layoutUnit collapsible="true">). O template também
define o parâmetro renderedMenuBar, para identificar se o menu
será ou não renderizado.

Já a parte central da tela será reservada para o conteúdo específico das


demais páginas. Essa área é definida pela tag <p:layoutUnit
position="center"> e faz uso do parâmetro mainContentTitle como
título do painel principal da aplicação. Além disso, possui um
componente de renderização de mensagens do PrimeFaces para
exibir mensagens globais do JSF (sem associação com nenhum
componente da tela), que é útil, por exemplo, para alertar o usuário
em caso de algum erro ou mensagem informativa. Por fim, a
tag ui:insert determina o local onde o conteúdo de cada um dos
XHTMLs será inserido no template.

O Apache Cassandra é um banco de dados NoSQL que vale a pena se


conhecer para ter como alternativa aos tradicionais bancos de dados
relacionais. Isso decorre do fato de ele prover uma série de facilidades
e condições essenciais para uma arquitetura distribuída em nível de
banco de dados.

Como desenvolvedor, a visão aqui apresentada dá uma base


conceitual do assunto que o deixa apto a se aprofundar no “Planeta
Cassandra”. No entanto, vale ressaltar que é importante conhecer um
pouco mais à fundo a parte prática, tema que será abordado com mais
detalhes no próximo artigo.

Outro aspecto a ser observado é que o Cassandra tem suas aplicações,


mas não é uma bala de prata que irá resolver todos os problemas
relacionados a banco de dados. Assim, é interessante saber identificar
em quais cenários essa tecnologia se encaixa melhor e para quais ela
não é uma boa solução. Por exemplo, foi informado que ele não tem
suporte a transações ACID. Deste modo, se sua aplicação precisa
disso, provavelmente o Cassandra não será uma boa opção. Por outro
lado, caso seu software esteja precisando escalar linearmente,
distribuir dados pela rede e ter tolerância a falhas, o Cassandra será
um forte candidato.

Para identificar esses cenários, a melhor opção é ler sobre cases de


empresas que já estão usando esse banco e entender quais problemas
ele resolveu. Existem diversos relatos desse tipo no site Planet
Cassandra (veja a seção Links). Da mesma forma, você pode ler sobre
empresas que deixaram o Cassandra e adotaram outra solução para
aprender quando ele não é tão eficiente.

Links

NoSQL Distilled.

http://martinfowler.com/books/nosql.html

Apache Cassandra Product Guide.

http://docs.datastax.com/en/cassandra/2.2/index.html

DataStax Dev Blog.

http://www.datastax.com/dev/blog

Planet Cassandra.

http://www.planetcassandra.org/

Cassandra introduction at FinishJUG.

http://pt.slideshare.net/doanduyhai/cassandra-introduction-at-finishjug

Cassandra Modeling Kata.

https://github.com/allegro/cassandra-modeling-kata

PrimeFaces Web Site.

http://www.primefaces.org
Parte II
Veja abaixo a segunda parte do artigo - Agora as partes I e II foram
compiladas em um único artigo. Bons estudos :)

Como usar o Apache Cassandra em


aplicações Java EE – Parte 2

Por que eu devo ler este artigo:Veremos neste artigo uma visão prática da
utilização do Apache Cassandra seguindo as melhores recomendações do
mercado. Assim, para quem conhece o Cassandra num nível apenas teórico,
poderá aqui “colocar a mão na massa” e dar os primeiros passos neste que é um
dos bancos de dados NoSQL mais empregados.

Ademais, tudo é feito dentro do contexto de uma aplicação Java EE, demonstrando
como essas duas tecnologias podem ser integradas. A aplicação de exemplo
utilizará o driver da DataStax para se comunicar com o Cassandra, WildFly 9,
PrimeFaces 5.3, Cassandra 2.2, além de outras tecnologias.

Com a explosão da Internet que vivenciamos atualmente, muitos


novos desafios estão surgindo para a indústria de software, desde a
preocupação com a escalabilidade das aplicações, que agora possuem
milhões de usuários, até mesmo a melhoria contínua da usabilidade
desses sistemas, dado que os usuários se tornam cada vez mais
exigentes e desejam mais facilidades.

Um desses desafios trata-se do armazenamento e processamento da


imensa massa de dados gerada pelos diversos serviços disponíveis.
Por conta dela, novas tecnologias de banco de dados emergiram nos
últimos anos para atender uma série de requisitos que o modelo mais
tradicional (Relacional) não conseguiu suprir. A esse novo movimento
de tecnologias de bancos de dados deu-se o nome de NoSQL.

Diante da relevância do tema, apresentamos na primeira parte desse


artigo um dos bancos NoSQL mais renomados desse ecossistema, o
Apache Cassandra. Para isso, foram abordados em detalhes vários
aspectos da arquitetura do Cassandra, fornecendo ao leitor um
embasamento teórico fundamental para o aprendizado prático desse
banco de dados. Além disso, demos início à implementação de uma
aplicação Java EE, onde detalhamos a preparação do ambiente de
desenvolvimento, a configuração do projeto, a apresentação do driver
Cassandra, uma introdução ao DevCenter e o desenvolvimento inicial
da integração entre Cassandra e Java EE.

Nesta segunda etapa do artigo, iremos aprofundar a parte prática e


evoluir a aplicação de modo que ela se torne funcional já com quase
todas as funcionalidades propostas. Ao final o leitor será capaz de
executar comandos no Cassandra via Java, tanto usando CQL
diretamente quanto usando a API object-mapping. O leitor também irá
adquirir um entendimento básico sobre modelagem de dados no
Cassandra, que como dito na primeira parte do artigo, é bem diferente
da modelagem relacional.

Para tanto, evoluiremos nossa aplicação adicionando funcionalidades


para executar comandos CQL de forma a seguir as recomendações do
driver Cassandra, bem como demonstraremos a maneira adequada de
utilizar tais funcionalidades. Além disso, serão abordados os novos
recursos do PrimeFaces para criar páginas responsivas, a API de
validação client-side dessa biblioteca, algumas features do CDI, como
o uso de qualifiers e eventos, alguns recursos do DeltaSpike, entre
outros. Todas essas opções serão utilizadas de forma a facilitar a
implementação da aplicação de exemplo, bem como demonstrar a
integração de todas essas tecnologias.

Evoluindo o WebShelf
Na primeira parte do artigo iniciamos o desenvolvimento do
WebShelf: uma aplicação que possibilita aos seus usuários manter
uma prateleira de livros online ao estilo do Amazon Shelfari. Até o
momento, as principais configurações do projeto já foram
demonstradas e explicadas, o que nos possibilita agora focar mais nas
funcionalidades da aplicação.

Elaborando a página responsiva de


Cadastro de Usuário com PrimeFaces
Como na primeira parte foi criado o template padrão (default.xhtml)
das páginas JSF para a aplicação WebShelf, podemos agora criar a tela
de Cadastro de Usuário. A Listagem 1 traz essa implementação.
Listagem 1. Tela de cadastro de Usuário (public/insertUser.xhtml).

<?xml version='1.0' encoding='UTF-8' ?>


<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:p="http://primefaces.org/ui"
template="/WEB-INF/templates/default.xhtml">

<ui:param name="mainContentTitle" value="Cadastro de Usuário" />


<ui:param name="renderedMenuBar" value="false" />

<ui:define name="mainContent">
<h:form prependId="false">
<div class="ui-fluid">
<p:panelGrid columns="3"
layout="grid"
columnClasses="ui-
grid-col-1,ui-grid-col-4,ui-grid-col-7"
styleClass="ui-
panelgrid-blank">

<p:outputLabel
for="userName" value="Nome" />
<p:inputText
id="userName" value="#{userController.user.name}" />
<p:message
for="userName" />

<p:outputLabel
for="userLogin" value="Login" />
<p:inputText
id="userLogin" autocomplete="false"

value="#{userController.user.login}" />
<p:message
for="userLogin" />

<p:outputLabel
for="userPassword" value="Senha" />
<p:password
id="userPassword" autocomplete="false"
match="userPasswordConfirmation"

value="#{userController.password}" />
<p:message
for="userPassword" />

<p:outputLabel
for="userPasswordConfirmation"

value="Confirmar Senha" />


<p:password
id="userPasswordConfirmation" autocomplete="false"

value="#{userController.password}" />
<p:message
for="userPasswordConfirmation" />

<p:commandButton
value="Salvar" action="#{userController.insert}"

ajax="false" validateClient="true" />

</p:panelGrid>
</div>
</h:form>
</ui:define>
</ui:composition>

De modo simples, essa página define os dois parâmetros que o


template precisa através da tag ui:param. Em seguida, adiciona o seu
conteúdo no template através da tag ui:define.

Para deixar a tela mais responsiva envolvemos todo o conteúdo da


página numa div que tem a classe ui-fluid do PrimeFaces. Através
dessa classe diversos componentes deste framework são renderizados
de forma responsiva.

Ainda pensando na responsividade, utilizamos o


componente p:panelGrid (não confundir com o componente padrão
do JSF h:panelGrid) em conjunto com o Grid CSS, ambos do
PrimeFaces. O Grid CSS é um layout leve que produz uma interface
responsiva para celulares, tablets e desktops e está presente no
PrimeFaces desde sua versão 5.1, possibilitando ao desenvolvedor
dividir a tela em 12 colunas de mesmo tamanho.

O p:panelGrid, ao ser configurado com o layout grid, irá fazer uso do


Grid CSS para definir o tamanho de cada uma das suas colunas. Isso é
configurado através do atributo columnClasses, que define uma
classe CSS indicando quantas colunas do Grid CSS elas devem ocupar
(ui-grid-col-1, ui-grid-col-4, ui-grid-col-7) totalizando as 12 colunas
que preenchem a tela. No nosso exemplo, a primeira coluna
do p:panelGrid irá ocupar 1 coluna do Grid CSS (ui-grid-col-1), a
segunda coluna irá ocupar 4 colunas do Grid CSS (ui-grid-col-4) e a
terceira 7 colunas do Grid CSS. Caso você precise fazer um arranjo
diferente, é interessante saber que existem 12 classes ui-grid-col-*,
indo de ui-grid-col- 1 até ui-grid-col-12.

Para encerrar, temos um p:commandButton para invocar o método


que irá gravar o usuário no banco de dados. Esse botão irá usar a
validação no lado do cliente oferecida pelo PimeFaces
(validateClient="true"). Dessa forma, erros simples de validação
como tamanho máximo do campo e campo obrigatório podem ser
alertados antes de qualquer requisição ser enviada para o servidor,
evitando roundtrips desnecessários e melhorando a performance da
aplicação. Para que essa feature seja usada, é necessário ativá-la
através do
parâmetro primefaces.CLIENT_SIDE_VALIDATION no web.xml,
como pode ser visto na Listagem 5 da parte 1, publicada na edição
anterior.

Desenvolvendo o controller de Usuário


Implementada a página web do Cadastro de Usuário, na Listagem
2 criaremos o controller que irá intermediar a camada de negócio com
a UI.

Listagem 2. Código do controller de usuário (webshelf-web).

package br.com.devmedia.webshelf.controller;

import java.io.Serializable;

import javax.faces.view.ViewScoped;
import javax.inject.Inject;
import javax.inject.Named;

import org.apache.deltaspike.jsf.api.message.JsfMessage;
import org.hibernate.validator.constraints.NotBlank;

import br.com.devmedia.webshelf.model.User;
import br.com.devmedia.webshelf.service.UserBean;
import br.com.devmedia.webshelf.util.Messages;

@Named
@ViewScoped
public class UserController implements Serializable {

private static final long serialVersionUID = 1L;

@Inject
private UserBean userBean;

@Inject
private JsfMessage<Messages> messages;

private User user = new User();

@NotBlank(message="Senha: não pode está em branco.")


private String password;

public User getUser() {


return user;
k! }

public String getPassword() {


return password;
}

public void setPassword(String password) {


this.password = password;
}

public String insert() {


user.setClearPassword(password);
this.userBean.insertUser(this.user);
messages.addInfo().insertUserSuccess();
return "login.xhtml?faces-redirect=true";
}
}

Como podemos notar, UserController é um bean CDI com escopo


View. Esse escopo é obtido através da
classe javax.faces.view.ViewScoped, presente a partir do JSF 2.2, e
não deve ser confundida com a antiga
classe javax.faces.bean.ViewScope, que tem vários problemas, como
pode ser visto em The benefits and pitfalls of ViewScoped (veja a
seção Links).

A anotação @Named permite que o bean possa ser acessado por um


nome específico, por exemplo, em telas JSF através de Expression
Language. Como não foi especificado nenhum nome no
atributo value, então o nome do bean passa a ser o nome da classe
com a primeira letra minúscula: userController.

Já o atributo messages usa a classe JsfMessage do DeltaSpike, uma


biblioteca que fornece diversas extensões para se trabalhar com Java
EE e que nasceu da junção de outros players (JBoss Seam e Apache
CODI). A classe JsfMessage fornece uma maneira elegante e simples
de adicionar mensagens JSF no contexto atual. Para isso, basta criar
uma interface como na Listagem 3.

Listagem 3. Classe de mensagens (webshelf-business).

package br.com.devmedia.webshelf.util;

import org.apache.deltaspike.core.api.message.MessageBundle;
import org.apache.deltaspike.core.api.message.MessageTemplate;

@MessageBundle
public interface Messages {

@MessageTemplate("Agora é só informar os dados de login e iniciar a


sua prateleira de livros online!")
String insertUserSuccess();
@MessageTemplate("Login/Senha inválido.")
String invalidCredentials();
}

Preparando CassandraCluster para suportar


object-mapping
Antes de implementar a lógica de negócio, será necessário adicionar
alguns métodos na classe CassandraCluster para que possamos, de
fato, executar comandos no Cassandra, visto que anteriormente
havíamos definido apenas alguns atributos. O primeiro deles é o
método mapper(), apresentado a seguir. Sua função será criar uma
instância da classe Mapper que irá possibilitar o uso da API object-
mapping do driver DataStax.

<E> Mapper<E> mapper(Class<E> entityClazz){


return mappingManager.mapper(entityClazz);

Este método simplesmente delega sua execução para o


método MappingManager.mapper() e como parâmetro recebe
qualquer classe anotada com @Table, como é o caso de User.

O seu retorno é uma instância da classe Mapper parametrizada com o


mesmo tipo da classe do parâmetro entityClazz, e dessa forma, irá
prover operações como busca, inserção e deleção de registros na
tabela correspondente da entidade sem que seja necessário escrever o
CQL diretamente.

Implementando o cache de
PreparedStatements em CassandraCluster
Como dito na seção “Regras de utilização do driver DataStax”, se você
perceber que irá executar um statement de forma repetida,
recomenda-se que essas instruções sejam feitas através
de PreparedStatement. Além disso, as instâncias dessa classe
precisam ser mantidas num cache para evitar que o mesmo CQL seja
preparado mais de uma vez, o que pode gerar problemas de
performance.

Para implementar tais recomendações utilizamos no nosso exemplo


um Map de PreparedStatements, onde o Map será instanciado uma
única vez dentro da classe CassandraCluster e com isso se
encarregará de fazer o cache desses objetos (vide BOX 1). A ideia é
tornar a própria String que representa o comando CQL (passada como
parâmetro) a chave do Map. Dessa forma, para cada instrução CQL
passada nesse método haverá apenas um PreparedStatement. Essa
estratégia pode ser vista através do
método CassandraCluster.prepare(), apresentado na Listagem 4.

Listagem 4. Cache de PreparedStatement em CassandraCluster.

private BoundStatement prepare(String cql){


if(!preparedStatementCache.containsKey(cql)){
preparedStatementCache.put(cql, session.prepare(cql));
}

return preparedStatementCache.get(cql).bind();
}

O ganho com PreparedStatements é verificado quando uma instrução é


executada repetidas vezes, pois com esse tipo de statement o parse
acontece uma única vez em cada nó que for executá-lo. Nas execuções
subsequentes, apenas o ID do statement e os valores dos parâmetros
são enviados pela rede. Considerando isso em um ambiente
distribuído com vários nós, uma melhora de performance significativa
pode ser obtida.

Por fim, saiba que o método prepare() sempre retornará uma nova
instância de BoundStatement, a qual será utilizada pelos clientes para
fazer o bind dos parâmetros contidos na instrução CQL.

BOX 1. Caches

Como já demonstrado na implementação de CassandraCluster, a utilização de

caches ao se trabalhar com Cassandra é de fundamental importância. Portanto,

vale a pena estudar mais a fundo as estratégias de cache a fim de identificar a

que melhor se adequa à sua realidade. Apesar de ser possível utilizar maps

para esse intuito, como fizemos aqui, esta não é a única maneira e,

principalmente, não é a melhor abordagem para grandes aplicações. Por

exemplo, você pode precisar de um cache que expira itens (mais antigos,
menos utilizados, etc.) ou caso contrário irá acumular uma grande quantidade

de objetos e poderá ter problemas de estouro de memória.

Adicionando método para execução de


comandos no CassandraCluster
Para que a classe CassandraCluster esteja com todas as principais
funcionalidades disponíveis, agora falta incluir o método execute(),
apresentado na Listagem 5. Como o próprio nome já diz, esse método
será o responsável por executar instruções CQL no Cassandra.

Listagem 5. Método para execução de CQL em CassandraCluster.

@Lock(LockType.READ)
public ResultSet execute(Statement stmt){
return session.execute(stmt);
}

Apesar desse código ser pequeno é interessante prestar bem atenção


para entender o que acontece. Basicamente, ele recebe
um Statement e delega a chamada para o
método Session.execute() que, por sua vez, irá executar o comando
no Cassandra.

Ao finalizar a execução, o método retorna um ResultSet que pode ser


utilizado para obter os dados de uma consulta (no caso do comando
ser uma consulta).

A parte interessante nesse método é a presença da anotação @Lock.


Essa anotação faz parte da especificação EJB e deve ser usada em
conjunto com EJBs singleton, como é o caso de CassandraCluster. Um
EJB singleton, por padrão, não permite que mais de uma thread
invoque um método de negócio. Nesse caso, entenda-se método de
negócio como qualquer método de classe público, como é o caso
de execute().

Assim, um cliente terá que esperar o outro terminar para que então
seu pedido seja atendido. Por exemplo, suponha que a
thread A chamou execute() passando um statement que vai demorar
cinco minutos para finalizar sua execução. Quando a chamada
de A estava com 1 minuto de execução, a thread B também
invocou execute() com outro statement, que nesse caso irá levar
apenas 1 segundo para executar. No entanto, como A iniciou a
execução primeiro, a thread B só será atendida quando A terminar, ou
seja, o comando de B que deveria levar apenas 1 segundo irá levar 4
minutos (tempo restante para completar a execução de A) e 1
segundo.

Para que isso não aconteça, utilizamos @Lock(LockType.READ) para


sinalizar ao container EJB que esse método, especificamente, não
deve funcionar como da forma descrita anteriormente. Assim, não
haverá lock no método execute() e qualquer cliente que invocá-lo será
atendido de imediato.

Nesse momento os leitores mais experientes podem se perguntar:


“Mais o que acontece quando diversas threads chamam esse método
simultaneamente? Não há perigo de uma chamada atrapalhar a
outra?”. A resposta é que não há problema algum. Como dito na
primeira parte deste artigo, a classe Session é thread-safe e, portanto,
pode ser utilizada num ambiente multithreading sem qualquer perigo.

Outro método que poderia sofrer esse mesmo problema seria


o prepare(). Isso porque ele também pode ter execuções que vão levar
um tempo considerável e, por isso, ficar aguardando sua finalização
geraria um grande gargalo. No nosso cenário isso não irá acontecer
porque esse método é privado e, sendo assim, não sofre o lock do
container, pois não é considerado um método de negócio. No entanto,
se sua aplicação precisar expor esse método, teria também que
remover o lock do container EJB. Caso você necessite usar o controle
de lock constantemente, isso pode indicar que é preciso repensar a
implementação, como explicado no BOX 2.

BOX 2. Exposição de Session

Assim como os métodos execute() e prepare(), caso outros métodos da

classe Session precisem ser expostos em CassandraCluster, uma outra

estratégia seria criar uma classe encapsuladora; CassandraSession, por

exemplo. Essa nova classe teria uma instância de Session encapsulada e só

seriam expostos os métodos que fossem convenientes, evitando-se expor

métodos de configuração da classe Session para toda a aplicação. Na


classe CassandraCluster, então, seria criado um

método getCassandraSession() que retornaria esse novo objeto. Dessa forma,

a classe Session se manteria privada a um único ponto da aplicação, bem como

a preocupação com locks seria eliminada, já que os métodos de longa duração

seriam chamados fora do contexto

EJB: cassandraCluster.getCassandraSession().execute().

Criando o CQL para inserir Usuário


Agora que o método prepare() está implementado, pode-se criar
algumas instruções, como a de inserção de um usuário, demonstrada
na Listagem 6. O intuito desse método é disponibilizar para o cliente
uma instância de BoundStatement com o comando necessário para
inserir um usuário no Cassandra. De posse do statement o
desenvolvedor poderá setar os valores dos parâmetros (identificados
pelo caractere ?) e então fazer a execução do comando.

Note que a sintaxe do CQL nesse exemplo é bem semelhante à do SQL.


A principal diferença está no uso da instrução IF NOT EXISTS, que será
melhor explicada na seção “Usando Lightweight Transactions (LWT)
para garantir a unicidade”, logo mais à frente.

Listagem 6. CQL para inserir usuário (CassandraCluster).

public BoundStatement boundInsertUser(){


return prepare("INSERT INTO webshelf.user(login,name,password)
VALUES (?,?,?) IF NOT EXISTS;");
}

Enfim, o bean de Usuário


Feitas as melhorias em CasandraCluster, agora é possível criar o bean
da entidade usuário, UserBean. Trata-se de um EJB Stateless que usa
CDI para injetar algumas dependências, como verificado na Listagem
7. O sufixo “Bean” refere-se à convenção de nomes de EJB.

Listagem 7. Código do bean de usuário (webshelf-business).

package br.com.devmedia.webshelf.service;
import java.util.Set;

import javax.ejb.Stateless;
import javax.ejb.TransactionAttribute;
import javax.ejb.TransactionAttributeType;
import javax.inject.Inject;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import javax.validation.Validator;

import com.datastax.driver.core.BoundStatement;
import com.datastax.driver.core.ResultSet;
import com.datastax.driver.mapping.Mapper;

import br.com.devmedia.webshelf.data.CassandraCluster;
import br.com.devmedia.webshelf.exception.BusinessRuleException;
import br.com.devmedia.webshelf.model.User;

@Stateless
@TransactionAttribute(TransactionAttributeType.NOT_SUPPORTED)
public class UserBean {

@Inject
private CassandraCluster cassandra;

@Inject
private Validator validator;

public User findUserByLogin(String login) {


Mapper<User> mapper = cassandra.mapper(User.class);
return mapper.get(login);
}

public void deleteUser(User user) {


Mapper<User> mapper = cassandra.mapper(User.class);
mapper.delete(user);
}

public void insertUser(User user) {


executeBeanValidation(user);

BoundStatement insertUser =
cassandra.boundInsertUser();
insertUser.bind(user.getLogin(), user.getName(),
user.getPassword());

ResultSet result = cassandra.execute(insertUser);

if (!result.wasApplied()) {
throw new BusinessRuleException("Login já
existente.");
}
}

private void executeBeanValidation(User user) {


Set<ConstraintViolation<User>> constraintViolations =
validator.validate(user);

if (!constraintViolations.isEmpty()) {
throw new
ConstraintViolationException(constraintViolations);
}
}
}

Como pode ser visto no método executeBeanValidation(), nesta


classe injetamos um Validator para invocar as validações da API Bean
Validation. Além disso, é injetado um CassandraCluster para realizar
a comunicação com o Cassandra.

Ainda analisando este código, o método findUserByLogin() obtém


um Mapper<User> e então faz a consulta pelo login, que nesse caso é
também a primary key da tabela. Essa consulta é realizada através do
método Mapper.get(), que aceita uma lista de parâmetros
correspondente à primary key da tabela na ordem declarada na sua
criação. O retorno é um objeto com os campos devidamente
preenchidos graças ao mapeamento feito na classe User. Operação
semelhante acontece no método deleteUser(), que também faz uso
de Mapper para remover o registro.
Usando Lightweight Transactions (LWT)
para garantir a unicidade
Por fim, tem-se o método insertUser(), o qual cadastra o usuário no
Cassandra. Apesar da classe Mapper possuir um método save() que
poderia ser usado aqui, foi necessário criar um statement
manualmente através do
método CassandraCluster.boundInsertUser() para que fosse possível
usar lightweight transactions, já que o Mapper não oferece (ainda) essa
possibilidade.

Como já informado, no WebShelf o login do usuário é único. Para


garantir essa regra em bancos de dados relacionais, normalmente
usamos uma transação, na qual é executada uma consulta para
verificar a existência do login. Caso ele não exista, é executado o
comando de insert e finalmente é feito o commit da operação.

No Cassandra, por sua vez, não é possível proceder dessa forma


devido à ausência de transações ACID. Desse modo, se você tentar
fazer isso, poderá cair numa race condition, como exemplificado
na Figura 1. Para contornar esse problema foi criado o conceito de
LWT, que no caso da inserção do usuário se caracteriza pelo uso da
seguinte condição ao fim do insert: IF NOT EXISTS. Ou seja, ao invés
de fazer uma consulta para verificar se o login existe ou não, no ato do
insert já será feito essa checagem sem que outras requisições
interfiram na operação.

A execução dessa instrução retorna um ResultSet a partir do qual é


possível checar se o insert foi aplicado ou não através do
método wasApplied(). Caso este retorne false, significa que o login já
existe.

Contudo, como dito na primeira parte deste tutorial, essa feature deve
ser usada com moderação, pois impacta fortemente na performance.
Nesse exemplo, optou-se por fazer uso de LWT porque a regra de
unicidade de usuário é considerada crítica para a aplicação.
Figura 1. Exemplo de race condition – Fonte: FinishJUG.

Implementação do Login
Como em quase todas as aplicações web, também iremos desenvolver
um mecanismo de login. O funcionamento deste será simples: o
usuário terá que informar o seu login e sua senha e, caso tudo esteja
correto, deverá ser redirecionado para a página inicial da aplicação,
caso contrário, uma mensagem de erro deverá ser mostrada. Para
quem ainda não tem cadastro, será disponibilizado um botão que
levará o usuário para a tela de Cadastro de Usuário.

Elaborando página responsiva de login com


PrimeFaces
A tela de login tem quatro componentes principais, conforme pode ser
visto na Listagem 8: um input de login, outro da senha, um botão para
efetuar o login e outro para se cadastrar.

Listagem 8. Código da tela de login (public/login.xhtml).

<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:p="http://primefaces.org/ui"
template="/WEB-INF/templates/default.xhtml">
<ui:param name="mainContentTitle" value="WebShelf" />
<ui:param name="renderedMenuBar" value="false" />

<ui:define name="mainContent">
<h:form prependId="false">
<div class="ui-fluid">
<p:panelGrid columns="3"
layout="grid"
columnClasses="ui-
grid-col-1,ui-grid-col-4,ui-grid-col-7"
styleClass="ui-
panelgrid-blank">

<p:outputLabel
for="userLogin" value="Login" />
<p:inputText
id="userLogin" autocomplete="false"

value="#{loginController.login}" />
<p:message
for="userLogin" />

<p:outputLabel
for="userPassword" value="Senha" />
<p:password
id="userPassword" autocomplete="false"

value="#{loginController.password}" />
<p:message
for="userPassword" />
</p:panelGrid>
<p:panelGrid columns="3"
layout="grid"
columnClasses="ui-
grid-col-1,ui-grid-col-2,ui-grid-col-2"
styleClass="ui-
panelgrid-blank">

<h:outputText />

<p:commandButton
value="Entrar"

action="#{loginController.doLogin()}" ajax="false"
validateClient="true" />

<p:button
value="Cadastrar-se" outcome="insertUser" />
</p:panelGrid>
</div>
</h:form>
</ui:define>
</ui:composition>

Observe que o parâmetro renderedMenuBar, logo no início do


código, foi setado para false, já que não deve ser exibido nenhum
menu enquanto o usuário não se logar.

Com o intuito de deixar a tela responsiva, utilizamos os mesmos


recursos apresentados na Listagem 1. Assim, todos os componentes
visuais da página foram englobados por uma div que tem a classe ui-
fluid e os inputs foram organizados dentro de
componentes p:panelGrid configurados para usar o Grid CSS
(layout=“grid”). Além disso, os p:panelGrid definiram o tamanho de
suas colunas em função das 12 colunas que o Grid CSS usa para dividir
a tela responsivamente (ui-grid-col-*).

Uma coisa a observar é que nessa página temos dois p:panelGrid. Isso
foi necessário porque, para gerar uma melhor visualização da tela, as
configurações dos tamanhos das colunas são diferentes para a área
dos campos texto (Login e Senha) e a área dos botões
(Entrar e Cadastrar-se). Enquanto a primeira área usa os tamanhos 1, 4
e 7 (columnClasses="ui-grid-col-1,ui-grid-col-4,ui-grid-col-7") para
suas três colunas, a segunda área usa os tamanhos 1, 2 e 2
(columnClasses="ui-grid-col-1,ui-grid-col-2,ui-grid-col- 2").

Ainda com relação aos botões, o primeiro (Entrar) será utilizado para
fazer o login e o segundo (Cadastrar-se) servirá para redirecionar o
usuário para a tela de cadastro de usuário, caso o mesmo ainda não
tenha se registrado no site.

As Figuras 2 e 3 demonstram o comportamento responsivo da tela de


login.
Figura 2. Tela de login visualizada em um computador.

Figura 3. Tela de login visualizada em um celular.

Desenvolvendo controller de login


Após criarmos o XHTML do login, podemos desenvolver a classe que
efetuará as principais ações dessa tela: LoginController. Basicamente,
essa classe terá a responsabilidade de executar as funcionalidades de
login e logout da aplicação e pode ser visualizada na Listagem 9.

Listagem 9. Controller do login (webshelf-web)

package br.com.devmedia.webshelf.controller;

// imports omitidos...

@Named
@RequestScoped
public class LoginController implements Serializable {
private static final long serialVersionUID = 1L;

@Inject
private HttpServletRequest request;

@Inject
private UserBean userBean;

@Inject
private JsfMessage<Messages> messages;

@NotBlank(message="Senha: não pode está em branco.")


private String password;

@NotBlank(message="Login: não pode está em branco.")


private String login;

public String getPassword() {


return this.password;
}

public String getLogin() {


return this.login;
}

public void setPassword(String senha) {


this.password = senha;
}

public void setLogin(String usuario) {


this.login = usuario;
}

public String doLogin() {


User user = userBean.findUserByLogin(login);

String encryptedPassword =
User.encryptPassword(password);
if(user==null ||
!user.getPassword().equals(encryptedPassword)){
messages.addWarn().invalidCredentials();
return null;
}

if(request.getSession(Boolean.FALSE) != null){

request.getSession(Boolean.FALSE).invalidate();
}

request.getSession().setAttribute("loggedInUser",
user);

return "/private/home.xhtml?faces-redirect=true";
}

public String logout() throws ServletException {


request.getSession().removeAttribute("loggedInUser");
request.getSession().invalidate();
return "/public/login.xhtml?faces-redirect=true";
}
}

Visto que não precisará guardar nenhum estado entre requests, esta
classe é um bean CDI com escopo de requisição (@RequestScoped). E
como ela será acessada nos XHTMLs (login.xhtml e default.xhtml)
através de Expression Language, também foi anotada com @Named.

Dentre os atributos da classe, os campos de login e senha (utilizados


na página de login) são devidamente validados com o auxílio da API
Bean Validation através da anotação @NotBlank. Dessa forma, ambos
são obrigatórios. Além desses dois, existem mais três campos na
classe, todos injetados via CDI:

● HttpServletRequest: utilizado para obter acesso a HttpSession;

● JsfMessage: serve para adicionar mensagens ao contexto JSF; e

● UserBean: utilizado para consultar o usuário.


A principal funcionalidade provida por LoginController é o login do
usuário, que é realizado através do método doLogin(). Este faz a
pesquisa do usuário pelo seu login e caso o mesmo exista e sua senha
seja igual à senha informada, então é considerado um login válido e
assim o objeto usuário é armazenado na sessão. Caso não exista o
usuário ou sua senha seja diferente do inputado, uma mensagem de
erro é retornada.

Por fim, o método logout() simplesmente remove o usuário da sessão


e invalida-a, bem como redireciona o usuário para a tela de login.

É oportuno salientar que a maneira utilizada aqui para fazer o


controle de login do sistema é muito simples e, portanto, deve ser
evitada em produção, conforme comentado no BOX 3.

BOX 3. Cassandra vs JAAS

Diferentemente de bancos de dados relacionais, não existe uma forma out-of-

box para implementar um secutiry-domain no WildFly com o Cassandra. Por

isso, nessa aplicação de exemplo utilizamos um mecanismo próprio e bem

simples de controle de acesso, ao invés de usar o JAAS (Java Authentication and

Authorization Service). No entanto, num ambiente de produção recomenda-se

investir tempo para criar um módulo de login que possa utilizar o Cassandra

em conjunto com o JAAS ou a utilização de algum framework de segurança

como o Apache Shiro.

Disponibilizando o usuário logado como


bean CDI
Obter o usuário logado é uma das tarefas mais comuns a qualquer
aplicação web. Para isso, iremos criar a classe ResourceProducer,
que fará uso das facilidades do CDI a fim de tornar essa tarefa mais
simples dentro do projeto WebShelf. Essa implementação é mostrada
na Listagem 10.

Listagem 10. Código da classe ResourceProducer (webshelf-web)

package br.com.devmedia.webshelf.util;
import javax.enterprise.context.SessionScoped;
import javax.enterprise.inject.Produces;
import javax.inject.Inject;
import javax.inject.Named;
import javax.servlet.http.HttpSession;

import br.com.devmedia.webshelf.model.User;

public class ResourceProducer {

@Inject
private HttpSession session;

@Produces
@LoggedInUser
@SessionScoped
@Named("loggedInUser")
protected User getLoggedInUser() {
User loggedInUser =
(User)session.getAttribute("loggedInUser");

if (loggedInUser == null) {
loggedInUser = new User();
}

return loggedInUser;
}
}

Na classe ResourceProducer temos apenas um


método: getLoggedInUser(). Este produz beans CDI (@Produces)
com escopo de sessão (@SessionScoped) e que são acessíveis via
Expression Language (@Named) através do nome loggedInUser. A
outra anotação (@LoggedInUser) é um qualifier CDI para denotar que
o bean aqui produzido equivale ao usuário logado. Sua implementação
e explicação serão apresentadas com a Listagem 11.

Como podemos observar, a implementação do


método getLoggedInUser() é bem simples. Ele retorna o usuário
logado que está na sessão caso exista um. Vale lembrar que o usuário
logado é colocado na sessão através do
método LoginController.doLogin(). Se não houver nenhum usuário
logado, então ele devolve um objeto User “vazio”. Isso porque não é
permitido retornar nulo em métodos produtores no CDI.

Listagem 11. Qualifier CDI para Usuário logado

package br.com.devmedia.webshelf.util;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import javax.inject.Qualifier;

@Qualifier
@Target({ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD })
@Retention(RetentionPolicy.RUNTIME)
public @interface LoggedInUser {

A anotação @LoggedInUser é um qualifier CDI (@Qualifier) usado


para produzir/injetar objetos do tipo User que representam um
usuário logado. Um qualifier nada mais é do que um meio de se
fornecer várias implementações para o mesmo tipo de bean.

Por padrão, toda classe é produzida pelo CDI através de seu construtor
sem argumentos, no entanto, é possível instruí-lo a criar o bean de
outras maneiras. No caso de User, criamos o
método ResourceProducer.getLoggedInUser() , que irá produzir
beans que representam um usuário logado, e para denotar isso, este
método foi anotado com @LoggedInUser. Dessa forma, quando uma
classe for injetar um objeto User, o CDI terá agora duas opções para
gerar o objeto: a opção default (construtor sem argumento) e o
método ResourceProducer.getLoggedInUser().

A questão que fica é: como informar ao CDI que desejamos obter o


objeto gerado pela classe ResourceProducer, ao invés do objeto
gerado pelo construtor default? É nesse contexto que entra o qualifier.
É através dele que o CDI saberá qual dos produtores usar. Veja o
exemplo na Listagem 12.

Listagem 12. Exemplo de uso de um Qualifier CDI

@Inject
@LoggedInUser
private User loggedInUser;

@Inject
private User dummyUser;

Nesse caso, como o atributo loggedInUser está anotado


com @LoggedInUser, ele será produzido pelo
método ResourceProducer.getLoggedInUser(), visto que somente
este método produz usuários com tal qualificador. Já o
atributo dummyUser, por não especificar nenhum qualifier, será
gerado pelo produtor padrão, que no caso é o construtor default da
própria classe (new User()).

Protegendo páginas privadas


Agora que já temos como obter e saber se existe um usuário logado na
aplicação, vamos criar um mecanismo de segurança para bloquear o
acesso a páginas privadas de usuários não autenticados. Para isso,
iremos utilizar um evento do CDI através da classe SecurityObserver,
apresentada na Listagem 13.

Listagem 13. Classe de segurança para páginas privadas (webshelf-


web).

package br.com.devmedia.webshelf.security;

import javax.enterprise.event.Observes;
import javax.faces.context.FacesContext;
import javax.faces.event.PhaseEvent;
import javax.servlet.http.HttpServletRequest;

import org.apache.deltaspike.jsf.api.listener.phase.AfterPhase;
import org.apache.deltaspike.jsf.api.listener.phase.JsfPhaseId;

import br.com.devmedia.webshelf.model.User;
import br.com.devmedia.webshelf.util.LoggedInUser;

public class SecurityObserver {


private static final String URL_PATTERN_PRIVATE_PAGES =
"/private/";
private static final String LOGIN_PAGE = "/public/login.xhtml";
private static final String HOME_PAGE = "/private/home.xhtml";

protected void checkAfterRestoreView(@Observes


@AfterPhase(JsfPhaseId.RESTORE_VIEW) PhaseEvent event,
HttpServletRequest request, @LoggedInUser
User user) {
this.redirectLoggedinUserToHome(user, request);
this.redirectAnonymousToLogin(user, request);
}

private void redirectLoggedinUserToHome(User user,


HttpServletRequest request) {
if (request.getRequestURI().contains(LOGIN_PAGE)) {
if (user.getLogin()!=null) {
handleNavigation(HOME_PAGE);
}
}
}

private void redirectAnonymousToLogin(User user,


HttpServletRequest request) {
if
(request.getRequestURI().contains(URL_PATTERN_PRIVATE_PAGES)) {
if (user.getLogin()==null) {
handleNavigation(LOGIN_PAGE);
}
}
}

private void handleNavigation(String page) {


FacesContext context =
FacesContext.getCurrentInstance();

context.getApplication().getNavigationHandler().handleNavigation(context,
null, page + "?faces-
redirect=true");
}
}

Nesta classe, o método checkAfterRestoreView() será notificado pelo


CDI todas as vezes que ocorrer o evento After Restore View do JSF.
Isso acontece porque este método possui um parâmetro anotado
com @Observes que é utilizado para indicar ao CDI que o método
precisa ser avisado quando o evento observado ocorrer.

O evento é definido com a próxima anotação; nesse


caso, @AfterPhase(JsfPhaseId.RESTORE_VIEW). Essa anotação é
fornecida pelo módulo JSF da biblioteca DeltaSpike e possibilita à
aplicação monitorar o ciclo de vida JSF como eventos do CDI,
dispensando assim o uso de PhaseListeners. Para conseguir capturar
os eventos, essa anotação deve ser utilizada em objetos do
tipo PhaseEvent.

Além de notificar o evento, o CDI ainda injetará os três parâmetros do


método: PhaseEvent, HttpServletRequest e também o usuário logado
(@LoggedInUser). Note que o PhaseEvent nem é utilizado, mas é
necessário para que se possa usar @AfterPhase.

Baseado nessas informações recebidas o


método checkAfterRestoreView() não permitirá que usuários logados
acessem a tela de login e os redirecionará para a página home da
aplicação (redirectLoggedinUserToHome()). Da mesma forma,
também não permitirá que usuários não logados acessem páginas
privadas, redirecionando-os para a tela de login
(redirectAnonymousToLogin()).

Cadastro de Livro
O cadastro de livro permitirá ao usuário do WebShelf cadastrar novos
livros que, por ventura, ele não tenha conseguido encontrar na
pesquisa. Essa funcionalidade seguirá o mesmo molde do cadastro de
usuário, que é composto de uma tela JSF, um controller e um bean de
negócio.

Criando a classe modelo de Livro


A primeira etapa a seguir é criar a classe de domínio Book, que será
composta dos seguintes campos: ISBN(único), título, autor, país,
editora e uma imagem. A implementação pode ser vista na Listagem
14. Diferentemente de User, esta classe não utilizará a API de object-
mapping, pois trata-se de um cenário mais complexo, onde será
necessário criar mais de uma tabela na base de dados para
representar a mesma entidade. Assim, para não termos que gerar
várias classes de livro (uma para cada tabela), preferiu-se não usar
essa feature do driver Cassandra.

Nota: Atualmente a API de object-mapping é bastante limitada, e por conta disso seu
uso será restrito a cenários mais simples, normalmente CRUDs sem qualquer feature

mais complexa do Cassandra.

Listagem 14. Código da classe Book (webshelf-business).

package br.com.devmedia.webshelf.model;

import java.nio.ByteBuffer;
import javax.validation.constraints.NotNull;
import org.hibernate.validator.constraints.NotBlank;

public class Book {

@NotNull(message = "ISBN: não pode estar em branco.")


private Long isbn;

@NotBlank(message = "Título: não pode estar em branco.")


private String title;

@NotBlank(message = "Autor: não pode estar em branco.")


private String author;

private String country;

private String publisher;

@NotNull(message = "Imagem: não pode estar vazio.")


private byte[] image;
//getters and setters

public ByteBuffer getImageBuffer() {


return ByteBuffer.wrap(image);
}

@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((isbn == null) ? 0 :
isbn.hashCode());
return result;
}

@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Book other = (Book) obj;
if (isbn == null) {
if (other.isbn != null)
return false;
} else if (!isbn.equals(other.isbn))
return false;
return true;
}
}

Por se tratar de um POJO, Book é uma classe simples que contém


basicamente seus campos e os respectivos métodos de acesso. A
mesma também faz uso de Bean Validation para assegurar as
validações básicas sob os seus atributos.
Um importante aspecto nessa classe é a implementação da
dupla equals() e hashCode(). Assim, a classe poderá ser utilizada em
conjunto com a API Collections de forma mais segura e consistente.
Como pode ser visto, um Book é considerado igual a outro se os seus
ISBNs forem iguais. Isso é importante porque, por conta da
desnormalização do Cassandra, essa unicidade do ISBN não poderá
ser garantida via banco de dados, como veremos logo mais.

Outra parte a se prestar atenção é o método getImageBuffer(). O


intuito desse método é converter o campo byte[] que representa uma
imagem em um objeto do tipo ByteBuffer. A razão disso é que o driver
do Cassandra utiliza esta última classe para mapear atributos Java
com suas colunas do tipo blob, justamente o tipo da coluna imagem
nas tabelas de livro.

Implementando a página responsiva de


Cadastro de Livro com PrimeFaces
A Listagem 15 apresenta o código da tela de Cadastro de Livro. De
forma simples, essa página contém os inputs necessários para
preencher todos os campos da classe Book, e assim como as demais,
também foi implementada de maneira responsiva, utilizando os
mesmos recursos do PrimeFaces. Portanto, não terá seu código
detalhado.

Listagem 15. Tela de Cadastro de Livro (private/insertBook.xhtml).

<?xml version='1.0' encoding='UTF-8' ?>


<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:p="http://primefaces.org/ui"
template="/WEB-INF/templates/default.xhtml">

<ui:param name="mainContentTitle" value="Cadastro de Livro" />


<ui:param name="renderedMenuBar" value="true" />

<ui:define name="mainContent">
<h:form prependId="false" enctype="multipart/form-
data">
<div class="ui-fluid">
<p:panelGrid columns="3"
layout="grid"
columnClasses="ui-
grid-col-1,ui-grid-col-4,ui-grid-col-7"
styleClass="ui-
panelgrid-blank">

<p:outputLabel
for="bookTitle" value="Título:" />
<p:inputText
id="bookTitle" value="#{bookController.book.title}"

/>
<p:message
for="bookTitle" />

<p:outputLabel
for="bookAuthor" value="Autor:" />
<p:inputText
id="bookAuthor" value="#{bookController.book.author}"

/>
<p:message
for="bookAuthor" />

<p:outputLabel
for="bookPublisher" value="Editora:" />
<p:inputText
id="bookPublisher"

value="#{bookController.book.publisher}" />
<p:message
for="bookPublisher" />

<p:outputLabel
for="bookCountry" value="País:" />
<p:inputText
id="bookCountry"

value="#{bookController.book.country}" />
<p:message
for="bookCountry" />
<p:outputLabel
for="bookIsbn" value="ISBN:" />
<p:inputText
id="bookIsbn" value="#{bookController.book.isbn}" />
<p:message
for="bookIsbn" />
</p:panelGrid>

<br />

<p:panelGrid columns="1"
layout="grid"
columnClasses="ui-
grid-col-1"
styleClass="ui-
panelgrid-blank">

<p:fileUpload
id="bookImage" label="Imagem" auto="true"

allowTypes="/(\.|\/)(jpg|jpeg|png)$/" sizeLimit="20480"

value="#{bookController.image}"

invalidFileMessage="Tipo de arquivo inválido."

invalidSizeMessage="Tamanho de arquivo não permitido."

fileUploadListener="#{bookController.uploadImage}"

process="@this"

update="@this bookImageName">

</p:fileUpload>

<h:outputText
id="bookImageName"

value="#{bookController.image == null ? '' :

bookController.image.fileName}" />
<h:outputText />

<p:commandButton
value="Salvar" action="#{bookController.insert}"

ajax="false" validateClient="true" />


</p:panelGrid>
</div>
</h:form>
</ui:define>
</ui:composition>

Modelando o cadastro de livros no


Cassandra
Antes de pensarmos na modelagem do cadastro de livros, é
importante contextualizarmos outra funcionalidade que irá impactar
diretamente na modelagem: a pesquisa de livros. Essa pesquisa será
um dos principais recursos do WebShelf e é através dele que será
possível consultar livros por ISBN, Título e Autor.

A utilização desses três campos do cadastro de livros para realizar


buscas demonstra claramente três padrões de consulta. Como
explicado na seção sobre modelagem de dados no Cassandra, na
primeira parte do artigo, uma das melhores práticas é modelar suas
tabelas com base nas consultas que precisarão ser realizadas.

Essa abordagem normalmente resulta na criação de uma tabela para


cada padrão de consulta. Isso significa que para atender os requisitos
da pesquisa de livros vamos criar três
tabelas: book_by_isbn, book_by_title e book_by_author, todas elas com
os mesmos dados. A diferença será a constituição de sua primary key,
mais especificamente, a partition key, como pode ser notado
na Listagem 16.

Essa modelagem é o principal motivo para não usarmos object-


mapping nas funcionalidades relacionadas ao cadastro de livros, já que
a definição da tabela através de @Table é estática e teríamos que criar
três classes para mapear cada uma das tabelas.

Listagem 16. Criação das tabelas de Livro.


CREATE TABLE IF NOT EXISTS webshelf.book_by_isbn (
isbn bigint,
title text,
author text,
country text,
publisher text,
image blob,
PRIMARY KEY (isbn)
);

CREATE TABLE IF NOT EXISTS webshelf.book_by_title (


isbn bigint,
title text,
author text,
country text,
publisher text,
image blob,
PRIMARY KEY (title, isbn)
);

CREATE TABLE IF NOT EXISTS webshelf.book_by_author (


isbn bigint,
title text,
author text,
country text,
publisher text,
image blob,
PRIMARY KEY (author, isbn)
);

E porque precisamos de três tabelas ao invés de uma, se todas têm as


mesmas informações?

A questão é que no Cassandra qualquer query precisa filtrar a tabela


no mínimo pelas colunas que compõem sua partition key, ou seja, se
você quiser filtrar a consulta de livros apenas pelo Título, terá que ter
uma tabela onde o campo title, sozinho, seja a partition key. Dito isso,
podemos observar que a única diferença entre cada tabela
apresentada na Listagem 15 é justamente esse elemento.
Assim, a tabela book_by_isbn, por exemplo, que será usada para buscar
livros de acordo com o seu ISBN, tem sua primary key composta
apenas por esse campo, e consequentemente, sua partition
key também é formada somente por esse campo.

Nota: Vale lembrar que a partition key é o primeiro campo ou o primeiro

conjunto de campos da primary key. Os exemplos a seguir deixam mais clara

essa definição:

· PRIMARY KEY (user_login, status, book_isbn): user_login é a


partition key;

· PRIMARY KEY ( (user_login, status), book_isbn): user_login e status,


colocados entre parênteses, formam a partition key.

Já a tabela book_by_title, que será usada para buscar livros de acordo


com o título, tem sua primary key composta pelas colunas title e isbn.
O campo title vem na primeira posição para assegurar que o mesmo
será a partition key da tabela. Deste modo, poderão ser executadas
consultas utilizando apenas esse o campo como filtro. Todavia, como
a coluna title não garante a unicidade dessa tabela, foi incluído o
campo isbn na primary key.

A tabela book_by_author, por sua vez, tem intensão semelhante


a book_by_title, sendo a única diferença que a mesma será usada para
pesquisar livros de acordo com o autor.

A lógica por trás da partition key, como explicado na primeira parte


do artigo, é que através dessa chave o Cassandra determina em qual
nó ficará armazenado o dado. Daí a necessidade de sempre ter esse
filtro nas consultas, pois sem essa restrição, como o Cassandra
poderia saber em qual nó do cluster está a informação?

Também pelo mesmo motivo, as condições suportadas para as


colunas que compõem a partition key são apenas igual (=) e in, ou
seja, o Cassandra precisa saber o valor exato da partition key para
determinar em qual máquina buscar a informação. Se fosse possível
fazer algo como uma operação like, o Cassandra teria de varrer todas
as máquinas do cluster para achar o dado procurado, o que resultaria
numa grande perda de performance. Um bom exemplo desse
mecanismo é descrito na primeira parte do artigo na seção
“Distribuição – A chave para a escalabilidade horizontal”.

Vale ressaltar que mesmo sendo permitido o uso do in, essa clausula é
vista como uma má prática por vários desenvolvedores, pois pode
obrigar o Cassandra a obter dados de vários nós diferentes numa
única operação. Por exemplo, digamos que no in você informe três
valores, e suponha que o particionador do Cassandra distribuiu os
dados dessas três chaves em máquinas diferentes. Nesse cenário, uma
única consulta fará com que o Cassandra colete dados de três nós
distintos para então retornar a resposta para o cliente. Esse tipo de
operação envolvendo várias máquinas acarreta uma perda de
performance considerável e por isso deve ser evitada.

Desenvolvendo a lógica de negócio de Livro


Como já modelamos as tabelas do cadastro de livros, agora é possível
pensar na lógica de negócio que será inserida na classe BookBean,
implementada na Listagem 17.

Assim como UserBean, esta classe também é um EJB Stateless sem


suporte a transações que injeta, através de CDI, os
objetos Validator e CassandraCluster. No entanto, o
método insertBook() traz novos conceitos. Como os livros precisam
ser cadastrados em três tabelas, a operação de insert é composta de
três statements, cada um inserindo os dados numa tabela distinta.

Listagem 17. Código da classe BookBean (webshelf-business)

package br.com.devmedia.webshelf.service;

//imports omitidos...

@Stateless
@TransactionAttribute(TransactionAttributeType.NOT_SUPPORTED)
public class BookBean {

@Inject
private CassandraCluster cassandra;

@Inject
private Validator validator;

public void insertBook(Book book) {


executeBeanValidation(book);

BatchStatement batch = new BatchStatement();

batch.add(cassandra.boundInsertBookByISBN().bind(book.getIsbn(),
book.getTitle(), book.getAuthor(),

book.getCountry(), book.getPublisher(), book.getImageBuffer()));

batch.add(cassandra.boundInsertBookByTitle().bind(book.getIsbn(),
book.getTitle(), book.getAuthor(),

book.getCountry(), book.getPublisher(), book.getImageBuffer()));

batch.add(cassandra.boundInsertBookByAuthor().bind(book.getIsbn(),
book.getTitle(), book.getAuthor(),

book.getCountry(), book.getPublisher(), book.getImageBuffer()));

cassandra.execute(batch);
}

private void executeBeanValidation(Book book) {


Set<ConstraintViolation<Book>> constraintViolations =
validator.validate(book);

if (!constraintViolations.isEmpty()) {
throw new
ConstraintViolationException(constraintViolations);
}
}
}

Essa operação de gravar o livro nas três tabelas modeladas


(book_by_isbn, book_by_title e book_by_author), de uma maneira geral,
poderia ser feita de duas formas: executar cada insert numa operação
independente ou executar os três inserts numa única operação
(batch).
A primeira abordagem, no entanto, poderia resultar numa base de
dados inconsistente, visto que o Cassandra não tem o conceito de
transações ACID. Dessa maneira, uma operação poderia completar
com sucesso e outra não. Por exemplo, suponha que o primeiro
comando seja o insert na tabela book_by_isbn e que o mesmo completa
com sucesso. Em seguida, durante os inserts nas
tabelas book_by_title e book_by_author, imagine que aconteça algum
erro e tais comandos não sejam gravados na base. Nessa situação, o
livro que deveria estar presente nas três tabelas só existirá em uma,
pois mesmo que os últimos dois comandos tenham sofrido algum
erro, a primeira execução já foi completada com sucesso, gravada na
base e já se encontra disponível para os demais clientes consultarem.

Usando BatchStatements para garantir a


atomicidade
Para contornar o problema descrito utilizamos a segunda abordagem
– o uso de batches – como recomenda a quarta regra de utilização do
driver DataStax. Por isso declaramos BatchStatement no
método insertBook() .

Nesse método, o batch é composto pelos três inserts do cadastro de


livro, como pode ser notado através das chamadas ao método add() da
classe BatchStatement. Este método recebe como parâmetro um
Statement que é enfileirado para posterior execução na ordem em que
foi adicionado. No código da Listagem 17 os statements são criados
pelos métodos CassandraCluster.boundInsert*(), que serão melhor
explanados ao analisarmos a Listagem 18.

Observe que o método add() não executa o comando ainda. Isso só


acontece na chamada ao método CassandraCluster.execute(), onde
o BatchStatement é passado como parâmetro
(cassandra.execute(batch)). Nesse momento, a execução de cada um
dos statements adicionados previamente acontece numa operação
atômica, e com isso, ou os três inserts funcionarão ou nenhum será de
fato persistido.

É importante ressaltar que o principal intuito para usar batches no


Cassandra é o que acabamos de citar: atomicidade. Portanto, não
espere melhorias de performance por conta disso. Na verdade, essa
atomicidade geralmente implica numa piora da performance, já que o
Cassandra terá que se preocupar com esse quesito (atomicidade da
operação), o que envolverá várias checagens extras para garantir a
consistência e que não ocorrem numa operação isolada.

Preparando instruções CQL para inserção


de livros
Para fechar o cadastro de livros falta apenas criar os métodos na
classe CassandraCluster que irão gerar os PreparedStatements com os
comandos necessários para a inclusão. Esses métodos podem ser
visualizados na Listagem 18.

Listagem 18. Comandos CQL para inserção de livros


(CassandraCluster),

public BoundStatement boundInsertBookByAuthor(){


return prepare("insert into webshelf.book_by_author
(isbn,title,author,country,publisher,image) values

(?,?,?,?,?,?);");
}

public BoundStatement boundInsertBookByTitle(){


return prepare("insert into webshelf.book_by_title
(isbn,title,author,country,publisher,image) values(?,?,?,?,?,?);");
}

public BoundStatement boundInsertBookByISBN(){


return prepare("insert into webshelf.book_by_isbn
(isbn,title,author,country,publisher,image) values(?,?,?,?,?,?);");
}

Note que cada um dos métodos boundInsertBook*() prepara uma


instrução CQL para executar um insert em uma das tabelas de livro.
Como essas tabelas são estruturalmente iguais, mudando apenas a
primary key e a partition key, os três comandos são semelhantes,
diferenciando-se unicamente pelo nome da tabela alvo.

Note ainda que as implementações desses métodos também são bem


parecidas, consistindo apenas em declarar o método prepare() para
que seja criado um PreparedStatement para cada um dos inserts e
que estes sejam armazenados num cache para utilização em
operações subsequentes. Em seguida, o método prepare() retorna
um BoundStatement que, por sua vez, é retornado para os clientes
poderem setar os parâmetros dos CQLs (caractere ?) e executar o
comando.

De acordo com o que apresentamos neste artigo, ficou evidente que o


Apache Cassandra é uma opção NoSQL bastante viável para utilização
com Java EE e as diversas tecnologias que cercam essa plataforma.
Isso pôde ser constatado quando abordamos o uso do Cassandra numa
aplicação com tecnologias como EJB, CDI, JSF, PrimeFaces e
DeltaSpike.

Neste segundo artigo o foco foi completamente voltado para a parte


prática, e mesmo para quem não conhecia muito a respeito do
Cassandra, podemos dizer que o leitor agora tem uma boa visão das
possibilidades, vantagens, melhores práticas e restrições que essa
tecnologia impõe.

Embora vários dos principais tópicos já tenham sido discutidos,


obviamente isso é apenas o início e ainda será preciso aprender
muitas outras técnicas, features e conceitos para se tornar um
profissional com domínio mais concreto sobre a tecnologia. Para
tanto, recomendamos que busque conhecer novas técnicas de
modelagem baseadas em cenários mais complexos. Isso fará com que
você “abra a cabeça” para pensar de uma forma “mais Cassandra e
menos relacional”. Duas ótimas fontes são as diversas apresentações
de cases reais disponíveis no SlideShare e no YouTube.

Além disso, sugerimos que o leitor se empenhe em ler a


documentação sobre CQL e Driver DataStax (veja na seção Links) para
que consiga atingir um nível profissional no Cassandra.

Para encerrar, vale ressaltar que se você não leu a primeira parte terá
bastante dificuldade em compreender aspectos mais avançados e
abstratos da arquitetura do Cassandra. Por isso, aconselhamos que dê
um passo atrás e leia o artigo introdutório, onde foi apresentado um
embasamento teórico essencial para evoluir no aprendizado dessa
tecnologia.

Você também pode gostar