aprendiendo ( Erlang ).

jueves, 16 de febrero de 2012

Mnesia I. Introducción.

| 0 comentarios |

Mnesia es el sistema de bases de datos nativo de Erlang. Se trata de un sistema de base de datos distribuida, ideal para sistemas que necesitan un funcionamiento continuo (24*7) con características de tiempo real, como los operadores de telefonía o telecomunicaciones.

Te habrás llamado la atención el nombre tan curioso para una base de datos. Pues como curiosidad, destacar que el nombre original del sistema era Amnesia, pero los señores de Erlang concluyeron que no era un nombre apropiado para una base de datos y mucho menos para una base de datos que presenta las siguientes características:

  • El esquema de base de datos puede se reconfigurado de forma dinámica en tiempo de ejecución.
  • Las tablas pueden declararse teniendo propiedades tales como localización, replicación y persistencia.
  • Las tablas pueden ser movidas y/o replicadas entre nodos en previsión de fallos y buscando mejorar la tolerancia a fallos.
  • La localización de las tablas debe ser transparente al programador, es decir, que los programas direccionan las tablas mediante el nombre de la tabla y es Erlang quien se encarga de saber localizar la tabla en el sistema anfitrión.
  • Las transacciones pueden ser distribuidas.
  • Varias transacciones pueden ejecutarse concurrentemente y su ejecución es completamente sincronizada. Vamos que Mnesia se encarga de no permitir que dos procesos manipulen los mismos datos a la vez.
  • Mnesia nos asegura que una transacción es ejecutada en todos los nodos del sistema o en ninguno. Es decir, transacciones atómicas.
  • Búsquedas en tiempo real a nivel de clave-valor.
  • Permite objetos complejos.

Como puedes observar he ido resaltando las ideas principales del sistema. Ideas como: distribuida, atómica, configuración en caliente, replicación y tolerancia a fallos. Es aquí donde reside realmente la fuerza y potencia que esta base de datos nos proporciona.

Imagínate un sistema de mensajería instantánea a nivel mundial, que requiera de un sistema de base de datos funcionando 24*7. Un sistema de bases de datos con características de búsquedas en miles de millones de datos. Un sistema que sea tolerante a fallos y que en el caso de caída nos permita seguir dando servicio. Un sistema que nos permita cientos de miles de ejecuciones concurrentes sin problemas. Un sistema que proporcione una respuesta rápida a nuestros usuarios. Si señores… estoy hablando de Mnesia y no de otra. Ya existen muchos ejemplos de éxito en nuestro día a día. Y no me refiero únicamente a los sistemas de telefonía, que existen muchísimos. Por citar algunos ejemplos tenemos Facebook, o Whatsapp, o Ejabberd, o CouchDb, o …

No nos equivoquemos. Como siempre decimos, no todos es oro lo que reluce y este caso no es menos. Tenemos que saber cuando es apropiado utilizar este sistema de base de datos. Y según sus creadores, es conveniente utilizarla cuando:

  • Requiramos replicación de datos.
  • Necesitemos realiza consultas complejas en un tiempo decente.
  • Atomicidad en las transacciones.
  • Aplicaciones con características de Soft real-time.

Si nuestra aplicación requiere de todas o algunas de estas necesidades entonces puede ser conveniente utilizar Mnesia.

Creando tablas y arrancando el sistema

Empecemos como siempre con un ejemplo simple que podamos moldear de forma sencilla y que iremos complicando sucesivamente en posteriores apartados.

En este primer ejemplo, vamos a modelar un problema en el que vamos a tener un conjunto de almacenes con artículos. Los artículos podrá estar ubicado en un almacén y habrá una cierta cantidad de ellos.

Diagrama de entidades

Lo primero que debemos codificar son los registros en un fichero que originalmente he llamado almacen.hrl que incluiremos en el fichero almacen.erl.

-record (articulo, {id, nombre}).

-record (almacen, {id, nombre, direccion}).

-record (ubicacion, {articulo_id, almacen_id, cantidad}).

Como puedes observar hemos creado los registros que formarán la estructura de nuestra base de datos. Ahora ya estamos en disposición de crear nuestras tablas con el comando mnesia:create_table(Name, ArgList).

-include("almacen.hrl").

crear_tablas() ->
    mnesia:create_table(articulo,
                        [{attributes, record_info(fields, articulo)}]),
    mnesia:create_table(almacen,
                        [{attributes, record_info(fields, almacen)}]),
    mnesia:create_table(ubicacion,
                        [{type, bag}, 
                         {attributes, record_info(fields, ubicacion)}]).

Lo primero que salta a la vista es que el primer elemento de la función es el nombre de la tabla y que, como nos gusta ser predecibles, tiene el mismo nombre que el registro, aunque no es necesario. Lo segundo, es la forma de asociar los attributes o campos de nuestra tabla, que realizado con la macro record_info(fields, Registro) nos proporcionará una lista con los campos de nuestro registro sin necesidad de editarla otra vez. Y en último orden de cosas, y no por ello, menos importante, la definición de la tabla ubiciacion indica que es de tipo bag (ver ETS-DETS) y eso es debido a que se trata de una tabla que mapea un comportamiento mucho a mucho.

Nuestra base de datos va a trabajar en memoria y en un único nodo Erlang, por lo que no hace falta configurar más para este nuestro primer ejemplo de uso de Mnesia.

Muy, muy importante es no olvidar incluir el fichero de cabeceras almacen.hrl. Para que la definición de nuestros registros estén disponibles en nuestra aplicación.

Ahora, sin más abrimos nuestra consola Erlang, arrancamos el servidor de base de datos y comprobamos que nuestra función crea las tablas.

1> mnesia:start().
ok
2> c(almacen).
{ok,almacen}
3> almacen:crear_tablas().
{atomic,ok}
4> mnesia:info().
---> Processes holding locks <--- 
---> Processes waiting for locks <--- 
---> Participant transactions <--- 
---> Coordinator transactions <---
---> Uncertain transactions <--- 
---> Active tables <--- 
ubicacion      : with 0        records occupying 300      words of mem
almacen        : with 0        records occupying 300      words of mem
articulo       : with 0        records occupying 300      words of mem
schema         : with 4        records occupying 730      words of mem
===> System info in version "4.4.17", debug level = none <===
opt_disc. Directory "/home/verdi/Documentos/aprendiendo-erlang/mnesia-I/Mnesia.nonode@nohost" is NOT used.
use fallback at restart = false
running db nodes   = [nonode@nohost]
stopped db nodes   = [] 
master node tables = []
remote             = []
ram_copies         = [almacen,articulo,schema,ubicacion]
disc_copies        = []
disc_only_copies   = []
[{nonode@nohost,ram_copies}] = [schema,articulo,almacen,ubicacion]
5 transactions committed, 0 aborted, 0 restarted, 0 logged to disc
0 held locks, 0 in queue; 0 local transactions, 0 remote
0 transactions waits for other nodes: []
ok

Con el comando mnesia:info() comprobamos como efectivamente se han creado nuestras tablas, y que estas han sido creadas como ramcopies, es decir, que son tablas en RAM y no se grabarán en disco. También podemos ver los nodos de ejecución en nuestro caso se trata de una base de datos local pero como sabemos Mnesia es una base de datos distribuida y como tal, podría crear copias de nuestras tablas en otros nodos Erlang.

Insertando datos

Ahora, ha llegado el momento de poblar con datos nuestras tablas. Y Para ello, vamos a hacer uso de la función mnesia:write/1. Con esta función vamos a poder insertar las tuplas en nuestra base de datos.

insertar_articulo(Id, Nombre) ->
    Fun = fun() ->
                  mnesia:write(#articulo{id=Id, nombre=Nombre})
          end,
    mnesia:transaction(Fun).

insertar_almacen(Id, Nombre, Direccion) ->
    Fun = fun() ->
                  mnesia:write(#almacen{id=Id, nombre=Nombre, direccion=Direccion})
          end,
    mnesia:transaction(Fun).

insertar_ubicacion(Articulo_id, Almacen_id, Cantidad) ->
    Fun = fun() ->
                  mnesia:write(#ubicacion{articulo_id=Articulo_id, almacen_id=Almacen_id, cantidad=Cantidad})
          end,
    mnesia:transaction(Fun).

Como habrás intuido todas las operaciones son transacciones, asegurando así la atomicidad de la operación. La función que se encargada de las transacciones es mnesia:transaction/1, la cual recibe una Fun que englobe la operación.

5> almacen:insertar_articulo(1, "Articulo 1").
{atomic,ok}
6> almacen:insertar_articulo(2, "Articulo 2").
{atomic,ok}
7> almacen:insertar_almacen(1, "Almacen 1", "direc 1").                    
{atomic,ok}
8> almacen:insertar_almacen(2, "Almacen 2", "direc 2").
{atomic,ok}
9> almacen:insertar_ubicacion(1, 1, 4).
{atomic,ok}                                                                                                                                                                                  
10> almacen:insertar_ubicacion(1, 2, 3).                                                                                                                                                      
{atomic,ok}                                                                                                                                                                                  
11> almacen:insertar_ubicacion(2, 2, 10).                                                                                                                                                    
{atomic,ok}
12> mnesia:info().
---> Processes holding locks <--- 
---> Processes waiting for locks <--- 
---> Participant transactions <--- 
---> Coordinator transactions <---
---> Uncertain transactions <--- 
---> Active tables <--- 
ubicacion      : with 3        records occupying 327      words of mem
almacen        : with 1        records occupying 341      words of mem
articulo       : with 2        records occupying 356      words of mem
schema         : with 4        records occupying 730      words of mem
===> System info in version "4.4.17", debug level = none <===
opt_disc. Directory "/home/verdi/Documentos/aprendiendo-erlang/mnesia-I/Mnesia.nonode@nohost" is NOT used.
use fallback at restart = false
running db nodes   = [nonode@nohost]
stopped db nodes   = [] 
master node tables = []
remote             = []
ram_copies         = [almacen,articulo,schema,ubicacion]
disc_copies        = []
disc_only_copies   = []
[{nonode@nohost,ram_copies}] = [schema,articulo,almacen,ubicacion]
12 transactions committed, 0 aborted, 0 restarted, 0 logged to disc
0 held locks, 0 in queue; 0 local transactions, 0 remote
0 transactions waits for other nodes: []
ok 

En el info podemos comprobar las transacciones que se han realizado con éxito, las abortadas, etc. Y por ahora, nos permite comprobar que efectivamente se han realizado nuestras inserciones correctamente.

Consultado registros.

Para empezar vamos hablar del concepto de Oid. Se trata del identificador de objeto dentro Mnesia. Este Oid es una tupla con dos elementos {nombre, id}, uno el nombre de la tabla y el identificador. De esta forma identificamos un registro dentro de nuestra base de datos y se corresponde con los dos primeros elementos de nuestra definición de registro. Por ejemplo, en la tabla de artículos nuestro Oid corresponde con la tupla {articulo, Id}.

Sin más, hemos implementado tres funciones que nos permite leer registros de nuestra tablas.

articulo(Articulo_id) ->
    Oid = {articulo, Articulo_id},
    Fun = fun() ->
                  mnesia:read(Oid)
          end,
    mnesia:transaction(Fun).

almacen(Almacen_id) ->
    Oid = {almacen, Almacen_id},
    Fun = fun() ->
                  mnesia:read(Oid)
          end,
    mnesia:transaction(Fun).

ubicacion(Articulo_id) ->
    Oid = {ubicacion, Articulo_id},
    Fun = fun () ->
                  mnesia:read(Oid)
          end,
    mnesia:transaction(Fun).

Para implementarlas hacemos uso de la función mnesia:read/1, que recibe un Oid y nos devuelve, siempre que todo haya ido bien, una tupla con el formato siguiente {atomic, Lista_registros}.

13> almacen:articulo(1).
{atomic,[{articulo,1,"Articulo 1"}]}
14> almacen:almacen(1). 
{atomic,[{almacen,1,"Almacen 1","direc 1"}]}
15> almacen:ubicacion(1).
{atomic,[{ubicacion,1,1,4},{ubicacion,1,2,3}]}

Lo interesante de la ejecución es como funciona la tabla de ubicaciones de artículos. Esta tabla, la definimos como de tipo bag, por lo que, para un mismo Oid obtenemos una lista de ubicaciones del artículo en cuestión. En nuestro ejemplo hemos solicitado las ubicaciones del artículo con id 1 y la consulta nos ha devuelto que esta en el almacén con id 1 y 2 y que existen 4 y 3 elementos respectivamente.

Consultas (QLC)

El QLC es un lenguaje de consultas que utiliza las ya conocidas listas por comprensión. No, no hay que asustarse por ello, son tan fáciles de usar como el propio SQL y en cuanto veas las consultas …

-include_lib("stdlib/include/qlc.hrl").

articulos() ->
    Fun = fun() ->
                  Q = qlc:q([Art || Art <- mnesia:table(articulo)]),
                  qlc:e(Q)
          end,
    mnesia:transaction(Fun).

almacenes() ->
    Fun = fun() ->
                  Q = qlc:q([Alm || Alm <- mnesia:table(almacen)]),
                  qlc:e(Q)
          end,
    mnesia:transaction(Fun).

A que no necesita explicación. Nuestro generador de elementos de la lista por compresión es la propia tabla. Me encanta. Por cierto, no olvides incluir en la cabecera la librería del QLC (-include_lib("stdlib/include/qlc.hrl").).

16> almacen:articulos().
{atomic,[{articulo,1,"Articulo 1"},
         {articulo,2,"Articulo 2"}]}
17> almacen:almacenes().
{atomic,[{almacen,1,"Almacen 1","direc 1"},
         {almacen,2,"Almacen 2","direc 2"}]}

Ahora vamos a complicar un poco la cosa. Necesito saber los artículos que estén en un determinado almacén y que nos indique la cantidad de ellos. Para conseguirlo, vamos a tener que cruzar las tablas de artículo y ubicación.

articulos(Almacen_id) ->
    Fun = fun() ->
                  Q = qlc:q([{Art, Ubi#ubicacion.cantidad} || Art <- mnesia:table(articulo), 
                                                              Ubi <- mnesia:table(ubicacion),
                                                              Art#articulo.id =:= Ubi#ubicacion.articulo_id,
                                                              Ubi#ubicacion.almacen_id =:= Almacen_id]),
                  qlc:e(Q)
          end,
    mnesia:transaction(Fun).

Y lo propio para los almacenes, es decir, que vamos a necesitar los almacenes que tiene un determinado producto y por su puesto la cantidad que hay en ellos.

almacenes(Articulo_id) ->
    Fun = fun() ->
                  Q = qlc:q([{Alm, Ubi#ubicacion.cantidad} || Alm <- mnesia:table(almacen), 
                                                              Ubi <- mnesia:table(ubicacion),
                                                              Alm#almacen.id =:= Ubi#ubicacion.almacen_id,
                                                              Ubi#ubicacion.articulo_id =:= Articulo_id]),
                  qlc:e(Q)
          end,
    mnesia:transaction(Fun).

Probemos pues el resultados de nuestras consultas y que realmente nos proporcionan los datos deseados.

18> almacen:articulos(2).
{atomic,[{{articulo,1,"Articulo 1"},3},
         {{articulo,2,"Articulo 2"},10}]}
19> almacen:almacenes(1).
{atomic,[{{almacen,1,"Almacen 1","direc 1"},4},
         {{almacen,2,"Almacen 2","direc 2"},3}]}

Tengo que seguir profundizando en Mnesia pero la primera impresión ha sido muy, muy buena y pienso seguir probando cositas. Me encanta esto … YUUUUUJJUUUU.

Publicar un comentario en la entrada

0 comentarios:

 
Licencia Creative Commons
Aprendiendo Erlang por Verdi se encuentra bajo una Licencia Creative Commons Atribución-NoComercial-CompartirIgual 3.0 Unported.