NoSQL - Cassandra
Ejemplo práctico
Películas
Versión 1.1
Mayo 2017
Por Fernando Dodino
Distribuido bajo licencia Creative Commons Share-a-like
Indice
1 Tecnologías en las que vamos a trabajar
2 Primer ejemplo: Base de Películas
2.1 Casos de uso posibles y modelo conceptual
3 Implementación del modelo en Cassandra
3.3.1 Conocer los estrenos de las películas
3.3.2 Ver datos de una película
4 Aplicación para consultar estrenos
En el modelo relacional... | se conoce en un modelo NoSQL columnar como... |
base de datos/instancia | keyspace |
tabla | column family |
id | uuid (universally unique id): es válido para todas las particiones |
primary key | partition key - primary key: es la clave que se utiliza para particionar una entidad en los distintos clusters |
Queremos modelar los estrenos de películas de un portal de contenidos por streaming. Las entidades que surgen como candidatas son
Lo primero que tenemos que hacer es olvidarnos de la representación “tradicional” del modelo relacional:
Imaginemos algunos casos de uso:
En base a esto ya podemos empezar a diseñar un modelo conceptual:
Sharding peliculas
Tendremos una relación bidireccional
Cada entidad se modela como un mapa <RowKey, RowValue>
Recordamos qué características tienen los mapas[2]:
Si tenemos una gran cantidad de películas y queremos hacer la búsqueda por título, tenemos que repensar nuestro modelo para facilitar ese requerimiento.
Para crear las entidades vamos a utilizar CQL (Cassandra Query Language), un lenguaje que permite subir el nivel de abstracción de las operaciones básicas sobre el motor. Por ejemplo, al definir la entidad películas sabemos que nuestra clave de particionamiento será la fecha de estreno. Como tenemos varias películas que comparten esa fecha, la forma de identificar unívocamente cada película será utilizar un id universal o uuid. Luego vienen los datos estáticos de la película: el título y la sinopsis. Por último, tenemos que diseñar la relación one to many entre película y actores. Hay dos formas de lograr esto:
¿Qué información queremos mostrar de cada actor que participa en una película? Esta es la pregunta crucial que determina qué opción elegimos.
Algunas opciones posibles
Elegimos tener un mapa de actores, con el aval de nuestro usuario. El script en CQL que armamos es:
CREATE TABLE IF NOT EXISTS "peliculas" (
fecha_estreno timestamp,
id uuid,
titulo text,
sinopsis text,
actores map<uuid, text>,
primary key (fecha_estreno, id)
);
¿Cómo se traduce en el modelo?
Películas
Fíjense que tenemos un conjunto de mapas:
Queremos que los actores conozcan las películas en las que trabajaron. Pero tener una lista de identificadores de las películas no es conveniente:
Entonces la estrategia que vamos a adoptar es tener dos column families por separado:
CREATE TABLE IF NOT EXISTS "actores" (
id uuid primary key,
nombre text
);
CREATE TABLE IF NOT EXISTS "actores_peliculas" (
actor_id uuid,
pelicula_id uuid,
fecha_estreno timestamp,
titulo text,
primary key (actor_id, pelicula_id)
);
Vemos el modelo traducido:
Actores
Actores_Películas
Resolvemos los requerimientos en CQL[3]
SELECT fecha_estreno, id, sinopsis, titulo, actores
FROM peliculas
WHERE fecha_estreno = '2014-02-01';
fecha_estreno | id | titulo | actores |
2014-02-01 00:00:00-0300 | 08cbbce4-4341-4f26-888f-5a842af819f8 | Closed circuit | {272505d8-0e33-4837-90a9-cd79ad79396f: 'Rebecca Hall', 77f92efc-f48d-4f01-80a2-25c12ec9915c: 'Eric Bana'} |
2014-02-01 00:00:00-0300 | 0a1ec3aa-9333-458f-b859-4cf149c71bc8 | Enders Game | {4239a7c1-4427-4321-b588-de50590994b2: 'Asa Butterfield', c51be3f9-4963-4675-91f2-f314d0287e17: 'Harrison Ford', d596f571-51ff-4282-8594-180a2bfdf2b4: 'Ben Kingsley', eee25de1-2939-4ca7-9598-eb30b3077b97: 'Hailee Steinfeld'} |
...
La sintaxis de CQL se parece mucho a la del SQL, sin embargo, veremos que una consulta inocente como buscar una película por título nos vuelve a poner en contexto sobre la forma de almacenar que utiliza el motor Cassandra:
SELECT fecha_estreno, id, titulo, actores
FROM peliculas
WHERE titulo = 'Gravity';
No podemos hacer una consulta por cualquier columna si esa columna
sin que explícitamente le digamos al motor que estamos dispuestos a pagar el costo de la performance de dicho query. Esto lo hacemos agregando al final de la consulta el parámetro ALLOW FILTERING:
SELECT fecha_estreno, id, titulo, actores
FROM peliculas
WHERE titulo = 'Gravity'
ALLOW FILTERING;
Tampoco podemos hacer una consulta que involucre más de una tabla (no están permitidos los JOINs[4]).
Supongamos que conocemos el id de una película, al ejecutar esta consulta en CQL
SELECT fecha_estreno, id, titulo, actores
FROM peliculas
WHERE id = 7011f15d-37a4-4df6-8cba-9c7095a08e49;
Nuevamente vemos un cartel de advertencia del motor:
Unable to execute CQL script on 'local': 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.
Entonces incorporamos la cláusula ALLOW FILTERING en la consulta:
SELECT fecha_estreno, id, titulo, actores
FROM peliculas
WHERE id = 7011f15d-37a4-4df6-8cba-9c7095a08e49;
ALLOW FILTERING;
El problema de esta consulta es que requiere hacer una búsqueda en cada uno de los clusters, por eso es muy importante que el negocio asuma que yo conozco la fecha de estreno de una película a la que quiero acceder. De otra forma tendré que cambiar mi modelo de datos o asumir una notable degradación de la performance[5].
Si queremos conocer el perfil de un actor, tenemos que acceder por la clave del mapa que es su identificador:
SELECT id, nombre
FROM actores
WHERE id = 805a8a59-2e34-438c-8949-8a515a673809;
id | nombre |
805a8a59-2e34-438c-8949-8a515a673809 | John Cusack |
Y para conocer las películas de ese actor, necesitamos otro acceso a la column family por id de actor:
/* Nuevo query, vemos las películas de John Cusack */
SELECT *
FROM actores_peliculas
WHERE actor_id = 805a8a59-2e34-438c-8949-8a515a673809;
id | pelicula_id | fecha_estreno | titulo |
805a8a59-2e34-438c-... | 7011f15d-37a4-4df6-... | 2014-02-01 | The frozen ground |
805a8a59-2e34-438c-... | f4d4449e-d0df-4fb3-... | 2014-02-01 | The Butler |
...
Fíjense que en esta column family desnormalizamos los datos más importantes de un film:
Recordemos la clave primaria de la CF actores_peliculas:
CREATE TABLE IF NOT EXISTS "actores_peliculas" (
...
PRIMARY KEY (actor_id, pelicula_id)
Y la definición de la CF peliculas:
CREATE TABLE IF NOT EXISTS "peliculas" (
...
actores map<uuid, text>,
PRIMARY KEY (fecha_estreno, id)
);
Esta redundancia nos permite atacar satisfactoriamente dos requerimientos:
Los índices en esta tecnología tienen que tener una alta cardinalidad, que tenga una buena cantidad de valores. Debemos tener en cuenta estas restricciones a la hora de modelar las entidades.
Pensemos una arquitectura sencilla para una aplicación que permita consultar los estrenos de películas por fecha[6]:
El Repo utiliza el driver[7] Astyanax[8]:
Tenemos que armar la consulta con el Date que recibimos, esto se puede hacer
val statement = QueryBuilder
.select()
.all()
.from("imdb", "peliculas")
.where(QueryBuilder.eq("fecha_estreno", fechaEstrenoPosta))
.limit(10)
.enableTracing()
val rs = session.execute(statement)
Luego, tenemos que transformar (mapear) la información que recibimos al modelo de objetos. Por eso
val peliculas = rs.all.map [ fila |
new Pelicula => [
id = fila.getUUID("id")
titulo = fila.getString("titulo")
sinopsis = fila.getString("sinopsis")
// Transformo el mapa de actores en una List<Actor>
val mapaActores = fila.getMap("actores", UUID, String)
actores = mapaActores.entrySet.map [ entryActor |
new Actor(entryActor.key, entryActor.value)
].toList
]
][9]
En negrita y resaltado mostramos las asignaciones a cada atributo de película.
de
[1] Incluiremos en esta categoría a las series, pero por fines didácticos sólo lo aclaramos aquí
[2] Mapa de Java o su equivalente Dictionary de Smalltalk, hablamos de un conjunto de pares clave/valor
[3] Utilizamos la herramienta Datastax DevCenter 1.6.0, que viene con la suite de instalación de Cassandra
[4] Para profundizar más puede verse la documentacion sobre Data Manipulation Language de Cassandra
[5] Por eso es importante entender la tecnología, para no echarle la culpa a ella de nuestras malas decisiones
[6] El link al ejemplo se encuentra en https://github.com/uqbar-project/eg-peliculas-cassandra
[7] Un driver, también llamado controlador, es el medio de comunicación entre el motor de la base de datos y nuestra aplicación hecha en Xtend (Java)
[8] https://github.com/Netflix/astyanax, construido por el equipo de desarrollo Netflix y basado en otro driver existente llamado Héctor
[9] En CQL la forma de acceder a la column family varía muy ligeramente, para más información ver el branch astyanax2 de https://github.com/uqbar-project/eg-peliculas-cassandra