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:
La oss komme i gang, skal vi?
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.
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!
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.
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]
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 ()])
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.
: 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
.:<, :"$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"
.[: "$$"]
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.
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.
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.
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.
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!