Dans l'un de mes articles précédents, j'ai écrit sur les tables Erlang Term Storage (ou simplement ETS), qui permettent de stocker des mulples de données arbitraires en mémoire. Nous avons également discuté de l'ETS basé sur disque (DETS), qui fournit des fonctionnalités légèrement plus limitées, mais vous permet d'enregistrer votre contenu dans un fichier..
Cependant, vous pouvez parfois avoir besoin d’une solution encore plus puissante pour stocker les données. Découvrez Mnesia, un système de gestion de base de données distribuée en temps réel initialement introduit à Erlang. Mnesia possède un modèle de données hybride relationnel / objet et offre de nombreuses fonctionnalités intéressantes, notamment la réplication et la recherche rapide de données..
Dans cet article, vous apprendrez:
Commençons, allons-nous?
Ainsi, comme déjà mentionné ci-dessus, Mnesia est un modèle de données objet et relationnel qui évolue très bien. Il utilise un langage de requête DMBS et supporte les transactions atomiques, comme toute autre solution populaire (Postgres ou MySQL, par exemple). Les tables de Mnesia peuvent être stockées sur disque et en mémoire, mais les programmes peuvent être écrits sans connaître l'emplacement réel des données. De plus, vous pouvez répliquer vos données sur plusieurs nœuds. Notez également que Mnesia fonctionne dans la même instance BEAM que tous les autres codes..
Comme Mnesia est un module Erlang, vous devriez y accéder en utilisant un atome:
: mnesia
Bien qu'il soit possible de créer un alias comme ceci:
alias: mnesia, en tant que: mnesia
Les données de Mnesia sont organisées en les tables qui ont leurs propres noms représentés sous forme d'atomes (ce qui est très similaire à ETS). Les tables peuvent avoir l'un des types suivants:
:ensemble
-le type par défaut. Vous ne pouvez pas avoir plusieurs lignes avec exactement la même clé primaire (nous verrons dans un instant comment définir une clé primaire). Les rangées ne sont pas ordonnées de manière particulière.: Order_set
-pareil que :ensemble
, mais les données sont ordonnées par la clé primaire. Nous verrons plus tard que certaines opérations de lecture se comporteront différemment avec : Order_set
les tables.:sac
-plusieurs lignes peuvent avoir la même clé, mais les lignes ne peuvent toujours pas être totalement identiques.Les tableaux ont d'autres propriétés que l'on peut trouver dans la documentation officielle (nous en discuterons certaines dans la section suivante). Cependant, avant de commencer à créer des tables, nous avons besoin d’un schéma. Nous allons donc passer à la section suivante et en ajouter un..
Pour créer un nouveau schéma, nous allons utiliser une méthode avec un nom peu surprenant: create_schema / 1
. Fondamentalement, il va créer une nouvelle base de données pour nous sur un disque. Il accepte un nœud comme argument:
: mnesia.create_schema ([node ()])
Un nœud est une machine virtuelle Erlang qui gère ses communications, sa mémoire et d’autres éléments. Les nœuds peuvent se connecter les uns aux autres, et ils ne sont pas limités à un PC. Vous pouvez également vous connecter à d'autres nœuds via Internet..
Après avoir exécuté le code ci-dessus, un nouveau répertoire nommé Mnesia.nonode@nohost sera créé qui va contenir votre base de données. nonode @ nohost est le nom du noeud ici. Avant de pouvoir créer des tables, toutefois, Mnesia doit être démarré. C’est aussi simple que d’appeler le début / 0
une fonction:
: mnesia.start ()
Mnesia doit être démarré sur tous les nœuds participants. Chacun de ces nœuds a normalement un dossier dans lequel les fichiers seront écrits (dans notre cas, ce dossier est nommé Mnesia.nonode@nohost). Tous les noeuds qui composent le système Mnesia sont écrits dans le schéma. Vous pouvez ensuite ajouter ou supprimer des noeuds individuels. De plus, au démarrage, les noeuds échangent des informations de schéma pour s'assurer que tout va bien..
Si Mnesia a démarré avec succès, un :D'accord
atome sera retourné à la suite. Vous pouvez ensuite arrêter le système en appelant arrêter / 0
:
: mnesia.stop () # =>: arrêté
Nous pouvons maintenant créer une nouvelle table. À tout le moins, nous devrions fournir son nom et une liste d'attributs pour les enregistrements (considérez-les comme des colonnes):
: mnesia.create_table (: utilisateur, [attributs: [: id,: nom,: nom de famille]]) # => : atomique,: ok
Si le système ne fonctionne pas, la table ne sera pas créée et un : abandonné, : node_not_running,: nonode @ nohost
l'erreur sera retournée à la place. En outre, si la table existe déjà, vous obtiendrez un : aborted, : already_exists,: user
Erreur.
Alors notre nouvelle table s'appelle :utilisateur
, et il a trois attributs: : id
, :prénom
, et :nom de famille
. Notez que le premier attribut de la liste est toujours utilisé comme clé primaire et que nous pouvons l'utiliser pour rechercher rapidement un enregistrement. Nous verrons plus loin dans cet article comment écrire des requêtes complexes et ajouter des index secondaires.
De plus, rappelez-vous que le type par défaut pour la table est :ensemble
, mais cela peut être changé assez facilement:
: mnesia.create_table (: utilisateur, [attributs: [: id,: nom,: nom de famille], type:: sac])
Vous pouvez même rendre votre table en lecture seule en réglant la :Mode d'accès
à :lecture seulement:
: mnesia.create_table (: utilisateur, [attributs: [: id,: nom,: nom de famille], type:: bag, mode_accès: read_only])
Une fois le schéma et la table créés, le répertoire aura un schéma.DAT déposer ainsi que certains .bûche des dossiers. Passons maintenant à la section suivante et insérons des données dans notre nouveau tableau.!
Pour stocker des données dans une table, vous devez utiliser une fonction écrire / 1
. Par exemple, ajoutons un nouvel utilisateur nommé John Doe:
: mnesia.write (: user, 1, "John", "Doe")
Notez que nous avons spécifié le nom de la table et tous les attributs de l'utilisateur à stocker. Essayez d’exécuter le code… et il échoue lamentablement avec un : aborted,: no_transaction
Erreur. Pourquoi cela arrive-t-il? Eh bien, c'est parce que le écrire / 1
La fonction doit être exécutée dans une transaction. Si, pour une raison quelconque, vous ne souhaitez pas vous en tenir à une transaction, l'opération d'écriture peut être effectuée de manière "sale" en utilisant dirty_write / 1
:
: mnesia.dirty_write (: user, 1, "John", "Doe") # =>: ok
Cette approche n’est généralement pas recommandée. Par conséquent, construisons une transaction simple à l’aide du transaction
une fonction:
: mnesia.transaction (fn ->: mnesia.write (: utilisateur, 1, "John", "Doe") end) # => : atomique,: ok
transaction
accepte une fonction anonyme comportant une ou plusieurs opérations groupées. Notez que dans ce cas le résultat est : atomique,: ok
, pas seulement :D'accord
comme c'était avec le dirty_write
une fonction. Le principal avantage ici est que si quelque chose ne va pas pendant la transaction, toutes les opérations sont annulées.
En fait, c'est un principe d'atomicité, qui dit que toutes les opérations doivent avoir lieu ou qu'aucune opération ne doit avoir lieu en cas d'erreur. Supposons, par exemple, que vous payez les salaires de vos employés et que soudainement, quelque chose ne va pas. L'opération s'arrête et vous ne voulez certainement pas vous retrouver dans une situation où certains employés touchent leur salaire et d'autres non. C'est quand les transactions atomiques sont vraiment pratiques.
le transaction
function peut avoir autant d'opérations d'écriture que nécessaire:
write_data = fn ->: mnesia.write (: utilisateur, 2, "Kate", "Brown"): mnesia.write (: utilisateur, 3, "Will", "Smith") fin: mnesia.transaction (write_data) # => : atomic,: ok
Fait intéressant, les données peuvent être mises à jour en utilisant le écrire
fonctionner aussi bien. Fournissez simplement la même clé et de nouvelles valeurs pour les autres attributs:
update_data = fn ->: mnesia.write (: utilisateur, 2, "Kate", "Smith"): mnesia.write (: utilisateur, 3, "Will", "Brown") fin: mnesia.transaction (update_data)
Notez cependant que cela ne fonctionnera pas pour les tables de la :sac
type. Comme de telles tables permettent à plusieurs enregistrements d'avoir la même clé, vous obtiendrez tout simplement deux enregistrements: [: utilisateur, 2, "Kate", "Brown", : utilisateur, 2, "Kate", "Smith"]
. Encore, :sac
les tables n'autorisent pas l'existence d'enregistrements totalement identiques.
Bon, maintenant que nous avons des données dans notre tableau, pourquoi ne pas essayer de les lire? Tout comme pour les opérations d'écriture, vous pouvez effectuer une lecture de manière "sale" ou "transactionnelle". Le "sale chemin" est bien sûr plus simple (mais c'est le côté obscur de la Force, Luke!):
: mnesia.dirty_read (: utilisateur, 2) # => [: utilisateur, 2, "Kate", "Smith"]
Alors dirty_read
renvoie une liste des enregistrements trouvés en fonction de la clé fournie. Si la table est un :ensemble
ou un : Order_set
, la liste aura un seul élément. Pour :sac
tables, la liste peut, bien sûr, avoir plusieurs éléments. Si aucun enregistrement n'était trouvé, la liste serait vide.
Essayons maintenant d’effectuer la même opération mais en utilisant l’approche transactionnelle:
read_data = fn ->: mnesia.read (: utilisateur, 2) fin: mnesia.transaction (read_data) => : atomique, [: utilisateur, 2, "Kate", "Brown"]
Génial!
Existe-t-il d'autres fonctions utiles pour la lecture de données? Mais bien sûr! Par exemple, vous pouvez récupérer le premier ou le dernier enregistrement de la table:
: mnesia.dirty_first (: utilisateur) # => 2: mnesia.dirty_last (: utilisateur) # => 2
Tous les deux dirty_first
et sale_last
ont leurs contreparties transactionnelles, à savoir premier
et dernier
, cela devrait être emballé dans une transaction. Toutes ces fonctions renvoient la clé de l'enregistrement, mais notez que dans les deux cas, nous obtenons 2
en conséquence, même si nous avons deux enregistrements avec les clés 2
et 3
. Pourquoi cela arrive-t-il?
Il semble que pour le :ensemble
et :sac
tables, les dirty_first
et sale_last
(aussi bien que premier
et dernier
) les fonctions sont synonymes car les données ne sont pas triées dans un ordre spécifique. Si, toutefois, vous avez un : Order_set
table, les enregistrements seront triés par leurs clés et le résultat serait:
: mnesia.dirty_first (: utilisateur) # => 2: mnesia.dirty_last (: utilisateur) # => 3
Il est également possible de saisir la clé suivante ou précédente en utilisant dirty_next
et dirty_prev
(ou suivant
et prev
):
: mnesia.dirty_next (: utilisateur, 2) => 3: mnesia.dirty_next (: utilisateur, 3) =>: "$ end_of_table"
S'il n'y a plus d'enregistrements, un atome spécial : "$ end_of_table"
est retourné. En outre, si la table est un :ensemble
ou :sac
, dirty_next
et dirty_prev
sont des synonymes.
Enfin, vous pouvez obtenir toutes les clés d’une table en utilisant dirty_all_keys / 1
ou all_keys / 1
:
: mnesia.dirty_all_keys (: utilisateur) # => [3, 2]
Afin de supprimer un enregistrement d'une table, utilisez sale_delete
ou effacer
:
: mnesia.dirty_delete (: user, 2) # =>: ok
Cela va supprimer tous les enregistrements avec une clé donnée.
De même, vous pouvez supprimer toute la table:
: mnesia.delete_table (: utilisateur)
Il n'y a pas de contrepartie "sale" pour cette méthode. Évidemment, après la suppression d’une table, vous ne pouvez rien y écrire, et un : annulé, : no_exists,: utilisateur
l'erreur sera retournée à la place.
Enfin, si vous êtes vraiment d'humeur à supprimer, vous pouvez supprimer l'ensemble du schéma en utilisant delete_schema / 1
:
: mnesia.delete_schema ([node ()])
Cette opération retournera un : error, 'Mnesia n'est pas arrêté partout', [: nonode @ nohost]
error si Mnesia n'est pas arrêté, alors n'oubliez pas de le faire:
: mnesia.stop (): mnesia.delete_schema ([node ()])
Maintenant que nous avons vu les bases du travail avec Mnesia, approfondissons un peu et voyons comment écrire des requêtes avancées. Tout d'abord, il y a match_object
et dirty_match_object
fonctions pouvant être utilisées pour rechercher un enregistrement en fonction de l'un des attributs fournis:
: mnesia.dirty_match_object (: utilisateur,: _, "Kate", "Brown") # => [: utilisateur, 2, "Kate", "Brown"]
Les attributs qui ne vous intéressent pas sont marqués du : _
atome. Vous pouvez définir uniquement le nom de famille, par exemple:
: mnesia.dirty_match_object (: user,: _,: _, "Brown") # => [: user, 2, "Kate", "Brown"]
Vous pouvez également fournir des critères de recherche personnalisés en utilisant sélectionner
et dirty_select
. Pour voir cela en action, commençons par renseigner le tableau avec les valeurs suivantes:
write_data = fn ->: mnesia.write (: utilisateur, 2, "Kate", "Brown"): mnesia.write (: utilisateur, 3, "Will", "Smith"): mnesia.write ( : user, 4, "Will", "Smoth"): mnesia.write (: user, 5, "Will", "Smath") end: mnesia.transaction (write_data)
Maintenant, ce que je veux faire est de trouver tous les disques qui ont Volonté
comme le nom et dont les clés sont inférieures à 5
, ce qui signifie que la liste résultante ne devrait contenir que "Will Smith" et "Will Smoth". Voici le code correspondant:
: mnesia.dirty_select (: utilisateur, [: utilisateur,: "$ 1",: "$ 2",: "$ 3", [:<, :"$1", 5, :==, :"$2", "Will" ], [:"$$"] ] ) # => [[3, "Volonté", "Smith"], [4, "Volonté", "Smoth"]
Les choses sont un peu plus complexes ici, alors discutons de cet extrait étape par étape.
: utilisateur,: "$ 1",: "$ 2",: "$ 3"
partie. Nous fournissons ici le nom de la table et une liste de paramètres de position. Ils devraient être écrits sous cette forme étrange afin que nous puissions les utiliser plus tard. 1 $
correspond à la : id
, 2 $
est le prénom
, et 3 $
est le nom de famille
.:<, :"$1", 5
signifie que nous aimerions sélectionner uniquement les enregistrements dont l'attribut marqué comme 1 $
(C'est, : id
) est inférieur à 5
. : ==,: "$ 2", "Will"
, à son tour, signifie que nous sélectionnons les enregistrements avec le :prénom
mis à "Volonté"
.[: "$$"]
signifie que nous aimerions inclure tous les champs dans le résultat. Vous pouvez dire [: "$ 2"]
pour afficher uniquement le nom. Notez, en passant, que le résultat contient une liste de listes: [[3, "Volonté", "Smith"], [4, "Volonté", "Smoth"]
.Vous pouvez également marquer certains attributs comme ceux qui ne vous intéressent pas. : _
atome. Par exemple, ignorons le nom de famille:
: mnesia.dirty_select (: utilisateur, [: utilisateur,: "$ 1",: "$ 2",: _, [:<, :"$1", 5, :==, :"$2", "Will" ], [:"$$"] ] ) # => [[3, "Volonté"], [4, "Volonté"]]
Dans ce cas, toutefois, le nom de famille ne sera pas inclus dans le résultat..
Supposons maintenant que nous souhaitons modifier notre table en ajoutant un nouveau champ. Cela peut être fait en utilisant le transform_table
function, qui accepte le nom de la table, une fonction à appliquer à tous les enregistrements et la liste des nouveaux attributs:
: mnesia.transform_table (: utilisateur, fn (: utilisateur, identifiant, nom) -> : utilisateur, identifiant, nom, nom de famille,: rand.uniform (1000) end, [: id,: nom, : nom de famille, salaire]
Dans cet exemple, nous ajoutons un nouvel attribut nommé :un salaire
(il est fourni dans le dernier argument). En ce qui concerne la transformer la fonction (le deuxième argument), nous définissons ce nouvel attribut à une valeur aléatoire. Vous pouvez également modifier tout autre attribut dans cette fonction de transformation. Ce processus de modification des données s'appelle une "migration" et ce concept devrait être familier aux développeurs du monde Rails..
Maintenant, vous pouvez simplement récupérer des informations sur les attributs de la table en utilisant table_info
:
: mnesia.table_info (: utilisateur,: attributs) # => [: id,: nom,: nom de famille,: salaire]
le :un salaire
attribut est là! Et, bien sûr, vos données sont également en place:
: mnesia.dirty_read (: utilisateur, 2) # => [: utilisateur, 2, "Kate", "Brown", 778]
Vous pouvez trouver un exemple un peu plus complexe d’utilisation de la create_table
et transform_table
fonctions sur le site Web ElixirSchool.
Mnesia vous permet de créer n'importe quel attribut indexé à l'aide du add_table_index
une fonction. Par exemple, faisons notre :nom de famille
attribut indexé:
: mnesia.add_table_index (: utilisateur,: nom de famille) # => : atomique,: ok
Si l'index existe déjà, vous obtiendrez une erreur : aborted, : already_exists,: user, 4
.
Comme l'indique la documentation de cette fonction, les index ne sont pas gratuits. Plus précisément, ils occupent un espace supplémentaire (proportionnel à la taille de la table) et ralentissent un peu les opérations d’insertion. D'autre part, ils vous permettent de rechercher les données plus rapidement, ce qui en fait un compromis équitable..
Vous pouvez rechercher par un champ indexé en utilisant soit le dirty_index_read
ou index_read
une fonction:
: mnesia.dirty_index_read (: utilisateur, "Smith",: nom de famille) # => [: utilisateur, 3, "Will", "Smith"]
Ici nous utilisons l'index secondaire :nom de famille
rechercher un utilisateur.
Il peut sembler fastidieux de travailler directement avec le module Mnesia, mais heureusement, il existe un package tiers appelé Amnesia (duh!) Qui vous permet d'effectuer des opérations triviales avec une plus grande facilité..
Par exemple, vous pouvez définir votre base de données et une table comme celle-ci:
utiliser Amnesia defdatabase Demo do Deftable User, [: id, autoincrement,: name,: name,: email], index: [: email] do end end
Cela va définir une base de données appelée Démo
avec une table Utilisateur
. L'utilisateur va nommer un nom, un nom de famille, un e-mail (un champ indexé) et un identifiant (clé primaire définie sur auto-incrémentation).
Ensuite, vous pouvez facilement créer le schéma en utilisant la tâche de mixage intégrée:
mélanger amnesia.create -d Demo --disk
Dans ce cas, la base de données sera basée sur disque, mais vous pouvez définir certaines autres options disponibles. Il existe également une tâche de suppression qui va, évidemment, détruire la base de données et toutes les données:
mélanger amnesia.drop -d Demo
Il est possible de détruire à la fois la base de données et le schéma:
mix amnesia.drop -d Demo --schema
Une fois la base de données et le schéma en place, il est possible d'effectuer diverses opérations sur la table. Par exemple, créez un nouvel enregistrement:
Amnesia.transaction do will_smith =% User name: "Will", nom de famille: "Smith", email: "[email protected]">> User.write end
Ou obtenez un utilisateur par identifiant:
Amnesia.transaction do will_smith = User.read (1) end
De plus, vous pouvez définir un Message
table tout en établissant une relation avec le Utilisateur
table avec un identifiant d'utilisateur
en tant que clé étrangère:
Message définissable, [: user_id,: content] do end
Les tables peuvent contenir de nombreuses fonctions d’aide, par exemple pour créer un message ou obtenir tous les messages:
utilisateur utilisable, [: id, autoincrement,: name,: name,: email], index: [: email] do def add_message (self, content) do% Message user_id: self.id, content: content | > Message.write end def messages (self) do Message.read (self.id) end end
Vous pouvez maintenant trouver l'utilisateur, créer un message pour lui ou lister facilement tous ses messages:
Amnesia.transaction do will_smith = User.read (1) will_smith |> User.add_message "hi!" will_smith |> User.messages end
Assez simple, n'est-ce pas? Vous trouverez d'autres exemples d'utilisation sur le site officiel d'Amnesia..
Dans cet article, nous avons parlé du système de gestion de base de données Mnesia disponible pour Erlang et Elixir. Nous avons discuté des concepts principaux de ce SGBD et avons vu comment créer un schéma, une base de données et des tables, ainsi que pour effectuer toutes les opérations principales: créer, lire, mettre à jour et détruire. De plus, vous avez appris à travailler avec des index, à transformer des tables et à utiliser le package Amnesia pour simplifier l'utilisation de bases de données..
J'espère vraiment que cet article a été utile et que vous êtes impatient d'essayer Mnesia en action également. Comme toujours, je vous remercie de rester avec moi et à la prochaine fois!