Alles opslaan met Elixir en Mnesia

In een van mijn vorige artikelen schreef ik over Erlang Term Storage-tabellen (of eenvoudigweg ETS), waarmee tuples van willekeurige gegevens in het geheugen kunnen worden opgeslagen. We bespraken ook schijfgebaseerde ETS (DETS), die iets meer beperkte functionaliteit bieden, maar waarmee je je inhoud in een bestand kunt opslaan.

Soms hebt u echter een nog krachtigere oplossing nodig om de gegevens op te slaan. Maak kennis met Mnesia - een real-time gedistribueerd databasemanagementsysteem dat oorspronkelijk werd geïntroduceerd in Erlang. Mnesia heeft een relationeel / object hybride gegevensmodel en heeft veel leuke functies, waaronder replicatie en snelle zoekopdrachten naar gegevens.

In dit artikel leer je:

  • Hoe een Mnesia-schema te maken en het hele systeem te starten.
  • Welke tafeltypen beschikbaar zijn en hoe ze te maken.
  • Hoe CRUD-bewerkingen uit te voeren en wat het verschil is tussen "vervuilde" en "transactionele" functies.
  • Hoe tabellen te wijzigen en secundaire indices toe te voegen.
  • Hoe het Amnesia-pakket te gebruiken om het werken met databases en tabellen te vereenvoudigen.

Laten we beginnen, zullen we?

Inleiding tot Mnesia

Zoals al eerder vermeld, is Mnesia een object- en relationeel gegevensmodel dat zich heel goed schaalt. Het heeft een DMBS-querytaal en ondersteunt atomaire transacties, net als elke andere populaire oplossing (bijvoorbeeld Postgres of MySQL). De tabellen van Mnesia kunnen worden opgeslagen op schijf en in het geheugen, maar programma's kunnen worden geschreven zonder medeweten van de daadwerkelijke gegevenslocatie. Bovendien kunt u uw gegevens repliceren over meerdere knooppunten. Merk ook op dat Mnesia in hetzelfde BEAM-exemplaar als alle andere code wordt uitgevoerd.

Aangezien Mnesia een Erlang-module is, zou je er toegang toe moeten hebben met behulp van een atoom:

: mnesia

Hoewel het mogelijk is om een ​​alias als deze te maken:

alias: mnesia, as: Mnesia

Gegevens in Mnesia zijn georganiseerd in tafels die hun eigen namen hebben weergegeven als atomen (wat erg lijkt op ETS). De tabellen kunnen een van de volgende typen hebben:

  • : set-het standaardtype. U kunt niet meerdere rijen hebben met exact dezelfde primaire sleutel (we zullen zo meteen zien hoe u een primaire sleutel definieert). De rijen worden niet op een bepaalde manier besteld.
  • : ordered_set-hetzelfde als : set, maar de gegevens worden gesorteerd op de primaire sleutel. Later zullen we zien dat sommige leesbewerkingen zich anders gedragen : ordered_set tafels.
  • :zak-meerdere rijen kunnen dezelfde sleutel hebben, maar de rijen kunnen nog steeds niet volledig identiek zijn.

Tabellen hebben andere eigenschappen die kunnen worden gevonden in de officiële documenten (we zullen enkele in de volgende sectie bespreken). Voordat we echter beginnen met het maken van tabellen, hebben we een schema nodig, dus laten we doorgaan naar de volgende sectie en er een toevoegen.

Een schema en tabellen maken

Om een ​​nieuw schema te maken, zullen we een methode gebruiken met een nogal niet-verrassende naam: create_schema / 1. Kortom, het gaat een nieuwe database voor ons maken op een schijf. Het accepteert een knooppunt als argument:

: Mnesia.create_schema ([knooppunt ()])

Een knooppunt is een Erlang VM die de communicatie, het geheugen en andere dingen verwerkt. Knooppunten kunnen met elkaar verbonden worden en ze zijn niet beperkt tot één pc - u kunt ook via internet verbinding maken met andere knooppunten.

Nadat u de bovenstaande code hebt uitgevoerd, wordt een nieuwe map genoemd Mnesia.nonode@nohost zal worden gemaakt die uw database zal bevatten. nonode @ nohost is de naam van het knooppunt hier. Voordat we echter tabellen kunnen maken, moet Mnesia worden gestart. Dit is zo simpel als het oproepen van de start / 0 functie:

: Mnesia.start ()

Mnesia moet worden gestart op alle deelnemende knooppunten, die normaal gesproken een map hebben waar de bestanden naartoe worden geschreven (in ons geval is deze map genaamd Mnesia.nonode@nohost). Alle knooppunten waaruit het Mnesia-systeem bestaat, worden naar het schema geschreven en later kunt u afzonderlijke knooppunten toevoegen of verwijderen. Bovendien wisselen knooppunten bij het starten schema-informatie uit om te zorgen dat alles in orde is.

Als Mnesia met succes is gestart, :OK atoom zal als resultaat worden geretourneerd. U kunt het systeem later stoppen door te bellen stop / 0:

: mnesia.stop () # =>: gestopt

Nu kunnen we een nieuwe tabel maken. Op zijn minst moeten we de naam en een lijst met kenmerken voor de records opgeven (denk aan hen als kolommen):

: mnesia.create_table (: user, [attributes: [: id,: name,: surname]]) # => : atomic,: ok

Als het systeem niet actief is, wordt de tabel niet gemaakt en een : afgebroken, : node_not_running,: nonode @ nohost fout zal worden teruggegeven. Ook als de tabel al bestaat, krijgt u een : afgebroken, : already_exists,: user fout.

Dus onze nieuwe tafel wordt genoemd :gebruiker, en het heeft drie attributen: :ID kaart, :naam, en :achternaam. Merk op dat het eerste attribuut in de lijst altijd als de primaire sleutel wordt gebruikt, en we kunnen het gebruiken om snel naar een record te zoeken. Later in het artikel zullen we zien hoe complexe query's kunnen worden geschreven en secundaire indexen kunnen worden toegevoegd.

Vergeet ook niet dat het standaardtype voor de tabel is : set, maar dit kan vrij gemakkelijk worden veranderd:

: mnesia.create_table (: user, [attributes: [: id,: name,: surname], type:: bag])

U kunt zelfs uw tabel alleen-lezen maken door de : access_mode naar :alleen lezen:

: mnesia.create_table (: user, [attributes: [: id,: name,: surname], type:: bag, access_mode: read_only])

Nadat het schema en de tabel zijn gemaakt, krijgt de map een schema.DAT bestand evenals sommige .logboek bestanden. Laten we nu naar de volgende sectie gaan en wat gegevens invoegen in onze nieuwe tabel!

Schrijfbewerkingen

Om sommige gegevens in een tabel op te slaan, moet u een functie gebruiken schrijven / 1. Laten we bijvoorbeeld een nieuwe gebruiker toevoegen met de naam John Doe:

: mnesia.write (: user, 1, "John", "Doe")

Houd er rekening mee dat we zowel de naam van de tabel als alle attributen van de gebruiker hebben opgegeven om op te slaan. Probeer de code uit te voeren ... en het faalt jammerlijk met een : afgebroken,: no_transaction fout. Waarom gebeurt dit? Nou, dit komt omdat het schrijven / 1 functie moet worden uitgevoerd in een transactie. Als u om een ​​of andere reden niet wilt vasthouden aan een transactie, kan de schrijfbewerking op een "vuile manier" worden gedaan met dirty_write / 1:

: mnesia.dirty_write (: user, 1, "John", "Doe") # =>: ok

Deze aanpak wordt meestal niet aanbevolen, dus laten we in plaats daarvan een eenvoudige transactie bouwen met behulp van de transactie functie:

: mnesia.transaction (fn ->: mnesia.write (: user, 1, "John", "Doe") end) # => : atomic,: ok

transactie accepteert een anonieme functie die een of meer gegroepeerde bewerkingen heeft. Merk op dat in dit geval het resultaat is : atomic,: ok, niet alleen maar :OK zoals het was met de dirty_write functie. Het belangrijkste voordeel hiervan is dat als er iets misgaat tijdens de transactie, alle bewerkingen worden teruggedraaid.

Eigenlijk is dat een atomiciteitsprincipe, dat zegt dat alle bewerkingen moeten plaatsvinden of dat er geen bewerkingen moeten plaatsvinden in het geval van een fout. Stel dat u bijvoorbeeld uw werknemers hun salarissen betaalt, en plotseling gaat er iets mis. De operatie stopt en je wilt absoluut niet in een situatie belanden waarin sommige werknemers hun salaris kregen en andere niet. Dat is wanneer atomaire transacties echt handig zijn.

De transactie functie kan zoveel schrijfbewerkingen hebben als nodig: 

write_data = fn ->: mnesia.write (: user, 2, "Kate", "Brown"): mnesia.write (: user, 3, "Will", "Smith") end: mnesia.transaction (write_data) # => : atomic,: ok

Interessant is dat gegevens kunnen worden bijgewerkt met behulp van de schrijven functie ook. Geef dezelfde sleutel en nieuwe waarden op voor de andere kenmerken:

update_data = fn ->: mnesia.write (: user, 2, "Kate", "Smith"): mnesia.write (: user, 3, "Will", "Brown") end: mnesia.transaction (gegevens bijwerken)

Merk echter op dat dit niet zal werken voor de tabellen van de :zak type. Omdat dergelijke tabellen toestaan ​​dat meerdere records dezelfde sleutel hebben, zult u eenvoudig twee records bereiken: [: user, 2, "Kate", "Brown", : user, 2, "Kate", "Smith"]. Nog steeds, :zak tabellen laten niet toe dat er volledig identieke records bestaan.

Lees de bewerkingen

Oké, nu we wat gegevens in onze tabel hebben, waarom proberen we ze dan niet te lezen? Net als bij schrijfbewerkingen, kunt u lezen op een "vuile" of "transactionele" manier uitvoeren. De "vuile manier" is natuurlijk eenvoudiger (maar dat is de duistere kant van de Force, Luke!):

: mnesia.dirty_read (: user, 2) # => [: user, 2, "Kate", "Smith"]

Zo dirty_read geeft een lijst met gevonden records terug op basis van de opgegeven sleutel. Als de tafel een is : set of een : ordered_set, de lijst heeft slechts één element. Voor :zak tabellen, de lijst kan natuurlijk meerdere elementen bevatten. Als er geen records zijn gevonden, is de lijst leeg.

Laten we nu proberen dezelfde bewerking uit te voeren, maar de transactionele benadering gebruiken:

read_data = fn ->: mnesia.read (: user, 2) end: mnesia.transaction (read_data) => : atomic, [: user, 2, "Kate", "Brown"]

Super goed!

Zijn er nog andere handige functies voor het lezen van gegevens? Maar natuurlijk! U kunt bijvoorbeeld het eerste of het laatste record van de tabel pakken:

: mnesia.dirty_first (: user) # => 2: mnesia.dirty_last (: user) # => 2

Beide dirty_first en dirty_last hebben hun transactionele tegenhangers, namelijk eerste en laatste, dat moet in een transactie worden ingepakt. Al deze functies retourneren de sleutel van de record, maar merk op dat we in beide gevallen krijgen 2 als een resultaat, ook al hebben we twee records met de toetsen 2 en 3. Waarom gebeurt dit?

Het lijkt erop dat voor de : set en :zak tabellen, de dirty_first en dirty_last (net zoals eerste en laatste) functies zijn synoniemen omdat de gegevens niet in een specifieke volgorde worden gesorteerd. Als u echter een : ordered_set tabel worden de records gesorteerd op hun sleutels en het resultaat zou zijn:

: mnesia.dirty_first (: user) # => 2: mnesia.dirty_last (: user) # => 3

Het is ook mogelijk om de volgende of vorige toets te pakken door te gebruiken dirty_next en dirty_prev (of volgende en prev):

: mnesia.dirty_next (: user, 2) => 3: mnesia.dirty_next (: user, 3) =>: "$ end_of_table"

Als er geen records meer zijn, een speciaal atoom : "$ End_of_table" wordt teruggestuurd. Ook als de tabel een is : set of :zak, dirty_next en dirty_prev zijn synoniemen.

Ten slotte kunt u alle sleutels van een tabel gebruiken door te gebruiken dirty_all_keys / 1 of all_keys / 1:

: mnesia.dirty_all_keys (: user) # => [3, 2]

Bewerkingen verwijderen

Als u een record uit een tabel wilt verwijderen, gebruikt u dirty_delete of verwijderen:

: mnesia.dirty_delete (: user, 2) # =>: ok

Dit gaat alle records verwijderen met een bepaalde sleutel.

Op dezelfde manier kunt u de hele tabel verwijderen:

: Mnesia.delete_table (: user)

Er is geen "vuile" tegenhanger voor deze methode. Het is duidelijk dat nadat een tabel is verwijderd, je er niets op kunt schrijven, en een : aborted, : no_exists,: user fout zal worden teruggegeven.

Tenslotte, als je echt in een verwijderingsstemming bent, kan het hele schema worden verwijderd door te gebruiken delete_schema / 1:

: Mnesia.delete_schema ([knooppunt ()])

Deze bewerking retourneert een : error, 'Mnesia wordt niet overal gestopt', [: nonode @ nohost] fout als Mnesia niet wordt gestopt, dus vergeet dit niet te doen:

: mnesia.stop (): mnesia.delete_schema ([node ()])

Complexere leesbewerkingen

Nu we de basis van het werken met Mnesia hebben gezien, laten we een beetje dieper graven en zien hoe we geavanceerde query's schrijven. Ten eerste zijn er match_object en dirty_match_object functies die kunnen worden gebruikt om naar een record te zoeken op basis van een van de opgegeven kenmerken:

: mnesia.dirty_match_object (: user,: _, "Kate", "Brown") # => [: user, 2, "Kate", "Brown"]

De attributen waar u niet om geeft zijn gemarkeerd met de : _ atoom. U mag alleen de achternaam instellen, bijvoorbeeld:

: mnesia.dirty_match_object (: user,: _,: _, "Brown") # => [: user, 2, "Kate", "Brown"]

U kunt ook aangepaste zoekcriteria gebruiken met kiezen en dirty_select. Om dit in actie te zien, laten we eerst de tabel vullen met de volgende waarden:

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") end: mnesia.transaction (write_data)

Wat ik nu wil doen is alle records vinden die er zijn Zullen als de naam en waarvan de sleutels kleiner zijn dan 5, wat betekent dat de resulterende lijst alleen "Will Smith" en "Will Smoth" zou moeten bevatten. Hier is de bijbehorende code:

: mnesia.dirty_select (: user, [: user,: "$ 1",: "$ 2",: "$ 3", [:<, :"$1", 5, :==, :"$2", "Will" ], [:"$$"] ] ) # => [[3, "Will", "Smith"], [4, "Will", "Smoth"]]

Het gaat hier wat ingewikkelder, dus laten we dit fragment stap voor stap bespreken.

  • Ten eerste hebben we de : gebruiker,: "$ 1",: "$ 2",: "$ 3" een deel. Hier geven we de tabelnaam en een lijst met positionele parameters. Ze moeten in deze vreemd uitziende vorm worden geschreven, zodat we ze later kunnen gebruiken. $ 1 komt overeen met de :ID kaart, $ 2 is de naam, en $ 3 is de achternaam.
  • Vervolgens is er een lijst met bewakingsfuncties die op de gegeven parameters moeten worden toegepast. :<, :"$1", 5 betekent dat we alleen de records willen selecteren waarvan het kenmerk is gemarkeerd als $ 1 (dat is, :ID kaart) is minder dan 5: ==,: "$ 2", "Will", op zijn beurt betekent dat we de records selecteren met de :naam ingesteld op "Zullen".
  • tenslotte, [: "$$"] betekent dat we alle velden in het resultaat willen opnemen. Je zou zeggen [: "$ 2"] om alleen de naam weer te geven. Merk overigens op dat het resultaat een lijst met lijsten bevat: [[3, "Will", "Smith"], [4, "Will", "Smoth"]].

U kunt ook enkele attributen markeren als degene die u niet geeft om het gebruik van de : _ atoom. Laten we bijvoorbeeld de achternaam negeren:

: mnesia.dirty_select (: user, [: user,: "$ 1",: "$ 2",: _, [:<, :"$1", 5, :==, :"$2", "Will" ], [:"$$"] ] ) # => [[3, "Will"], [4, "Will"]]

In dit geval wordt de achternaam echter niet in het resultaat opgenomen.

De tabellen wijzigen

Transformaties uitvoeren

Stel nu dat we onze tabel willen aanpassen door een nieuw veld toe te voegen. Dit kan gedaan worden door de transform_table functie, die de naam van de tabel accepteert, een functie die van toepassing is op alle records en de lijst met nieuwe kenmerken:

: mnesia.transform_table (: user, fn (: user, id, name, surname) -> : user, id, name, surname,: rand.uniform (1000) end, [: id,: name, : achternaam,: salaris])

In dit voorbeeld voegen we een nieuw attribuut toe met de naam :salaris (het wordt verstrekt in het laatste argument). Wat betreft de transformeer functie (het tweede argument), we stellen dit nieuwe attribuut in op een willekeurige waarde. U kunt ook elk ander kenmerk binnen deze transformatiefunctie wijzigen. Dit proces van het wijzigen van de gegevens staat bekend als een "migratie" en dit concept zou bekend moeten zijn bij ontwikkelaars uit de Rails-wereld.

Nu kunt u eenvoudig informatie over de attributen van de tabel pakken met behulp van table_info:

: mnesia.table_info (: user,: attributes) # => [: id,: name,: surname,: salary]

De :salaris kenmerk is daar! En natuurlijk zijn uw gegevens ook aanwezig:

: mnesia.dirty_read (: user, 2) # => [: user, 2, "Kate", "Brown", 778]

U kunt een iets gecompliceerder exemplaar vinden van beide create_table en transform_table functies op de ElixirSchool-website.

Indexen toevoegen

Met Mnesia kunt u elk attribuut indexeren met behulp van de add_table_index functie. Laten we bijvoorbeeld onze maken :achternaam kenmerk geïndexeerd:

: mnesia.add_table_index (: gebruiker,: achternaam) # => : atomic,: ok

Als de index al bestaat, krijgt u een foutmelding : afgebroken, : already_exists,: user, 4.

Zoals de documentatie voor deze functie vermeldt, zijn indexen niet gratis. Met name nemen ze extra ruimte in beslag (in verhouding tot de tabelgrootte) en worden de invoegbewerkingen iets langzamer. Aan de andere kant laten ze je toe om sneller naar de gegevens te zoeken, dus dat is een eerlijke afweging.

U kunt zoeken op een geïndexeerd veld met behulp van de dirty_index_read of index_read functie:

: mnesia.dirty_index_read (: gebruiker, "Smith",: achternaam) # => [: user, 3, "Will", "Smith"]

Hier gebruiken we de secundaire index :achternaam om naar een gebruiker te zoeken. 

Amnesia gebruiken

Het kan enigszins vervelend zijn om rechtstreeks met de Mnesia-module te werken, maar gelukkig is er een pakket van derden genaamd Amnesia (duh!) Waarmee u eenvoudiger handelingen kunt uitvoeren..

U kunt bijvoorbeeld uw database en een tabel als deze definiëren:

gebruik Amnesia defdatabase Demo do deftable User, [: id, autoincrement,: name,: surname,: email], index: [: email] do end end

Dit gaat een database definiëren met de naam demonstratie met een tafel Gebruiker. De gebruiker gaat een naam, een achternaam, een e-mail (een geïndexeerd veld) en een ID (primaire sleutel ingesteld om automatisch in te stellen) een naam geven.

Vervolgens kunt u eenvoudig het schema maken met behulp van de ingebouwde mixtaak:

mix amnesia.create -d Demo - schijf

In dit geval is de database gebaseerd op een schijf, maar er zijn enkele andere beschikbare opties die u kunt instellen. Er is ook een drop-taak die uiteraard de database en alle gegevens vernietigt:

mix amnesia.drop -d Demo

Het is mogelijk om zowel de database als het schema te vernietigen:

mix amnesia.drop -d Demo - schema

Als de database en het schema aanwezig zijn, is het mogelijk om verschillende bewerkingen uit te voeren op basis van de tabel. Maak bijvoorbeeld een nieuw record:

Amnesia.transaction do will_smith =% Gebruiker name: "Will", achternaam: "Smith", email: "[email protected]" |> User.write end

Of krijg een gebruiker op id:

Amnesia.transaction do will_smith = User.read (1) end

Bovendien kunt u een Bericht tabel terwijl een relatie tot de Gebruiker tafel met een gebruikersnaam als een buitenlandse sleutel:

Deftable Message, [: user_id,: content] do end

De tabellen kunnen een aantal hulpfuncties bevatten, bijvoorbeeld om een ​​bericht te maken of om alle berichten te ontvangen:

deftable Gebruiker, [: id, autoincrement,: naam,: achternaam,: e-mail], 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

U kunt nu de gebruiker vinden, een bericht voor hen maken of gemakkelijk al hun berichten vermelden:

Amnesia.transaction do will_smith = User.read (1) will_smith |> User.add_message "hallo!" will_smith |> User.messages end

Heel eenvoudig, toch? Enkele andere gebruiksvoorbeelden zijn te vinden op de officiële website van Amnesia.

Conclusie

In dit artikel hebben we gesproken over het Mnesia-databasebeheersysteem dat beschikbaar is voor Erlang en Elixir. We hebben de belangrijkste concepten van dit DBMS besproken en hebben gezien hoe een schema, database en tabellen kunnen worden gemaakt, evenals alle belangrijke bewerkingen: creëren, lezen, bijwerken en vernietigen. Bovendien hebt u geleerd hoe u met indexen moet werken, hoe u tabellen moet transformeren en hoe u het Amnesia-pakket kunt gebruiken om het werken met databases te vereenvoudigen.

Ik hoop echt dat dit artikel nuttig is en dat je Mnesia ook graag in actie wilt proberen. Zoals altijd bedank ik je dat je bij me bent gebleven, en tot de volgende keer!