aprendiendo ( Erlang ).

lunes, 2 de enero de 2012

Tablas ETS y DETS

| 6 comentarios |

ETS (Erlang Term storage) y DETS (Disk ETS) son sistemas de almacenamiento masivo, y altamente eficiente de términos Erlang. La estructura de almacenamiento es una colección de tuplas Erlang que, asocian a una clave un valor, es decir, se trata de una tabla típica de clave-valor, donde la tabla ETS se almacena en memoria, mientras que DETS lo hace en disco.
Ambas librerías realizan exactamente las mismas tareas permiten operaciones de inserción y búsqueda de forma eficiente.
La estructura de datos ETS es transitoria ya que cuando se desee puede ser borrada. La estructura de datos DETS es persistente e incluso sobrevive a una caída del sistema. Cuando se abre una tabla DETS, se le comprueba la consistencia. Si se encuentra que esta corrupta se repara.
Una tabla ETS podría utilizarse para una aplicación que tiene que usar una cantidad enorme de datos de forma eficiente.

Operaciones básicas

Ya que sabemos que es, y para que sirve, veamos ahora la funcionalidad básica. Existen cuatro operaciones básicas en las tabla ETS y DETS:
  1. Crear o abrir una tabla existente: Esto se consigue con las funciones ets:new o dets:open_file.
  2. Insertar una tupla o varias en una tabla: Para insertar utilizaremos la función insert(Tabla, X), donde X es un tupla o lista de ellas. Funciona igual para ETS como para DETS.
  3. Buscar un tupla: Llamaremos a lookup(Tabla, Clave). El resultado de esta función es una lista de tuplas que coinciden con la clave pasada. Si la función no encuentra coincidencia en la tabla retorna la lista vacía. Esta definida en ETS y DETS.
  4. Liberar la tabla: Cuando queremos dar por concluido el uso de la tabla podemos llamar a ets:delete(Tabla) o a dets:close(Tabla)

Tipos de tablas

Como ya he comentado las tablas ETS y DETS almacenan tuplas. Estas tuplas tienen que cumplir el siguiente requisito y es que uno de los elementos de la tupla es la clave de la tabla. Y..., ahora, te preguntará ¿cómo inserta y recupera los datos en la tabla? Pues, depende del tipo de tabla que se utilice. Existen cuatro variantes:
  1. Conjunto (Set): no permite duplicidades de claves, es decir, requiere de claves únicas.
  2. Conjunto ordenado(Ordered set): igual que el conjunto pero sus elementos están ordenados.
  3. Bolsa (Bag): permite duplicidades de claves, es decir, que una clave puede contemplar más de una tupla.
  4. Bolsa duplicada(Duplicate bag): es una bolsa que permite tuplas duplicadas.
La elección del tipo correcto, como siempre, depende de las necesidades de nuestra aplicación. Si necesitados duplicados o no, si deseamos ordenaciones, etc...

Creando/destruyendo una tabla ETS

Las tablas ETS se crean con la función ets:new. El proceso que crea la tabla es el dueño de la tabla. Si el proceso padre muere o termina, el espacio ocupado por la tabla es liberado. También podemos liberar el espacio de forma manual con la función ets:delete.
La función ets:new(Nombre, [Opciones])-> IdTabla recibe un nombre y un conjunto de opciones que son inmutables a lo largo de la vida de la tabla. Las opciones pueden ser:
  • [set | ordered_set | bag | duplicate_bag]: el tipo de tabla.
  • Otras opciones: determina el ámbito de acceso al contenido de la tabla. El tipo de acceso puede ser:
    • private: Crea una tabla ETS de ámbito privado. Es decir, sólo el proceso dueño puede leer y escribir en la tabla
    • public: Crea una tabla pública. Todo aquel proceso que conozca el identificador de tabla puede acceder a los datos.
    • protected: Crea una tabla tipo protegida. Cualquier proceso que conozca el identificador de tabla puede leer de ella, pero sólo el proceso dueño puede escribir.
Sino indicamos un valor para las opciones de la tabla los valores por defecto son [set, protected, {keypos,1}].


Principales funciones ETS

Veamos algunas de las principales funciones del módulo ets.
  • new(Nombre, Opciones) -> TablaId | Nombre: crea una instancia de tabla ETS
  • info(Tabla) -> [{Item, Valor}] | undefined: retorna una lista con la información como la memoria que ocupa, si esta comprimido o no, keypos, etc...
  • all() -> [Tabla]: retorna una lista con todas las tablas del nodo.
  • delete(Tabla) -> true: borra una tabla dada.
  • delete(Tabla, Clave) -> true: borra todas las coincidencias de la clave en la tabla.
  • file2tab(NombreFichero) -> {ok, Tab} | {error, Motivo}: carga una tabla grabada previamente en un fichero.
  • tab2file(Tabla,NombreFichero) -> ok | {error, Motivo}: realiza un volcado de los datos de una tabla a un fichero.
  • from_dets(Tabla, TablaDets) -> true: copia el contenido de una tabla DETS en una tabla ETS.
  • to_dets(Tabla, TablaDets) -> TablaDets: copia el contenido de la tabla ETS en otra DETS.
  • member(Tabla, Clave) -> true | false: busca la clave en la tabla ETS y devuelve true si la encuentra y false en otro caso.
  • lookup(Tabla, Clave) -> [Objeto]: retorna una lista con todos los objetos que contiene la clave en la tabla.
  • insert(Tabla, Objeto_O_Objetos) -> true: Inserta un objeto o una lista de objetos en la tabla. El modo de inserción dependerá del tipo de tabla elegida. La operación es atómica y aislada, incluso para una lista de objetos.
  • first(Tabla) -> Clave | '$end_of_table': Retorna la clave del primer elemento de la tabla. Si la tabla está vacía retorna un entidad especial '$end_of_table'
  • next(Tabla, Clave1) -> Clave2 | '$end_of_table': Retorna la clave del siguiente elemento a una clave dada.
  • prev(Tabla, Clave1) -> Clave2 | '$end_of_table': Retorna la clave del elemento previo a una clave dada.
  • last(Tabla) -> Key | '$end_of_table': Retorna la clave del último elemento de la tabla.
  • foldl(Funcion, Acc0, Tabla) -> Acc1: Realiza un foldl sobre la tabla.
  • foldr(Funcion, Acc0, Tabla) -> Acc1: Realiza un foldr sobre la tabla.

Principales funciones DETS

Las funciones de ETS y DETS son prácticamente las misma. Casi todas las funciones vistas para las tablas ETS están implementadas en el módulo dets por lo que sólo veremos las principales diferencias:
  • is_dets_file(NombreFichero) -> Bool | {error, Motivo}: Retorna true is el fichero es una tabla DETS y false si no lo es.
  • open_file(NombreFichero) -> {ok, Referencia} | {error, Motivo}: Abre una tabla existente. Si la tabla no se cerro adecuadamente el sistema se encarga de reparar la tabla.
  • open_file(Nombre, Opciones) -> {ok, Nombre} | {error, Motivo}: Abre o crea una tabla DETS si esta no existe. El Nombre es el nombre que debemos proporcionar a todas las operaciones. Si dos procesos abren la misma tabla con mismo nombre y argumentos, ambos procesos compartirán la tabla. Los argumentos es una lista de tuplas del tipo clave-valor. Veamos algunas tuplas útiles:
    • {access, tipo_acceso}: por defecto es read_write (lectura/escritura) pero también podemos establecer el modo de sólo lectura (read).
    • {auto_save, intervalo}: establece el intervalo de auto-salvado. El valor por defecto es 180000 milisegundos (3 minutos). También podemos establecer el valor a infinity, para que no se realice auto-salvado.
    • {file, NombreFichero}: establecemos el fichero donde deseamos persistir los datos. Si no establecemos el nombre de fichero, el valor por defecto es el nombre de la tabla.
    • {ram_file, Bool}: por defecto es false pero puede interesar tener la tabla en RAM para mejorar el rendimiento. Es muy útil para procesos pequeños (o de tiempo de ejecución corto) que abren, operan y cierran.
    • {repair, valor}: indicamos al sistema que ejecute automáticamente la reparación de tabla. El valor por defecto es true.
    • {type, TipoTabla}: indica el tipo de tabla DETS. Por defecto es set.
  • close(Nombre) -> ok | {error, Motivo}: cierra una tabla. Sólo el proceso que abrió la tabla puede cerrarla.
  • from_ets(Nombre, TablaETS) -> ok | {error, Motivo}: crea una tabla DETS a partir de una tabla ETS, borrando previamente todos objetos de la tabla.
  • to_ets(Nombre, TablaETS) -> TablaETS | {error, Motivo} inserta todos los elementos de la tabla DETS en la tabla ETS.
  • sync(Nombre) -> ok | {error, Motivo}: asegura quien los datos que estén en memoria se graben en disco.

Ejemplo de ETS

Bueno, este es el momento que todos estábamos esperando. Un ejemplo de uso de tabla ETS. Que ya esta bien de tanta teoría ... vamos a la práctica.
Para ilustrar el uso de tabla ETS he implementado un servidor genérico (gen_server) que ya explicamos en un artículo anterior. El ejemplo es una sencilla agenda de citas. La tabla ETS, será en nuestro caso una tupla con una fecha (clave) y una descripción, y actuará de estado del servidor. El interfaz implementado en el servidor es:
  • start/0: arranca el servidor, la agenda.
  • stop/0: para el servidor.
  • nueva_cita/2: crea una nueva cita en la agenda.
  • borrar_cita/1: borra una entrada de la agenda.
  • ver_cita/1: muestra las citas de un fecha dada.
  • listar_agenda/0: lista la agenda

-module(agenda).

-behaviour(gen_server).

%% API
-export([ start/0, stop/0,
         nueva_cita/2, borrar_cita/1, listar_agenda/0, 
         ver_cita/1]).

%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
         terminate/2, code_change/3]).

-define(SERVER, ?MODULE).


start() ->
    gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).

stop() ->
    gen_server:call(?MODULE, {parar}).

nueva_cita(Fecha, Descripcion) ->
    gen_server:call(?MODULE, {nueva, Fecha, Descripcion}).

borrar_cita(Fecha) ->
    gen_server:call(?MODULE, {borrar, Fecha}).

listar_agenda() ->
    gen_server:call(?MODULE, {listar}).

ver_cita(Fecha) ->
    gen_server:call(?MODULE, {ver, Fecha}).

%%====================================================================
%% gen_server callbacks
%%====================================================================
init([]) ->
    {ok, ets:new(?MODULE, [bag])}.

handle_call({nueva, Fecha, Descripcion}, _From, _State) ->
    io:format("Nueva cita: ~p~n", [ets:insert(_State, {Fecha, Descripcion})]),
    {reply, ok, _State};
handle_call({borrar, Fecha}, _From, _State) ->
    io:format("Borrar cita ~p ~p ~n", [Fecha, ets:delete(_State, Fecha)]),
    {reply, ok, _State};
handle_call({ver, Fecha}, _From, _State) ->
    io:format("Ver cita ~p ~p ~n", [Fecha, ets:lookup(_State, Fecha)]),
    {reply, ok, _State};
handle_call({listar}, _From, _State) ->
    io:format("Listado de Agenda: ~n", []),
    listar_ets(_State),
    {reply, ok, _State};
handle_call({parar}, _From, _State) ->
    io:format("Parando agenda~n", []),
    {stop, shutdown, _State};
handle_call(_, _From, _State) ->
    io:format("~p~n", [no_implementado]),
    {reply, ok, _State}.

handle_cast(_, _State) ->
    io:format("~p~n", [no_implementado]),
    {noreply, _State}.

handle_info(_Info, State) ->
    {noreply, State}.

terminate(_Reason, _State) ->
    ets:delete(_State),
    ok.

code_change(_OldVsn, State, _Extra) ->
    {ok, State}.

%%====================================================================
%% funciones auxiliares
%%====================================================================
listar_ets(Tabla) ->
    listar_ets(Tabla, ets:first(Tabla)).

listar_ets(_, '$end_of_table') ->
    ok;
listar_ets(Tabla, Clave) ->
    io:format("** ~p: ~p~n", [Clave, ets:lookup(Tabla,Clave)]),
    listar_ets(Tabla, ets:next(Tabla,Clave)).

2> agenda:start(). {ok,<0.138.0>} 3> agenda:nueva_cita("01-01-2011", "cita 1"). Nueva cita: true ok 4> agenda:nueva_cita("02-01-2011", "cita 2"). Nueva cita: true ok 5> agenda:nueva_cita("03-01-2011", "cita 3"). Nueva cita: true ok 6> agenda:nueva_cita("03-01-2011", "cita 4"). Nueva cita: true ok 7> agenda:nueva_cita("01-01-2011", "cita 5"). Nueva cita: true ok 8> agenda:listar_agenda(). Listado de Agenda: ** "01-01-2011": [{"01-01-2011","cita 1"},{"01-01-2011","cita 5"}] ** "03-01-2011": [{"03-01-2011","cita 3"},{"03-01-2011","cita 4"}] ** "02-01-2011": [{"02-01-2011","cita 2"}] ok 9> agenda:borrar_cita("01-01-2011"). Borrar cita "01-01-2011" true ok 10> agenda:listar_agenda(). Listado de Agenda: ** "03-01-2011": [{"03-01-2011","cita 3"},{"03-01-2011","cita 4"}] ** "02-01-2011": [{"02-01-2011","cita 2"}] ok 11> agenda:stop().

El tipo de tabla utilizada en la implementación es bag que nos permite tener más de una cita por día. En este ejemplo podemos ver fácilmente un uso básico de este tipo de estructuras. Creación, inserción, borrado, búsqueda y listado de los elementos de la tabla.
Alguien podría pensar que más ilustrativo quedaría implementar la agenda sin necesidad de que fuera un servidor genérico. Pues, tiene razón. Pero la idea era proporcionar un ejemplo más práctico de servidores.


Publicar un comentario en la entrada

6 comentarios:

  1. tachaaaan dijo...

    Genial me a gustado la explicación y el ejemplo.

    Muchas gracias muy completo

  2. Verdi dijo...

    Gracias tachaaan. Es un placer

  3. Cosas que ver ..... dijo...

    Bueno, me gustaría entender porque pones el "_State" en vez de "State". Entiendo que tendrá que ver con que el State es la ETS ?.
    Muchas gracias.

  4. Verdi dijo...

    Si que es verdad. Tienes toda la razón se refiere al State. Es simplemente un criterio personal. Aveces me gusta definir los parámetros de entrada como _XXX, cuando creo que voy a utiliza una variable XXX en algún momento dentro de la función. Supongo que son manías que he tomado de otros lenguajes. ;P

    verdi!gracias.

  5. Cosas que ver ..... dijo...

    .....bueno, no sé si reirme o llorar. Qué dificil es hacerse entender, pero me pregunto:
    - Si no la voy a utilizar por qué poner la init {ok, ets:new(?MODULE, [bag])}. y no un {ok, [] } o un {ok, #state{}} y ahorrame "33" sustituciones de State por _State ?.

    - En el ejemplo con gen_server de http://learnyousomeerlang.com/ets utilizan el State definido como allí como un Tid ? Y no ponen _Tid ?.

    - Existe alguna razón relacionada con la actualización de los campos de los registros y que la tabla por defecto es protected ?.

    - Perdón por mi ignorancia espero que vaya a menos con el tiempo, por ahora solo va a más.

  6. Verdi dijo...

    Hola Cosas que ver ...

    - Si no la voy a utilizar por qué poner la init {ok, ets:new(?MODULE, [bag])}. y no un {ok, [] } o un {ok, #state{}} y ahorrame "33" sustituciones de State por _State ?.

    Es importante, devolver el {ok, ets:new(?MODULE, [bag])} en el init, ya que el servidor utilizará la tabla ETS. Si quieres puedes poner State si te es más cómodo.

    - En el ejemplo con gen_server de http://learnyousomeerlang.com/ets utilizan el State definido como allí como un Tid ? Y no ponen _Tid ?.

    Como ya te comente el guión bajo es manía. Lo realmente importante es utilizar un nombre que sea lo suficientemente identificativo.

    - Existe alguna razón relacionada con la actualización de los campos de los registros y que la tabla por defecto es protected ?.

    Si el nivel de aislamiento es protected (opción por defecto) sólo el dueño de la tabla (el que la creo) puede modificar los valores. Es una medida de seguridad, algo así como, "mi tabla sólo la puedo modificar yo". Evitando así que otros procesos modifiquen el contenido.

    - Perdón por mi ignorancia espero que vaya a menos con el tiempo, por ahora solo va a más.

    No hay problema.

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