In einem meiner vorherigen Artikel habe ich über Erlang-Term-Storage-Tabellen (oder einfach ETS) geschrieben, mit denen Tupel beliebiger Daten im Speicher gespeichert werden können. Wir haben auch diskettenbasierte ETS (DETS) besprochen, die etwas eingeschränktere Funktionen bieten, aber Sie können Ihre Inhalte in einer Datei speichern.
Manchmal benötigen Sie jedoch eine noch leistungsfähigere Lösung zum Speichern der Daten. Lernen Sie Mnesia kennen - ein in Erlang erstmals eingeführtes verteiltes Datenbankmanagementsystem in Echtzeit. Mnesia verfügt über ein relationales / Objekt-Hybrid-Datenmodell und bietet viele nützliche Funktionen, darunter Replikation und schnelle Datensuche.
In diesem Artikel erfahren Sie:
Lass uns anfangen, sollen wir?
Wie bereits erwähnt, ist Mnesia also ein objekt- und relationales Datenmodell, das wirklich gut skaliert. Es hat eine DMBS-Abfragesprache und unterstützt atomare Transaktionen wie jede andere beliebte Lösung (z. B. Postgres oder MySQL). Mnesias Tabellen können auf der Festplatte und im Arbeitsspeicher gespeichert werden, Programme können jedoch ohne Kenntnis des tatsächlichen Speicherorts geschrieben werden. Darüber hinaus können Sie Ihre Daten über mehrere Knoten hinweg replizieren. Beachten Sie auch, dass Mnesia in derselben BEAM-Instanz wie der andere Code ausgeführt wird.
Da Mnesia ein Erlang-Modul ist, sollten Sie über ein Atom darauf zugreifen:
: Mnesia
Es ist jedoch möglich, einen Alias wie folgt zu erstellen:
Alias: Mnesia, wie: Mnesia
Daten in Mnesia sind in organisiert Tische deren eigene Namen als Atome dargestellt werden (was der ETS sehr ähnlich ist). Die Tabellen können einen der folgenden Typen haben:
:einstellen
-der Standardtyp. Sie können nicht mehrere Zeilen mit genau demselben Primärschlüssel haben (wir werden gleich sehen, wie Sie einen Primärschlüssel definieren). Die Reihen werden nicht auf eine bestimmte Art und Weise angeordnet.: order_set
-gleich wie :einstellen
, Die Daten werden jedoch nach dem Primärschlüssel sortiert. Später werden wir sehen, dass sich einige Leseoperationen anders verhalten werden : order_set
Tische.:Tasche
-Mehrere Zeilen können denselben Schlüssel haben, die Zeilen können jedoch immer noch nicht vollständig identisch sein.Tabellen haben andere Eigenschaften, die in den offiziellen Dokumenten gefunden werden können (einige werden im nächsten Abschnitt besprochen). Bevor Sie jedoch mit dem Erstellen von Tabellen beginnen, benötigen Sie ein Schema. Fahren wir mit dem nächsten Abschnitt fort und fügen Sie eines hinzu.
Um ein neues Schema zu erstellen, verwenden wir eine Methode mit einem ziemlich nicht überraschenden Namen: create_schema / 1
. Im Grunde wird für uns eine neue Datenbank auf einer Platte erstellt. Es akzeptiert einen Knoten als Argument:
: mnesia.create_schema ([node ()])
Ein Knoten ist eine Erlang-VM, die für die Kommunikation, den Speicher und anderes zuständig ist. Knoten können sich miteinander verbinden, und sie sind nicht auf einen PC beschränkt. Sie können sich auch über das Internet mit anderen Knoten verbinden.
Nachdem Sie den obigen Code ausgeführt haben, heißt ein neues Verzeichnis Mnesia.nonode@nohost wird erstellt, die Ihre Datenbank enthalten wird. nonode @ nohost ist hier der Name des Knotens. Bevor wir jedoch Tabellen erstellen können, muss Mnesia gestartet werden. Dies ist so einfach wie das Aufrufen der Start / 0
Funktion:
: mnesia.start ()
Mnesia sollte auf allen teilnehmenden Knoten gestartet werden, von denen jeder normalerweise einen Ordner hat, in den die Dateien geschrieben werden (in unserem Fall hat dieser Ordner einen Namen) Mnesia.nonode@nohost). Alle Knoten, aus denen das Mnesia-System besteht, werden in das Schema geschrieben. Später können Sie einzelne Knoten hinzufügen oder entfernen. Beim Start tauschen Knoten außerdem Schemainformationen aus, um sicherzustellen, dass alles in Ordnung ist.
Wenn Mnesia erfolgreich gestartet wurde, eine :OK
atom wird als Ergebnis zurückgegeben. Sie können das System später anrufen, indem Sie anrufen Stop / 0
:
: mnesia.stop () # =>: gestoppt
Jetzt können wir eine neue Tabelle erstellen. Zumindest sollten wir seinen Namen und eine Liste von Attributen für die Datensätze angeben (stellen Sie sich diese als Spalten vor):
: mnesia.create_table (: Benutzer, [Attribute: [: ID,: Name,: Nachname]]]) # => : atomar,: ok
Wenn das System nicht läuft, wird die Tabelle nicht erstellt und ein : abgebrochen, : node_not_running,: nonode @ nohost
Fehler wird stattdessen zurückgegeben. Wenn die Tabelle bereits vorhanden ist, erhalten Sie eine : abgebrochen, : already_exists,: user
Error.
So heißt unsere neue Tabelle :Nutzer
, und es hat drei Attribute: :Ich würde
, :Name
, und :Nachname
. Beachten Sie, dass das erste Attribut in der Liste immer als Primärschlüssel verwendet wird. Mit diesem Attribut können Sie schnell nach einem Datensatz suchen. Später in diesem Artikel erfahren Sie, wie Sie komplexe Abfragen schreiben und Sekundärindizes hinzufügen.
Denken Sie auch daran, dass der Standardtyp für die Tabelle ist :einstellen
, Dies kann jedoch leicht geändert werden:
: mnesia.create_table (: Benutzer, [Attribute: [: ID,: Name,: Nachname], Typ:: Bag])
Sie können Ihre Tabelle sogar schreibgeschützt machen, indem Sie die Option :Zugriffsmodus
zu :schreibgeschützt:
: mnesia.create_table (: Benutzer, [Attribute: [: ID,: Name,: Nachname], Typ:: Bag, Zugriffsmodus: Read_only])
Nachdem das Schema und die Tabelle erstellt wurden, hat das Verzeichnis eine schema.DAT Datei sowie einige .Log Dateien. Fahren wir nun mit dem nächsten Abschnitt fort und fügen Sie einige Daten in unsere neue Tabelle ein!
Um einige Daten in einer Tabelle zu speichern, müssen Sie eine Funktion verwenden schreiben / 1
. Fügen wir beispielsweise einen neuen Benutzer namens John Doe hinzu:
: mnesia.write (: user, 1, "John", "Doe")
Beachten Sie, dass wir sowohl den Namen der Tabelle als auch alle Attribute des Benutzers zum Speichern angegeben haben. Versuchen Sie, den Code auszuführen ... und es schlägt fehl mit einem : abgebrochen,: no_transaction
Error. Warum passiert dies? Nun, das liegt daran schreiben / 1
Funktion sollte in einer Transaktion ausgeführt werden. Wenn Sie aus irgendeinem Grund nicht bei einer Transaktion bleiben möchten, wird der Schreibvorgang möglicherweise auf "schmutzige Weise" ausgeführt dirty_write / 1
:
: mnesia.dirty_write (: Benutzer, 1, "John", "Doe") # =>: ok
Dieser Ansatz wird normalerweise nicht empfohlen, erstellen wir stattdessen eine einfache Transaktion mit Hilfe von Transaktion
Funktion:
: mnesia.transaction (fn ->: mnesia.write (: Benutzer, 1, "John", "Doe") end) # => : atomic,: ok
Transaktion
akzeptiert eine anonyme Funktion mit einer oder mehreren gruppierten Operationen. Beachten Sie, dass in diesem Fall das Ergebnis lautet : atomic,: ok
, nicht nur :OK
wie war es mit dem dirty_write
Funktion. Der Hauptvorteil hierbei ist, dass alle Vorgänge rückgängig gemacht werden, wenn während der Transaktion etwas schief geht.
Tatsächlich ist dies ein Atomitätsprinzip, das besagt, dass entweder alle Operationen ausgeführt werden sollten oder im Fehlerfall keine Operationen. Nehmen Sie beispielsweise an, Sie zahlen Ihren Angestellten ihre Gehälter, und plötzlich geht etwas schief. Die Operation wird gestoppt, und Sie möchten definitiv nicht in einer Situation enden, in der einige Mitarbeiter ihr Gehalt bekamen und andere nicht. Dann sind atomare Transaktionen sehr praktisch.
Das Transaktion
Funktion kann so viele Schreibvorgänge haben wie nötig:
write_data = fn ->: mnesia.write (: Benutzer, 2, "Kate", "Brown"): mnesia.write (: Benutzer, 3, "Will", "Smith") Ende: mnesia.transaction (write_data) # => : atomic,: ok
Interessanterweise können Daten mit dem aktualisiert werden schreiben
funktionieren auch. Geben Sie einfach den gleichen Schlüssel und die neuen Werte für die anderen Attribute an:
update_data = fn ->: mnesia.write (: Benutzer, 2, "Kate", "Smith"): mnesia.write (: Benutzer, 3, "Will", "Brown") Ende: mnesia.transaction (Daten aktualisieren)
Beachten Sie jedoch, dass dies nicht für die Tabellen von funktioniert :Tasche
Art. Da bei solchen Tabellen mehrere Datensätze denselben Schlüssel haben, erhalten Sie einfach zwei Datensätze: [: Benutzer, 2, "Kate", "Brown", : Benutzer, 2, "Kate", "Smith"]
. Immer noch, :Tasche
Tabellen lassen nicht zu, dass völlig identische Datensätze vorhanden sind.
Okay, jetzt, da wir einige Daten in unserer Tabelle haben, warum versuchen wir nicht, sie zu lesen? Genau wie bei Schreibvorgängen können Sie das Lesen entweder auf "schmutzige" oder "transaktionale" Weise durchführen. Der "schmutzige Weg" ist natürlich einfacher (aber das ist die dunkle Seite der Macht, Luke!):
: mnesia.dirty_read (: user, 2) # => [: user, 2, "Kate", "Smith"]
So dirty_read
Gibt eine Liste der gefundenen Datensätze basierend auf dem angegebenen Schlüssel zurück. Wenn der Tisch ein ist :einstellen
oder ein : order_set
, Die Liste hat nur ein Element. Zum :Tasche
Tabellen kann die Liste natürlich mehrere Elemente enthalten. Wenn keine Datensätze gefunden wurden, wäre die Liste leer.
Versuchen wir nun, dieselbe Operation auszuführen, jedoch den Transaktionsansatz zu verwenden:
read_data = fn ->: mnesia.read (: user, 2) end: mnesia.transaction (read_data) => : atomic, [: user, 2, "Kate", "Brown"]
Großartig!
Gibt es noch andere nützliche Funktionen zum Lesen von Daten? Aber natürlich! Beispielsweise können Sie den ersten oder den letzten Datensatz der Tabelle abrufen:
: mnesia.dirty_first (: user) # => 2: mnesia.dirty_last (: user) # => 2
Beide dirty_first
und dirty_last
haben ihre Transaktionspartner, nämlich zuerst
und zuletzt
, das sollte in einer Transaktion verpackt werden. Alle diese Funktionen geben den Schlüssel des Datensatzes zurück. Beachten Sie jedoch, dass wir in beiden Fällen erhalten 2
Als Ergebnis haben wir zwei Datensätze mit den Schlüsseln 2
und 3
. Warum passiert dies?
Es scheint das für die :einstellen
und :Tasche
Tabellen, die dirty_first
und dirty_last
(ebenso gut wie zuerst
und zuletzt
) Funktionen sind Synonyme, da die Daten nicht in einer bestimmten Reihenfolge sortiert sind. Wenn Sie jedoch eine haben : order_set
In der Tabelle werden die Datensätze nach ihren Schlüsseln sortiert, und das Ergebnis lautet:
: mnesia.dirty_first (: user) # => 2: mnesia.dirty_last (: user) # => 3
Es ist auch möglich, den nächsten oder vorherigen Schlüssel mit zu greifen dirty_next
und dirty_prev
(oder Nächster
und vorheriges
):
: mnesia.dirty_next (: user, 2) => 3: mnesia.dirty_next (: user, 3) =>: "$ end_of_table"
Wenn keine Datensätze mehr vorhanden sind, ein spezielles Atom : "$ end_of_table"
ist zurück gekommen. Auch wenn der Tisch ein ist :einstellen
oder :Tasche
, dirty_next
und dirty_prev
sind Synonyme.
Schließlich können Sie alle Schlüssel aus einer Tabelle erhalten, indem Sie verwenden dirty_all_keys / 1
oder all_keys / 1
:
: mnesia.dirty_all_keys (: user) # => [3, 2]
Um einen Datensatz aus einer Tabelle zu löschen, verwenden Sie dirty_delete
oder löschen
:
: mnesia.dirty_delete (: user, 2) # =>: ok
Dadurch werden alle Datensätze mit einem bestimmten Schlüssel entfernt.
Ebenso können Sie die gesamte Tabelle entfernen:
: mnesia.delete_table (: user)
Es gibt kein "schmutziges" Gegenstück für diese Methode. Nachdem eine Tabelle gelöscht wurde, können Sie natürlich nichts schreiben und eine : abgebrochen, : no_exists,: user
Fehler wird stattdessen zurückgegeben.
Wenn Sie wirklich in einer Stimmung zum Löschen sind, können Sie das gesamte Schema mithilfe von entfernen delete_schema / 1
:
: mnesia.delete_schema ([node ()])
Diese Operation gibt eine zurück : error, 'Mnesia wird nicht überall gestoppt', [: nonode @ nohost]
Fehler, wenn Mnesia nicht gestoppt wird, also nicht vergessen:
: mnesia.stop (): mnesia.delete_schema ([node ()])
Nun, da wir die Grundlagen der Arbeit mit Mnesia kennengelernt haben, wollen wir uns etwas näher mit dem Thema befassen, um erweiterte Abfragen zu schreiben. Erstens gibt es match_object
und dirty_match_object
Funktionen, die zum Suchen nach einem Datensatz basierend auf einem der angegebenen Attribute verwendet werden können:
: mnesia.dirty_match_object (: Benutzer,: _, "Kate", "Brown") # => [: Benutzer, 2, "Kate", "Brown"]
Die Attribute, die Sie nicht interessieren, sind mit gekennzeichnet : _
Atom. Sie können nur den Nachnamen angeben, zum Beispiel:
: mnesia.dirty_match_object (: Benutzer,: _,: _, "Brown") # => [: Benutzer, 2, "Kate", "Brown"]
Sie können auch benutzerdefinierte Suchkriterien angeben wählen
und dirty_select
. Um dies in Aktion zu sehen, füllen wir zuerst die Tabelle mit den folgenden Werten auf:
write_data = fn ->: mnesia.write (: user, 2, "Kate", "Brown"): mnesia.write (: user, 3, "Will", "Smith"): mnesia.write ( : user, 4, "Will", "Smoth"): mnesia.write (: user, 5, "Will", "Smath") Ende: mnesia.transaction (write_data)
Jetzt möchte ich alle Datensätze finden, die vorhanden sind Wille
als Name und deren Schlüssel sind kleiner als 5
, Das bedeutet, dass die resultierende Liste nur "Will Smith" und "Will Smoth" enthalten sollte. Hier ist der entsprechende Code:
: mnesia.dirty_select (: Benutzer, [: Benutzer,: "$ 1"): "$ 2",: "$ 3", [:<, :"$1", 5, :==, :"$2", "Will" ], [:"$$"] ] ) # => [3, "Will", "Smith"], [4, "Will", "Smoth"]]
Die Dinge sind hier etwas komplexer, lassen Sie uns diesen Ausschnitt also Schritt für Schritt besprechen.
: user,: "$ 1",: "$ 2",: "$ 3"
Teil. Hier geben wir den Tabellennamen und eine Liste der Positionsparameter an. Sie sollten in dieser seltsam aussehenden Form geschrieben werden, damit wir sie später verwenden können. 1 US-Dollar
entspricht dem :Ich würde
, 2 $
ist der Name
, und 3 $
ist der Nachname
.:<, :"$1", 5
bedeutet, dass wir nur die Datensätze auswählen möchten, deren Attribut als markiert ist 1 US-Dollar
(das ist, :Ich würde
) ist weniger als 5
. : ==,: "$ 2", "Will"
, wiederum bedeutet, dass wir die Datensätze mit auswählen :Name
einstellen "Wille"
.[: "$$"]
bedeutet, dass wir alle Felder in das Ergebnis aufnehmen möchten. Du könntest sagen [: "2 $"]
um nur den Namen anzuzeigen. Beachten Sie übrigens, dass das Ergebnis eine Liste von Listen enthält: [3, "Will", "Smith"], [4, "Will", "Smoth"]]
.Sie können einige Attribute auch als diejenigen kennzeichnen, die Sie nicht verwenden möchten : _
Atom. Lassen Sie uns beispielsweise den Nachnamen ignorieren:
: mnesia.dirty_select (: Benutzer, [: Benutzer,: "$ 1"): "$ 2",: _, [:<, :"$1", 5, :==, :"$2", "Will" ], [:"$$"] ] ) # => [3, "Will"], [4, "Will"]]
In diesem Fall wird der Nachname jedoch nicht in das Ergebnis aufgenommen.
Nehmen wir an, wir möchten unsere Tabelle durch Hinzufügen eines neuen Feldes ändern. Dies kann mit der transform_table
function, die den Namen der Tabelle akzeptiert, eine auf alle Datensätze anzuwendende Funktion und die Liste der neuen Attribute:
: mnesia.transform_table (: user, fn (: user, id, name, vorname) -> : user, id, name, familienname: rand.uniform (1000) end, [: id,: name, : Nachname, Gehalt])
In diesem Beispiel fügen wir ein neues Attribut namens hinzu :Gehalt
(wird im letzten Argument angegeben). Wie für die Transformationsfunktion (das zweite Argument), setzen wir dieses neue Attribut auf einen zufälligen Wert. Sie können auch jedes andere Attribut innerhalb dieser Transformationsfunktion ändern. Diese Änderung der Daten wird als "Migration" bezeichnet. Dieses Konzept sollte Entwicklern aus der Rails-Welt vertraut sein.
Jetzt können Sie einfach Informationen über die Attribute der Tabelle mithilfe von verwenden table_info
:
: mnesia.table_info (: Benutzer,: Attribute) # => [: ID,: Name,: Nachname,: Gehalt]
Das :Gehalt
Attribut ist da! Und natürlich sind auch Ihre Daten vorhanden:
: mnesia.dirty_read (: user, 2) # => [: user, 2, "Kate", "Brown", 778]
Sie können ein etwas komplexeres Beispiel für die Verwendung der beiden finden Tabelle erstellen
und transform_table
Funktionen auf der ElixirSchool-Website.
Mit Mnesia können Sie jedes indizierte Attribut mithilfe von add_table_index
Funktion. Zum Beispiel machen wir unser :Nachname
Attribut indiziert:
: mnesia.add_table_index (: Benutzer,: Nachname) # => : atomic,: ok
Wenn der Index bereits vorhanden ist, erhalten Sie eine Fehlermeldung : abgebrochen, : already_exists,: user, 4
.
Wie in der Dokumentation zu dieser Funktion angegeben, sind Indizes nicht kostenlos. Insbesondere nehmen sie zusätzlichen Platz ein (proportional zur Tabellengröße) und machen das Einfügen etwas langsamer. Auf der anderen Seite können Sie schneller nach den Daten suchen. Dies ist ein fairer Kompromiss.
Sie können nach einem indizierten Feld mit dem Feld suchen dirty_index_read
oder index_read
Funktion:
: mnesia.dirty_index_read (: Benutzer, "Smith",: Nachname) # => [: Benutzer, 3, "Will", "Smith"]
Hier verwenden wir den Sekundärindex :Nachname
um einen Benutzer zu suchen.
Es kann etwas umständlich sein, direkt mit dem Mnesia-Modul zu arbeiten. Glücklicherweise gibt es jedoch ein Drittanbieter-Paket namens Amnesia (duh!), Mit dem Sie triviale Operationen einfacher ausführen können.
Sie können beispielsweise Ihre Datenbank und eine Tabelle wie folgt definieren:
use amnesia defdatabase Demo duktiver Benutzer, [: id, autoincrement,: name,: Nachname,: email], Index: [: email] do end end
Dies wird eine Datenbank definieren, die aufgerufen wird Demo
mit einem Tisch Nutzer
. Der Benutzer benennt einen Namen, einen Nachnamen, eine E-Mail (ein indiziertes Feld) und eine ID (Primärschlüssel wird auf Autoincrement gesetzt)..
Als Nächstes können Sie das Schema einfach mit der integrierten Mischaufgabe erstellen:
mix amnesia.create -d Demo --disk
In diesem Fall ist die Datenbank festplattenbasiert. Es gibt jedoch noch weitere Optionen, die Sie festlegen können. Es gibt auch eine Drop-Task, die natürlich die Datenbank und alle Daten zerstört:
mix amnesia.drop -d Demo
Es ist möglich, sowohl die Datenbank als auch das Schema zu zerstören:
mix amnesia.drop -d Demo --schema
Wenn Datenbank und Schema vorhanden sind, können verschiedene Operationen für die Tabelle ausgeführt werden. Erstellen Sie beispielsweise einen neuen Datensatz:
Amnesia.transaction do will_smith =% Benutzer Name: "Will", Nachname: "Smith", E-Mail: "[email protected]" |> User.write end
Oder holen Sie sich einen Benutzer per ID:
Amnesia.transaction do will_smith = User.read (1) end
Darüber hinaus können Sie eine definieren Botschaft
Tabelle, während eine Beziehung zum Nutzer
Tisch mit einem Benutzeridentifikation
als Fremdschlüssel:
deftable Nachricht, [: user_id,: content] wird beendet
Die Tabellen können eine Reihe von Hilfsfunktionen enthalten, z. B. zum Erstellen einer Nachricht oder zum Abrufen aller Nachrichten:
Benutzer, [: id, autoincrement,: name,: Nachname,: email], Index: [: email] do def add_message (self, content) do% Nachricht user_id: self.id, content: content | > Message.write end def messages (self) Ende von Message.read (self.id)
Sie können jetzt den Benutzer finden, eine Nachricht für ihn erstellen oder alle seine Nachrichten mit Leichtigkeit auflisten:
Amnesia.transaction do will_smith = User.read (1) will_smith |> User.add_message "hi!" will_smith |> User.messages enden
Ganz einfach, nicht wahr? Einige andere Anwendungsbeispiele finden Sie auf der offiziellen Website von Amnesia.
In diesem Artikel haben wir über das Mnesia-Datenbankverwaltungssystem für Erlang und Elixir gesprochen. Wir haben die Hauptkonzepte dieses DBMS erörtert und gesehen, wie man ein Schema, eine Datenbank und Tabellen erstellt und alle wichtigen Vorgänge ausführt: Erstellen, Lesen, Aktualisieren und Zerstören. Außerdem haben Sie gelernt, wie Sie mit Indizes arbeiten, Tabellen transformieren und wie Sie das Amnesia-Paket verwenden, um die Arbeit mit Datenbanken zu vereinfachen.
Ich hoffe wirklich, dass dieser Artikel nützlich war und Sie möchten Mnesia auch in Aktion ausprobieren. Wie immer danke ich Ihnen, dass Sie bei mir geblieben sind und bis zum nächsten Mal!