Oppbevar alt med Elixir og Mnesia

I en av mine tidligere artikler skrev jeg om Erlang Term Storage-tabeller (eller bare ETS), som tillater at tomter med vilkårlig data lagres i minnet. Vi diskuterte også diskbasert ETS (DETS), som gir litt mer begrenset funksjonalitet, men lar deg lagre innholdet i en fil.

Noen ganger kan du imidlertid kreve en enda kraftigere løsning for å lagre dataene. Meet Mnesia-et distribuert databasehåndteringssystem i sanntid introdusert i Erlang. Mnesia har en relasjons / objekt hybrid datamodell og har mange fine funksjoner, inkludert replikering og rask datasøk.

I denne artikkelen vil du lære:

  • Hvordan lage et Mnesia-skjema og starte hele systemet.
  • Hvilke bordtyper er tilgjengelige og hvordan du oppretter dem.
  • Hvordan utføre CRUD operasjoner og hva forskjellen er mellom "skitne" og "transaksjonelle" funksjoner.
  • Hvordan endre tabeller og legge til sekundære indekser.
  • Hvordan bruke Amnesia-pakken for å forenkle arbeidet med databaser og tabeller.

La oss komme i gang, skal vi?

Introduksjon til mesnia

Så, som allerede nevnt ovenfor, er Mnesia en objekt- og relasjonsdatamodell som skalerer seg veldig bra. Den har et DMBS spørrespråk og støtter atomtransaksjoner, akkurat som alle andre populære løsninger (Postgres eller MySQL, for eksempel). Mnesiabordene kan lagres på disk og i minnet, men programmer kan skrives uten kjennskap til den faktiske datalokasjonen. Videre kan du kopiere dataene dine over flere noder. Legg også merke til at Mnesia kjører i samme BEAM-forekomst som all annen kode.

Siden Mnesia er en Erlang-modul, bør du få tilgang til den ved hjelp av et atom:

: mnesia

Selv om det er mulig å opprette et alias som dette:

alias: mesia, som: Mesia

Data i mesnia er organisert i tabeller som har egne navn representert som atomer (som er svært lik ETS). Tabellene kan ha en av følgende typer:

  • :sett-standardtypen. Du kan ikke ha flere rader med nøyaktig samme primærnøkkel (vi får se et øyeblikk hvordan du definerer en primærnøkkel). Rynene blir ikke bestilt på noen bestemt måte.
  • : ordered_set-samme som :sett, men dataene er bestilt av primærnøkkelen. Senere vil vi se at enkelte leseoperasjoner vil oppføre seg annerledes : ordered_set tabeller.
  • :bag-flere rader kan ha samme nøkkel, men radene kan fortsatt ikke være helt identiske.

Tabeller har andre egenskaper som kan bli funnet i de offisielle dokumentene (vi vil diskutere noen av dem i neste avsnitt). Men før vi begynner å lage tabeller, trenger vi et skjema, så la oss fortsette til neste seksjon og legge til en.

Opprette et skjema og tabeller

For å lage et nytt skjema vil vi bruke en metode med et ganske overraskende navn: create_schema / 1. I utgangspunktet skal det opprettes en ny database for oss på en disk. Den aksepterer en node som et argument:

: Mnesia.create_schema ([node ()])

En node er en Erlang VM som håndterer kommunikasjon, minne og andre ting. Noder kan koble til hverandre, og de er ikke begrenset til en PC. Du kan også koble til andre noder via Internett.

Etter at du har kjørt ovenstående kode, en ny katalog som heter Mnesia.nonode@nohost vil bli opprettet som skal inneholde databasen din. nonode @ nohost er nodens navn her. Før vi kan lage noen tabeller, må imidlertid Mnesia startes. Dette er så enkelt som å kalle start / 0 funksjon:

: Mnesia.start ()

Mesia skal startes på alle deltakende noder, hver med normalt en mappe som filene skal skrives til (i vårt tilfelle er denne mappen kalt Mnesia.nonode@nohost). Alle noder som komponerer Mnesia-systemet er skrevet til skjemaet, og senere kan du legge til eller fjerne individuelle noder. Videre bytter nodene ut skjemainformasjon for å sikre at alt er i orden.

Hvis Mnesia startet vellykket, an : ok atom vil bli returnert som et resultat. Du kan senere stoppe systemet ved å ringe stopp / 0:

: mnesia.stop () # =>: stoppet

Nå kan vi lage et nytt bord. I det minste skal vi gi navnet sitt og en liste over attributter for postene (tenk på dem som kolonner):

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

Hvis systemet ikke kjører, blir ikke bordet opprettet og en : avbrutt, : node_not_running,: nonode @ nohost Feil vil bli returnert i stedet. Også, hvis tabellen allerede eksisterer, får du en : aborted : already_exists,: user feil.

Så kalles vårt nye bord :bruker, og den har tre attributter: : id, :Navn, og :etternavn. Vær oppmerksom på at det første attributtet i listen alltid brukes som primærnøkkel, og vi kan bruke den til å raskt søke etter en plate. Senere i artikkelen ser vi hvordan du skriver komplekse søk og legger til sekundære indekser.

Husk også at standardtypen for tabellen er :sett, men dette kan endres ganske enkelt:

: mnesia.create_table (: bruker, [attributter: [: id,: navn,: etternavn], type:: bag])

Du kan til og med gjøre bordet ditt skrivebeskyttet ved å sette inn : access_mode til : Read_only:

: mnesia.create_table (: bruker, [attributter: [: id,: navn,: etternavn], type:: bag, access_mode: read_only])

Etter at skjemaet og bordet er opprettet, vil katalogen ha en schema.DAT fil så vel som noen .Logg filer. La oss nå gå videre til neste del og sette inn noen data i vårt nye bord!

Skriv operasjoner

For å lagre noen data i et bord, må du utnytte en funksjon skrive / 1. For eksempel, la oss legge til en ny bruker som heter John Doe:

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

Merk at vi har angitt både tabellens navn og alle brukerens attributter for å lagre. Prøv å kjøre koden ... og det mislykkes dårlig med en : avbrutt,: no_transaction feil. Hvorfor skjer dette? Vel, dette er fordi skrive / 1 funksjonen skal utføres i en transaksjon. Hvis du av en eller annen grunn ikke vil holde fast ved en transaksjon, kan skriveoperasjonen gjøres på en "skitten måte" med dirty_write / 1:

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

Denne tilnærmingen er vanligvis ikke anbefalt, så la oss i stedet bygge en enkel transaksjon ved hjelp av transaksjon funksjon:

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

transaksjon aksepterer en anonym funksjon som har en eller flere grupperte operasjoner. Merk at i dette tilfellet er resultatet : atomic,: ok, ikke bare : ok som det var med dirty_write funksjon. Hovedfordelen her er at hvis noe går galt under transaksjonen, blir alle operasjoner rullet tilbake.

Faktisk er det et atomprinsipp, som sier at enten alle operasjoner skal oppstå eller det ikke skal oppstå operasjoner i tilfelle en feil. Anta at du for eksempel betaler dine ansatte lønn, og plutselig går noe feil. Operasjonen stopper, og du vil definitivt ikke ende opp i en situasjon da noen ansatte fikk lønn og noen ikke. Det er da atomtransaksjoner er veldig nyttige.

De transaksjon funksjonen kan ha så mange skriveoperasjoner etter behov: 

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

Interessant kan data oppdateres ved hjelp av skrive fungere også. Bare gi samme nøkkel og nye verdier for de andre attributter:

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

Vær imidlertid oppmerksom på at dette ikke kommer til å fungere for tabellene til :bag type. Fordi slike tabeller tillater flere poster å ha samme nøkkel, vil du bare ende opp med to poster: [: bruker, 2, "Kate", "Brown", : bruker, 2, "Kate", "Smith"]. Fortsatt, :bag Tabeller tillater ikke helt identiske poster å eksistere.

Les Operasjoner

OK, nå da vi har noen data i vårt bord, hvorfor prøver vi ikke å lese dem? Akkurat som med skriveoperasjoner, kan du utføre lesing på en "skitten" eller "transaksjonell" måte. Den "skitne veien" er enklere selvfølgelig (men det er den mørke siden av styrken, Luke!):

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

Så dirty_read returnerer en liste over funnet poster basert på den angitte nøkkelen. Hvis bordet er a :sett eller en : ordered_set, listen vil bare ha ett element. Til :bag Tabeller, listen kan selvsagt ha flere elementer. Hvis ingen oppføringer ble funnet, ville listen være tom.

La oss nå prøve å utføre den samme operasjonen, men bruke transaksjonsmetoden:

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

Flott!

Er det noen andre nyttige funksjoner for å lese data? Men selvfølgelig! For eksempel kan du hente den første eller siste posten av bordet:

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

Både dirty_first og dirty_last har sine transaksjonelle kolleger, nemlig først og siste, Det bør pakkes inn i en transaksjon. Alle disse funksjonene returnerer rekordets nøkkel, men merk at i begge tilfeller får vi 2 som et resultat selv om vi har to poster med nøklene 2 og 3. Hvorfor skjer dette?

Det ser ut til at for :sett og :bag bord, dirty_first og dirty_last (i tillegg til først og siste) funksjoner er synonymer fordi dataene ikke er sortert i en bestemt rekkefølge. Hvis du har en : ordered_set bordet blir postene sortert etter nøklene, og resultatet vil være:

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

Det er også mulig å ta tak i neste eller forrige tast ved å bruke dirty_next og dirty_prev (eller neste og prev):

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

Hvis det ikke er flere poster, et spesielt atom : "$ End_of_table" returneres. Også, hvis tabellen er en :sett eller :bag, dirty_next og dirty_prev er synonymer.

Til slutt kan du få alle nøklene fra et bord ved å bruke dirty_all_keys / 1 eller all_keys / 1:

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

Slett operasjoner

For å slette en plate fra et bord, bruk dirty_delete eller slette:

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

Dette kommer til å fjerne alle poster med en gitt nøkkel.

På samme måte kan du fjerne hele tabellen:

: Mnesia.delete_table (: user)

Det er ingen "skitten" motpart for denne metoden. Tydeligvis, etter at et bord er slettet, kan du ikke skrive noe til det, og en : aborted : no_exists,: user Feil vil bli returnert i stedet.

Til slutt, hvis du virkelig er i å slette humør, kan hele skjemaet fjernes ved å bruke delete_schema / 1:

: Mnesia.delete_schema ([node ()])

Denne operasjonen vil returnere a : error, 'Mnesia er ikke stoppet overalt', [: nonode @ nohost] Feil hvis Menneske ikke er stoppet, så ikke glem å gjøre det:

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

Mer komplekse leseoperasjoner

Nå som vi har sett grunnleggende om å jobbe med mesnia, la oss grave litt dypere og se hvordan du skriver avanserte spørringer. Først er det match_object og dirty_match_object funksjoner som kan brukes til å søke etter en plate basert på en av de angitte attributter:

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

Attributtene du ikke bryr deg om, er merket med : _ atom. Du kan bare angi etternavnet, for eksempel:

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

Du kan også gi tilpassede søkekriterier ved hjelp av å velge og dirty_select. For å se dette i aksjon, la oss først fylle tabellen med følgende verdier:

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)

Nå hva jeg vil gjøre er å finne alle postene som har Vil som navnet og hvis nøkler er mindre enn 5, noe som betyr at den resulterende listen bare skal inneholde "Will Smith" og "Will Smoth". Her er den tilsvarende koden:

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

Ting er litt mer komplekse her, så la oss diskutere denne koden trinn for trinn.

  • For det første har vi : user,: "$ 1",: "$ 2",: "$ 3" del. Her gir vi tabellnavnet og en liste over posisjonsparametere. De skal skrives i denne merkelige utseende slik at vi kan bruke dem senere. $ 1 tilsvarer : id, $ 2 er den Navn, og $ 3 er den etternavn.
  • Deretter er det en liste over beskyttelsesfunksjoner som skal brukes på de oppgitte parametrene. :<, :"$1", 5 betyr at vi ønsker å velge bare de postene hvis attributt er merket som $ 1 (det er, : id) er mindre enn 5: ==,: "$ 2", "Vil", i sin tur betyr at vi velger poster med :Navn satt til "Vil".
  • Til slutt, [: "$$"] betyr at vi ønsker å inkludere alle feltene i resultatet. Du kan si [: "$ 2"] for å vise bare navnet. Vær oppmerksom på at resultatet inneholder en liste over lister: [[3, "Will", "Smith"], [4, "Will", "Smoth"]].

Du kan også merke noen attributter som de du ikke bryr deg om å bruke : _ atom. For eksempel, la oss ignorere etternavnet:

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

I dette tilfellet vil imidlertid etternavnet ikke bli inkludert i resultatet.

Endre tabellene

Utfører transformasjoner

Anta nå at vi ønsker å endre vårt bord ved å legge til et nytt felt. Dette kan gjøres ved å bruke transform_table funksjon, som aksepterer tabellens navn, en funksjon som gjelder for alle poster, og listen over nye attributter:

: mnsia.transform_table (: bruker, fn (: bruker, id, navn, etternavn) -> : bruker, id, navn, etternavn: rand.uniform (1000) : etternavn, lønn])

I dette eksemplet legger vi til et nytt attributt som heter :lønn (den er gitt i det siste argumentet). Når det gjelder transformere funksjon (det andre argumentet), setter vi dette nye attributtet til en tilfeldig verdi. Du kan også endre andre attributter i denne transformasjonsfunksjonen. Denne prosessen med å endre data er kjent som en "migrasjon", og dette konseptet bør være kjent for utviklere som kommer fra Rails-verdenen.

Nå kan du bare få tak i informasjon om tabellens attributter ved å bruke table_info:

: mnesia.table_info (: user,: attributes) # => [: id,: navn,: etternavn,: lønn]

De :lønn attributtet er der! Og selvfølgelig er dine data også på plass:

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

Du kan finne et litt mer komplisert eksempel på å bruke begge create_table og transform_table Fungerer på ElixirSchools nettside.

Legge til indekser

Mnesi lar deg gjøre et hvilket som helst attributt indeksert ved å bruke add_table_index funksjon. For eksempel, la oss lage vår :etternavn attributt indeksert:

: mnesia.add_table_index (: bruker,: etternavn) # => : atomic,: ok

Hvis indeksen allerede eksisterer, får du en feil : aborted : already_exists,: user, 4.

Som dokumentasjonen for denne funksjonen står, kommer indekser ikke gratis. Spesielt tar de ekstra plass (proporsjonalt med tabellstørrelsen) og gjør innsatsoperasjonen litt langsommere. På den annen side gir de deg mulighet til å søke etter dataene raskere, så det er en fair trade-off.

Du kan søke etter et indeksert felt ved hjelp av enten dirty_index_read eller index_read funksjon:

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

Her bruker vi den sekundære indeksen :etternavn å søke etter en bruker. 

Bruke Amnesia

Det kan være litt kjedelig å jobbe med Mnesia-modulen direkte, men heldigvis er det en tredjeparts pakke kalt Amnesia (duh!) Som lar deg utføre trivielle operasjoner med større letthet.

For eksempel kan du definere databasen og et bord som dette:

bruk Amnesia defdatabase Demo gjør deftable Bruker, [: id, autoincrement,: navn,: etternavn, e-post], indeks: [: email]

Dette skal definere en database som heter Demo med et bord Bruker. Brukeren skal navngi et navn, etternavn, e-post (et indeksert felt) og et id (primærnøkkel satt til autoincrement).

Deretter kan du enkelt lage skjemaet ved hjelp av den innebygde blandingsoppgaven:

bland amnesia.create -d Demo - disk

I dette tilfellet vil databasen være en diskbasert, men det finnes noen tilgjengelige alternativer du kan sette. Også det er en drop-oppgave som åpenbart vil ødelegge databasen og alle dataene:

bland amnesia.drop -d Demo

Det er mulig å ødelegge både databasen og skjemaet:

bland amnesia.drop -d Demo - skjema

Å ha databasen og skjemaet på plass, er det mulig å utføre ulike operasjoner mot bordet. For eksempel, opprett en ny post:

Amnesia.transaction do will_smith =% Bruker navn: "Vil", etternavn: "Smith", e-post: "[email protected]" |> User.write avslutte

Eller få en bruker etter id:

Amnesia.transaction gjør will_smith = User.read (1) end

Videre kan du definere en Budskap bord mens du etablerer et forhold til Bruker bord med a bruker-ID som fremmed nøkkel:

deftable Message, [: user_id,: content] slutter

Tabellene kan ha en mengde hjelpefunksjoner inne, for eksempel for å opprette en melding eller få alle meldingene:

deftable User, [: id, autoincrement,: navn,: etternavn,: e-post], indeks: [: email] do def add_message (selv, innhold) gjør% Melding user_id: self.id, content: content | > Message.write end def messages (selv) gjør Message.read (self.id) slutten

Du kan nå finne brukeren, opprette en melding til dem, eller liste alle sine meldinger med letthet:

Amnesia.transaction gjør will_smith = User.read (1) will_smith |> User.add_message "hei!" will_smith |> User.messages end

Ganske enkelt, ikke sant? Noen andre brukseksempler finnes på Amnesias offisielle nettside.

Konklusjon

I denne artikkelen snakket vi om databasebehandlingssystemet Mnesia tilgjengelig for Erlang og Elixir. Vi har diskutert hovedbegrepene til dette DBMS og har sett hvordan du lager et skjema, en database og en tabell, samt å utføre alle større operasjoner: opprette, lese, oppdatere og ødelegge. I tillegg har du lært hvordan du arbeider med indekser, hvordan du forvandler tabeller, og hvordan du bruker Amnesia-pakken for å forenkle arbeidet med databaser.

Jeg håper virkelig denne artikkelen var nyttig, og du er ivrig etter å prøve Mnesia også. Som alltid takker jeg deg for at du bodde hos meg, og til neste gang!