Primeros pasos con Cassandra desde Java

De ChuWiki

Suponiendo que tenemos Apache Cassandra instalado vamos a ver aquí como acceder al servidor desde un programa Java.

Existen varios posibles drivers de acceso desde Java a Cassandra, como puede verse en http://www.datastax.com/download/clientdrivers . Básicamente hay dos protocolos de acceso a cassandra: CQL3 y Thrift. El primero es más alto nivel que el segundo, por lo que es relativamente más cómodo de usar y más similar a SQL, pero puede limitarnos un poco en determinados casos. Nos centramos aquí en CQL3 usando el driver datastax/java-driver. En este otro enlace tienes cómo hacerlo con el protocolo Thrift y el driver Hector


Conseguir el driver[editar]

El driver está accesible desde maven con la siguiente dependencia

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

La otra opción es descargase los fuentes del proyecto y compilarlos con maven.


Conexión a la base de datos[editar]

El código para conectarse a un cluster cassandra es el siguiente

Cluster cluster1 = Cluster.builder().addContactPoints("172.30.210.11","172.30.210.12").build();

Vemos que se pueden ir añadiendo varias IPs o nombres de host, correspondientes a distintos servidores donde corran ejecuables de Apache Cassandra dentro del módulo. En principio sólo es necesario dar una IP y a partir de ella el driver será capaz de interrogar a Cassandra por todas las demás IPs del conjunto de servidores Cassandra. Sin embargo, es buena idea dar más de una, de forma que si la única que damos está caída en ese momento, el driver intentará conectarse a la siguiente.

Una vez que tenemos el cluster, debemos conectarnos, con una de estas opciones

// Sin indicar keyspace
Session sesion = cluster1.connect();

// indicando un keyspace
Session sesion = cluster1.connect("unkeyspace");

Un keyspace es el equivalente en SQL a una de las bases de datos que hay en el servidor y donde de alguna forma se agrupan un conjunto de tablas. Podemos conectarnos sin indicar keyspace o bien conectarnos indicando uno que exista. En este segundo caso, si no existe, saltará una excepción.

La opción sin keyspace es adecuada para crear la primera vez un keyspace y un conjunto de tablas asociadas a él. Una vez existe el keyspace y las tablas, se podría usar la conexión con el keyspace, ahorrándonos así en las sentencias de insert, update, select el poner el keyspace delante de cada tabla. Vamos poco a poco, primero creamos el keyspace y una tabla


Crear un keyspace y una tabla[editar]

Como el keyspace no existe todavía, usamos la conexión sin keyspace

Session sesion = cluster1.connect();

La cración del keyspace es sencilla, basta ejecutar el siguiente código

sesion.execute("CREATE KEYSPACE prueba " +
   "WITH replication = " +
   "{ 'class' : 'SimpleStrategy', 'replication_factor' : 1 }");

Acabamos de crear un keyspace "prueba". En la creación, indicamos cuántas réplicas queremos del dato y cómo queremos que se distribuyan en los distintos servidores.

SimpleStrategy es adecuada cuando sólo tenemos un nodo (data center) con varios servidores de Cassandra. La otra opción es NetworkTopologyStrategy, adecuada cuando hay varios nodos (varios data center) cada uno de ellos con varios servidores Cassandra, de forma que la réplica de datos se repartirá también entre data center distintos.

replication_factor es cuántas copias queremos del dato. El numero debería ser menor o igual que el número de servidores cassandra disponibles. Si no hay bastantes servidores para las copias, los insert pueden fallar.

Ahora debemos crear tablas en este keyspace. Creamos una con un comando como el siguiente

session.execute(
      "CREATE TABLE prueba.songs (" +
            "id uuid PRIMARY KEY," + 
            "title text," + 
            "album text," + 
            "artist text," + 
            "tags set<text>," + 
            "data blob" + 
            ");");

Puesto que la conexión que hemos usado no indicaba keyspace, es necesario poner dicho keyspace delante del nombre de la tabla, prueba.songs. El resto son columnas que añadimos a la tabla. Aquí tenemos los posibles tipos de las columnas y su equivalente java.


Insertar datos[editar]

Una vez creado el keyspace y la tabla (realmente column family en el lenguaje propio de Cassandra), podemos insertar datos. Podemos usar la misma sesión que ya tenemos abierta que no está asociada a ningún keyspace, o podemos abrir una sesión nueva asociada al keyspace prueba. Si usamos la sesión existente que no está asociada a ningún keyspace, debemos poner delante de nuestra column family el nombre del keyspace, así

insert into prueba.songs ....

Si creamos una nueva sesión asociada al keyspace, esto no será necesario y eso es lo que vamos a hacer. El código java para hacer el insert sería como este

sesion = cluster1.connect("prueba");
String uuid=UUID.randomUUID().toString();
sesion.execute("insert into prueba.songs (id,title,album,artist,tags,data) "
   + "values ("
   + uuid
   + ",'titulo','album','artista',{'etiqueta1','etiqueta2'},0xffffffff);");

La sintaxis es más o menos la habitual de SQL, algo como

insert into songs (id,title,album,artist,tags,data) 
   values (B70DE1D0-9908-4AE3-BE34-5573E5B09F14,'titulo','album','artista',{'etiqueta1','etiqueta2'},0xffffffff);

Veamos algunos detalles sobre esto.


El id[editar]

En Cassandra no se generan automáticamente los id, no existen cosas como secuencias o claves autoincrementales, es cosa de nuestro programa el generarlos. Con Cassandra debemos diseñar nuestras column families de acuerdo a las consultas que queramos hacer, por lo que las claves deberían ser algún campo de valor único que tenga sentido para un usuario. Por ejemplo, el número de DNI (Documento Nacional de Identidad en España) de una persona, o su dirección de correo electrónico, etc. Si necesitamos un id que no sea parte de uno de los campos de nuestro modelo de datos, lo habitual es usar un UUID, que se puede generar fácilmente de forma aleatoria con java por medio de la clase UUID y su método randomUUID().

Nos guardamos el uuid puesto que es necesario para poder hacer luego update y delete.


Conjuntos[editar]

En nuestra tabla hemos definido un conjunto de etiquetas, el campo tags set<text>. Para rellenarlo, entre llaves ponemos las etiquetas separadas por comas, es decir, {'etiqueta1','etiqueta2'}


Blob[editar]

En nuestra tabla hay un campo blob, que no es más que una ristra de bytes que se almacenan tal cual. Para rellenarlo basta poner la ristra de bytes en hexadecimal precedida de 0x, como se ha visto arriba.


Update[editar]

El update es similar a sql, la sentencia se puede parecer a esto

update songs set title='nuevo titulo' where id=B70DE1D0-9908-4AE3-BE34-5573E5B09F14;

Le "pega" en Cassandra es que en el where debe ir obligatoriamente la clave primaria de nuestra column falmily. Por eso, nuevamente, es importante que definamos como clave primaria algo que forme parte de nuestro modelo de datos y tenga sentido desde el punto de vista del usuario. Si es algo tan exótico como un uuid y no lo sabemos, tendremos que hacer una consulta primero para buscar el id a partir de otra columna que sea conocida.

El código java para el update puede quedar así

sesion.execute("update songs set title='nuevo titulo' where id="+uuid);


Insert vs update[editar]

Hay un detalle en Apache Cassandra y es que, aparte de la sintaxis, no hay diferencia entre un insert y un update. Si hacemos un insert con una clave primaria que ya existe, Cassandra modificará la fila existente en base de datos, es decir, hará un update. Si hacemos un update con un id que no existe en base de datos, Cassandra añadirá un nueva fila a la base de datos, es decir, hará un insert.


Consultas[editar]

Para consultar, podemos hacer algo parecido a SQL. Por ejemplo, desde java y suponiendo conocido el uuid

ResultSet rs = sesion.execute("select * from songs where id="+uuid);
Iterator<Row> iterador = rs.iterator();
while (iterador.hasNext()){
   System.out.println(iterador.next().getString("title"));
}

El ResultSet que se obtiene no es el estándar de java.sql.ResultSet, sino uno específico del driver de Cassandra : com.datastax.driver.core.ResultSet

Pedimos un iterador y vamos recorriendo las filas de resultado. Podemos pedir cada columna por su nombre, los title en el ejemplo.

Si queremos consultar por cualquier otra columna que no sea el id, Cassandra nos obliga a tener creados índíces secundarios para esas columnas que queremos usar en el where. Por ejemplo, en el momento de crear las tablas y si prevemos que vamos a hacer consultas por la columna title, podemos crear un índice de esta forma

sesion.execute("CREATE INDEX idx_title ON prueba.songs (title)");

A partir de aquí, sí es posible hacer consultas usando la columna title en la cláusula where

rs = sesion.execute("select * from songs where title='nuevo titulo'");
iterador = rs.iterator();
while (iterador.hasNext()){
    Row fila = iterador.next();
    System.out.println(fila.getUUID("id")+" - " +fila.getString("title"));
}

La documentación de Cassandra indica que este tipo de índices secundarios son adecuados para aquellas columnas que tiene valores iguales en muchas de las filas. Por ejemplo, si tenemos una column family de personas en España, se puede hacer un índice secundario en una columna Ciudad, ya que habrá muchas personas que vivan en Madrid, otras muchas en Barcelona, etc. Sin embargo, no sería adecuado hacerlo para la columna email, puesto que cada persona tendrá presumiblemente un email distinto y no habrá dos iguales. Para consultas por este campo, hay una técnica especial en el diseño de las tablas, como se puede ver en http://rubyscale.com/blog/2011/03/06/basic-time-series-with-cassandra/ relativo a búsquedas por fecha/hora.

Borrado[editar]

El borrado es como se espera, un simple delete con el id de la fila a borrar.

sesion.execute("delete from songs where id="+uuid);

Enlaces externos[editar]