7
Dec/09
0

Mnesia, un dictionnaire transactionnel

Mnesia est une bête différente d’une base de données relationnelle. Il s’agit d’un dictionnaire clé/valeur transactionnel.

La conséquence de cette phrase est que toutes les opérations doivent être exécutées dans une transaction (même les lectures) et que les meilleures structures de données exploitables sont les enregistrements dénormalisés (pas de contrainte d’intégrité).

De même, il n’existe pas de système de requête type SQL, et la maîtrise des comprehension list est nécessaire afin de sélectionner des éléments.

Gestion des identités

Comme pour toute base de données, c’est une très mauvaise idée d’utiliser des données utilisateur en tant que clé d’identification de l’enregistrement.

Pour l’enregistrement suivant :

-record(user, {name, pass_hash, salt}).

la clé primaire correspond automatiquement au premier champ, donc le « nom » de l’utilisateur. Il est courant dans les différents sites de ne pas pouvoir changer son login une fois identifié (si utilisé comme clé primaire) ; cela permet de « simplifier » quelques requêtes quand à l’unicité du compte, mais je ne considère pas cela comme une bonne pratique.

Le mieux est de générer un identifiant indépendant et sans signification. Cet identifiant sera un UUID afin de rester au plus simple et au plus fiable au niveau implémentation.

Pour cela, on utilise un module de génération disponible sur le net : le notre sera celui de akreiling trouvé sur github (pour info, ce choix s’est fait après une recherche google seulement, sans trop d’analyse supplémentaire pour l’instant !!!! oui, je n’en suis pas très fier).

L’enregistrement devient :

-record(user, {uid, name, pass_hash, salt}).

et la création se fait en ajoutant un uuid :

#user{uid=uuid:srandom(), ...}

Gestion des erreurs

Une transaction mnesia ne semble pas générer d’erreurs. Pour faire planter le programme il est important de déclencher l’exception via une clause incorrecte.

Obligation de créer une API

Les fonctions mnesia sont vraiment très basiques. Il est obligatoire pour tout programme erlang de créer une API d’utilisation un peu plus flexible.

Toutes les applications seront donc plus ou moins obligées de redéfinir les méthodes de lecture, écriture, récupération globale d’éléments d’une table, de transaction, etc.

De plus, une des grosses difficultés de mnesia (c’est subjectif, je vous l’accorde) est de pouvoir écrire le code d’une façon lisible. On passe un temps trop important sur cette partie.

On va donc redéfinir ces opérations basiques comme suit :

write(Record) ->
  mnesia:transaction(fun() ->
    mnesia:write(Record)
  end).
 
tx(F) ->
  {atomic, Result} = mnesia:transaction(F),
  case Result of
    {error, _} -> throw(Result);
    _ -> Result
  end.
 
find(Q) ->
  F = fun() -> qlc:e(Q) end,
  case mnesia:transaction(F) of
    {atomic, Result} -> Result;
    _ -> []
  end.
 
find_all(Table) -> find(qlc:q([X || X <- mnesia:table(Table)])).

Si on ne fait pas ces méthodes, il sera beaucoup plus compliqué et lourd d’utiliser ce dictionnaire.

Ces fonctions ont été induites par l’utilisation de mnesia dans l’application. Un exemple d’utilisation de ces fonctions sera fourni ultérieurement.

Afin de pouvoir compiler le programme, il est important de pouvoir inclure les définitions des QLC. Selon le système, il peut être nécessaire d’introduire ce code en début de fichier :

-include("/usr/lib/erlang/lib/stdlib-1.16.1/include/qlc.hrl").

Le problème est l’utilisation d’un chemin en dur. Il est préférable de créer un lien symbolique au niveau du répertoire source :

cd src
ln -s /usr/lib/erlang/lib/stdlib-1.16.1/include

Puis d’utiliser ce chemin relatif dans le source :

-include("include/qlc.hrl").
Filed under: Erlang, mnesia
24
Nov/09
0

Mise en place de Mnesia dans mochiweb

Introduction

Les données de l’applicatif proviennent de plusieurs sources, et sont stockées différemment selon les besoins.

Par exemple, couchDB ne sera pas utilisé pour stocker les informations d’authentification des utilisateurs. Pour cela, on s’appuiera sur la base de données mnesia fournie avec erlang.

Lancement

La première action à faire est de modifier le fichier Rakefile.rb afin de démarrer le module mnesia au démarrage de l’application :

task :start => [ :build, 'env:start' ] do
  system "erl -pa #{EBIN_DIR} #{ROOT}/deps/*/ebin -boot start_sasl -s reloader -s inets -s app -mnesia dir 'db'"
end

Les fichiers de données seront stockés dans le répertoire indiqué dans la ligne de commande. Le répertoire sera créé au moment de l’initialisation de la base.

Initialisation

Il est nécessaire ensuite de créer la base elle même. On pourrait ajouter le code nécessaire au niveau de la fonction erlang de démarrage applicatif. Cependant, on va créer une tâche et une fonction spécifique à cette initialisation. Au démarrage de l’application, on suppose que la base est créée… sinon le système plantera.

Pour cela, on crée une tâche d’initialisation dans le fichier Rakefile.rb :

namespace 'db' do
  task :create do
    system "erl -pa #{EBIN_DIR} -s xxx_db create -mnesia dir 'db' -s init stop"
  end
end

On démarre la fonction create du module xxx_db, et les données seront stockées dans le même répertoire que vu précédemment.

Ce module est un fichier créé dans le répertoire des sources src/xxx_db.erl, et défini ainsi :

-module(xxx_db).
-export([create/0]).
-include("records.hrl"). (a)
 
create() ->
  mnesia:delete_schema([node()]),
  mnesia:create_schema([node()]), (b)
  mnesia:start(),
 
  mnesia:create_table(user, [{disc_copies, [node()]}, {attributes, record_info(fields, user)}]), (c)
 
  mnesia:stop().

Une base mnesia requiert un espace d’adressage qui comporte un certain nombre de propriétés. C’est ce qui est défini dans la création du schéma (b), ici les propriétés par défaut sont positionnées.

On démarre le moteur mnesia, et on créé les tables nécessaires (c) en suivant les définitions. Ces définitions sont introduites par une directive non vue encore (a) qui permet d’inclure des fichiers de définition. Ce fichier contient la définition des tables et se trouve dans le répertoire source src/records.hrl. Il a cette forme :

-record(user, {name, password}).

Les records sont une facilité d’écriture fournie par erlang et représentent des tuples nommés. Ce fichier sera inclus dans l’applicatif dès que l’on devra manipuler une table mnesia.

Important : mnesia considère que le premier champ du record représente sa clé ! Cette dernière pourra être utilisée pour une interrogation directe.

Note : De même, on peut créer la fonction init/0 qui pré-rempli la base avec des données de référence.

Intégration mochiweb

Mnesia doit être démarré comme tout autre service lors du lancement de notre applicatif. On modifie donc le fichier de démarrage src/xxx_app.erl afin d’intégrer ce comportement :

start(_Type, _StartArgs) ->
  application:start(crypto),
  application:start(mnesia),
  application:start(ecouch),
  xxx_deps:ensure(),
  xxx_sup:start_link().
 
stop(_State) ->
  application:stop(ecouch),
  application:stop(mnesia),
  application:stop(crypto),
  ok.

A partir de là, la base mnesia peut être interrogée par l’applicatif. Ces interrogations se font directement par les fonctions de l’API mnesia ou bien par le système qlc basé sur des expressions de listes.