In uno dei miei articoli precedenti ho scritto sulle tabelle Erlang Term Storage (o semplicemente ETS), che consentono di memorizzare in memoria tuple di dati arbitrari. Abbiamo anche discusso di ETS (DETS) basati su disco, che forniscono funzionalità leggermente più limitate, ma ti consentono di salvare i tuoi contenuti in un file.
A volte, tuttavia, potrebbe essere necessaria una soluzione ancora più potente per archiviare i dati. Incontra Mnesia, un sistema di gestione di database distribuito in tempo reale inizialmente introdotto in Erlang. Mnesia ha un modello di dati ibrido relazionale / oggetto e ha molte caratteristiche interessanti, inclusa la replicazione e ricerche veloci di dati.
In questo articolo imparerai:
Iniziamo, dobbiamo?
Quindi, come già accennato sopra, Mnesia è un modello di dati oggetto e relazionale che scala molto bene. Ha un linguaggio di query DMBS e supporta le transazioni atomiche, proprio come qualsiasi altra soluzione popolare (Postgres o MySQL, per esempio). Le tabelle di Mnesia possono essere archiviate su disco e in memoria, ma i programmi possono essere scritti all'insaputa dell'attuale posizione dei dati. Inoltre, è possibile replicare i dati su più nodi. Si noti inoltre che Mnesia viene eseguito nella stessa istanza BEAM di tutti gli altri codici.
Dato che Mnesia è un modulo di Erlang, dovresti accedervi usando un atomo:
: mnesia
Sebbene sia possibile creare un alias come questo:
alias: mnesia, come: Mnesia
I dati in Mnesia sono organizzati in tavoli che hanno i loro nomi rappresentati come atomi (che è molto simile a ETS). Le tabelle possono avere uno dei seguenti tipi:
:impostato
-il tipo predefinito. Non puoi avere più righe con esattamente la stessa chiave primaria (vedremo tra breve come definire una chiave primaria). Le righe non vengono ordinate in alcun modo particolare.: ordered_set
-uguale a :impostato
, ma i dati sono ordinati dalla chiave primaria. Più avanti vedremo che alcune operazioni di lettura si comportano diversamente con : ordered_set
tavoli.:Borsa
-più righe possono avere la stessa chiave, ma le righe non possono ancora essere completamente identiche.Le tabelle hanno altre proprietà che possono essere trovate nei documenti ufficiali (ne discuteremo alcuni nella prossima sezione). Tuttavia, prima di iniziare a creare tabelle, abbiamo bisogno di uno schema, quindi passiamo alla sezione successiva e ne aggiungiamo uno.
Per creare un nuovo schema, useremo un metodo con un nome abbastanza sorprendente: CREATE_SCHEMA / 1
. Fondamentalmente, creerà un nuovo database per noi su un disco. Accetta un nodo come argomento:
: Mnesia.create_schema ([node ()])
Un nodo è un VM Erlang che gestisce le sue comunicazioni, la memoria e altre cose. I nodi possono connettersi tra loro e non sono limitati a un solo PC: è possibile connettersi anche ad altri nodi tramite Internet.
Dopo aver eseguito il codice sopra, una nuova directory chiamata Mnesia.nonode@nohost verrà creato che conterrà il tuo database. nonode @ nohost è il nome del nodo qui. Prima di poter creare qualsiasi tabella, tuttavia, Mnesia deve essere avviato. Questo è semplice come chiamare il avviare / 0
funzione:
: Mnesia.start ()
Mnesia dovrebbe essere avviato su tutti i nodi partecipanti, ognuno dei quali ha normalmente una cartella in cui verranno scritti i file (nel nostro caso, questa cartella è chiamata Mnesia.nonode@nohost). Tutti i nodi che compongono il sistema Mnesia vengono scritti nello schema e in seguito è possibile aggiungere o rimuovere singoli nodi. Inoltre, all'avvio, i nodi scambiano le informazioni sullo schema per assicurarsi che tutto sia a posto.
Se Mnesia ha iniziato con successo, a :ok
atomo verrà restituito come risultato. In seguito, è possibile interrompere il sistema chiamando interrompere / 0
:
: mnesia.stop () # =>: interrotto
Ora possiamo creare una nuova tabella. Per lo meno, dovremmo fornire il suo nome e un elenco di attributi per i record (pensateli come colonne):
: mnesia.create_table (: user, [attributes: [: id,: nome,: cognome]]) # => : atomic,: ok
Se il sistema non è in esecuzione, la tabella non verrà creata e un : aborted, : node_not_running,: nonode @ nohost
l'errore verrà invece restituito. Inoltre, se la tabella esiste già, otterrai un : aborted, : already_exists,: user
errore.
Così viene chiamato il nostro nuovo tavolo :utente
, e ha tre attributi: : id
, :nome
, e :cognome
. Si noti che il primo attributo nell'elenco viene sempre utilizzato come chiave primaria e che possiamo utilizzarlo per cercare rapidamente un record. Più avanti nell'articolo, vedremo come scrivere query complesse e aggiungere indici secondari.
Inoltre, ricorda che il tipo predefinito per la tabella è :impostato
, ma questo può essere cambiato abbastanza facilmente:
: mnesia.create_table (: user, [attributes: [: id,: nome,: cognome], tipo:: bag])
Puoi persino rendere il tuo tavolo di sola lettura impostando il : access_mode
a :sola lettura:
: mnesia.create_table (: user, [attributes: [: id,: nome,: cognome], tipo:: bag, access_mode: read_only])
Dopo aver creato lo schema e la tabella, la directory avrà a schema.DAT file e alcuni .ceppo File. Passiamo ora alla sezione successiva e inseriamo alcuni dati nella nostra nuova tabella!
Per memorizzare alcuni dati in una tabella, è necessario utilizzare una funzione scrivere / 1
. Ad esempio, aggiungiamo un nuovo utente di nome John Doe:
: mnesia.write (: user, 1, "John", "Doe")
Nota che abbiamo specificato sia il nome della tabella che tutti gli attributi dell'utente da memorizzare. Prova a eseguire il codice ... e fallisce miseramente con un : aborted,: no_transaction
errore. Perché sta succedendo? Bene, questo è perché il scrivere / 1
la funzione deve essere eseguita in una transazione. Se, per qualche motivo, non si desidera attenersi a una transazione, l'operazione di scrittura può essere eseguita in "modo sporco" utilizzando dirty_write / 1
:
: mnesia.dirty_write (: user, 1, "John", "Doe") # =>: ok
Questo approccio di solito non è raccomandato, quindi, invece, costruiamo una semplice transazione con l'aiuto di transazione
funzione:
: mnesia.transaction (fn ->: mnesia.write (: user, 1, "John", "Doe") end) # => : atomic,: ok
transazione
accetta una funzione anonima che ha una o più operazioni raggruppate. Nota che in questo caso il risultato è : atomic,: ok
, non solo :ok
come era con il dirty_write
funzione. Il vantaggio principale qui è che se qualcosa va storto durante la transazione, tutte le operazioni vengono ripristinate.
In realtà, questo è un principio di atomicità, che dice che tutte le operazioni dovrebbero verificarsi o che nessuna operazione dovrebbe verificarsi in caso di errore. Supponiamo, ad esempio, di pagare i salari ai dipendenti e improvvisamente qualcosa va storto. L'operazione si interrompe e sicuramente non vuoi finire in una situazione in cui alcuni impiegati hanno i loro stipendi e altri no. Ecco quando le transazioni atomiche sono davvero utili.
Il transazione
la funzione può avere tutte le operazioni di scrittura necessarie:
write_data = fn ->: mnesia.write (: user, 2, "Kate", "Brown"): mnesia.write (: user, 3, "Will", "Smith") end: mnesia.transaction (write_data) # => : atomic,: ok
È interessante notare che i dati possono essere aggiornati utilizzando il Scrivi
funzione pure. Fornisci la stessa chiave e nuovi valori per gli altri attributi:
update_data = fn ->: mnesia.write (: user, 2, "Kate", "Smith"): mnesia.write (: user, 3, "Will", "Brown") end: mnesia.transaction (update_data)
Si noti, tuttavia, che questo non funzionerà per le tabelle di :Borsa
genere. Poiché tali tabelle consentono a più record di avere la stessa chiave, ti ritroverai semplicemente con due record: [: utente, 2, "Kate", "Brown", : user, 2, "Kate", "Smith"]
. Ancora, :Borsa
le tabelle non consentono l'esistenza di record completamente identici.
Bene, ora che abbiamo alcuni dati nella nostra tabella, perché non proviamo a leggerli? Proprio come con le operazioni di scrittura, è possibile eseguire la lettura in modo "sporco" o "transazionale". Il "modo sporco" è ovviamente più semplice (ma questo è il lato oscuro della Forza, Luca!):
: mnesia.dirty_read (: user, 2) # => [: user, 2, "Kate", "Smith"]
Così dirty_read
restituisce un elenco di record trovati in base alla chiave fornita. Se il tavolo è un :impostato
o un : ordered_set
, la lista avrà solo un elemento. Per :Borsa
tabelle, la lista può, ovviamente, avere più elementi. Se non sono stati trovati record, l'elenco sarebbe vuoto.
Ora proviamo a eseguire la stessa operazione ma usando l'approccio transazionale:
read_data = fn ->: mnesia.read (: user, 2) end: mnesia.transaction (read_data) => : atomic, [: user, 2, "Kate", "Brown"]
grande!
Ci sono altre utili funzioni per leggere i dati? Ma certo! Ad esempio, puoi prendere il primo o l'ultimo record della tabella:
: mnesia.dirty_first (: user) # => 2: mnesia.dirty_last (: user) # => 2
Tutti e due dirty_first
e dirty_last
hanno le loro controparti transazionali, vale a dire primo
e scorso
, che dovrebbe essere incluso in una transazione. Tutte queste funzioni restituiscono la chiave del record, ma si noti che in entrambi i casi si ottiene 2
di conseguenza anche se abbiamo due record con le chiavi 2
e 3
. Perché sta succedendo?
Sembra che per il :impostato
e :Borsa
tavoli, il dirty_first
e dirty_last
(così come primo
e scorso
) Le funzioni sono sinonimi perché i dati non sono ordinati in alcun ordine specifico. Se, tuttavia, hai un : ordered_set
tabella, i record verranno ordinati in base alle loro chiavi e il risultato sarà:
: mnesia.dirty_first (: user) # => 2: mnesia.dirty_last (: user) # => 3
È anche possibile afferrare la chiave successiva o precedente usando dirty_next
e dirty_prev
(o Il prossimo
e prev
):
: mnesia.dirty_next (: user, 2) => 3: mnesia.dirty_next (: user, 3) =>: "$ end_of_table"
Se non ci sono più record, un atomo speciale : "$ End_of_table"
viene restituito. Inoltre, se il tavolo è a :impostato
o :Borsa
, dirty_next
e dirty_prev
sono sinonimi.
Infine, puoi ottenere tutte le chiavi da una tabella usando dirty_all_keys / 1
o all_keys / 1
:
: mnesia.dirty_all_keys (: user) # => [3, 2]
Per eliminare un record da una tabella, utilizzare dirty_delete
o Elimina
:
: mnesia.dirty_delete (: user, 2) # =>: ok
Questo sta per rimuovere tutti i record con una determinata chiave.
Allo stesso modo, puoi rimuovere l'intera tabella:
: Mnesia.delete_table (: utente)
Non esiste una controparte "sporca" per questo metodo. Ovviamente, dopo che una tabella è stata cancellata, non è possibile scrivere nulla ad essa e un : aborted, : no_exists,: user
l'errore verrà invece restituito.
Infine, se sei davvero in uno stato d'animo di eliminazione, l'intero schema può essere rimosso usando delete_schema / 1
:
: Mnesia.delete_schema ([node ()])
Questa operazione restituirà a : error, 'Mnesia non viene fermato ovunque', [: nonode @ nohost]
errore se Mnesia non viene fermato, quindi non dimenticare di farlo:
: mnesia.stop (): mnesia.delete_schema ([node ()])
Ora che abbiamo visto le basi per lavorare con Mnesia, scaviamo un po 'più a fondo e vediamo come scrivere query avanzate. In primo luogo, ci sono match_object
e dirty_match_object
funzioni che possono essere utilizzate per cercare un record basato su uno degli attributi forniti:
: mnesia.dirty_match_object (: user,: _, "Kate", "Brown") # => [: user, 2, "Kate", "Brown"]
Gli attributi che non ti interessano sono contrassegnati con : _
atomo. Puoi impostare solo il cognome, ad esempio:
: mnesia.dirty_match_object (: user,: _,: _, "Brown") # => [: user, 2, "Kate", "Brown"]
Puoi anche fornire criteri di ricerca personalizzati usando selezionare
e dirty_select
. Per vedere questo in azione, in primo luogo compilare la tabella con i seguenti valori:
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)
Ora quello che voglio fare è trovare tutti i record che hanno Volontà
come il nome e le cui chiavi sono inferiori a 5
, il che significa che la lista risultante dovrebbe contenere solo "Will Smith" e "Will Smoth". Ecco il codice corrispondente:
: mnesia.dirty_select (: utente, [: utente,: "$ 1",: "$ 2",: "$ 3", [:<, :"$1", 5, :==, :"$2", "Will" ], [:"$$"] ] ) # => [[3, "Will", "Smith"], [4, "Will", "Smoth"]]
Le cose sono un po 'più complesse qui, quindi discutiamo questo frammento passo dopo passo.
: user,: "$ 1",: "$ 2",: "$ 3"
parte. Qui stiamo fornendo il nome della tabella e un elenco di parametri posizionali. Dovrebbero essere scritti in questa strana forma in modo che possiamo utilizzarli in seguito. $ 1
corrisponde al : id
, $ 2
è il nome
, e $ 3
è il cognome
.:<, :"$1", 5
significa che vorremmo selezionare solo i record il cui attributo contrassegnato come $ 1
(questo è, : id
) è meno di 5
. : ==,: "$ 2", "Will"
, a sua volta, significa che stiamo selezionando i record con il :nome
impostato "Volontà"
.[: "$$"]
significa che vorremmo includere tutti i campi nel risultato. Potresti dire [: "$ 2"]
per visualizzare solo il nome. Nota, a proposito, che il risultato contiene un elenco di liste: [[3, "Will", "Smith"], [4, "Will", "Smoth"]]
.Puoi anche contrassegnare alcuni attributi come quelli a cui non ti interessa usare il : _
atomo. Ad esempio, ignoriamo il cognome:
: mnesia.dirty_select (: user, [: user,: "$ 1",: "$ 2",: _, [::<, :"$1", 5, :==, :"$2", "Will" ], [:"$$"] ] ) # => [[3, "Will"], [4, "Will"]]
In questo caso, tuttavia, il cognome non sarà incluso nel risultato.
Supponiamo ora che vorremmo modificare la nostra tabella aggiungendo un nuovo campo. Questo può essere fatto usando il transform_table
funzione, che accetta il nome della tabella, una funzione da applicare a tutti i record e l'elenco dei nuovi attributi:
: mnesia.transform_table (: user, fn (: user, id, name, cognome) -> : user, id, name, cognome,: rand.uniform (1000) end, [: id,: name, : cognome,: salario])
In questo esempio stiamo aggiungendo un nuovo attributo chiamato :stipendio
(è fornito nell'ultimo argomento). Per quanto riguarda la trasformare la funzione (il secondo argomento), stiamo impostando questo nuovo attributo su un valore casuale. È inoltre possibile modificare qualsiasi altro attributo all'interno di questa funzione di trasformazione. Questo processo di modifica dei dati è noto come "migrazione" e questo concetto dovrebbe essere familiare agli sviluppatori che provengono dal mondo Rails.
Ora puoi semplicemente prendere informazioni sugli attributi della tabella usando table_info
:
: mnesia.table_info (: user,: attributes) # => [: id,: nome,: cognome,: stipendio]
Il :stipendio
l'attributo è lì! E, naturalmente, i tuoi dati sono anche a posto:
: mnesia.dirty_read (: user, 2) # => [: user, 2, "Kate", "Brown", 778]
È possibile trovare un esempio leggermente più complesso di utilizzo di entrambi crea tabella
e transform_table
funzioni sul sito Web di ElixirSchool.
Mnesia ti permette di rendere qualsiasi attributo indicizzato usando il add_table_index
funzione. Ad esempio, facciamo il nostro :cognome
attributo indicizzato:
: mnesia.add_table_index (: user,: cognome) # => : atomic,: ok
Se l'indice esiste già, si verificherà un errore : aborted, : already_exists,: user, 4
.
Come afferma la documentazione di questa funzione, gli indici non vengono offerti gratuitamente. Nello specifico, occupano uno spazio aggiuntivo (proporzionale alle dimensioni della tabella) e rendono le operazioni di inserimento un po 'più lente. D'altra parte, ti permettono di cercare i dati più velocemente, quindi è un giusto compromesso.
Puoi cercare per un campo indicizzato usando il dirty_index_read
o index_read
funzione:
: mnesia.dirty_index_read (: user, "Smith",: cognome) # => [: user, 3, "Will", "Smith"]
Qui stiamo usando l'indice secondario :cognome
per cercare un utente.
Potrebbe essere un po 'noioso lavorare direttamente con il modulo Mnesia, ma per fortuna c'è un pacchetto di terze parti chiamato Amnesia (duh!) Che ti permette di eseguire operazioni banali con maggiore facilità.
Ad esempio, puoi definire il tuo database e una tabella come questa:
usa Amnesia defdatabase Demo do deftable Utente, [: id, autoincrement,: nome,: cognome,: email], indice: [: email] do end end
Questo definirà un database chiamato dimostrazione
con un tavolo Utente
. L'utente chiamerà un nome, un cognome, un'e-mail (un campo indicizzato) e un id (chiave primaria impostata per l'autoincremento).
Successivamente, è possibile creare facilmente lo schema utilizzando l'attività di missaggio integrata:
mix amnesia.create -d Demo --disk
In questo caso, il database sarà basato su disco, ma ci sono alcune altre opzioni disponibili che è possibile impostare. Inoltre c'è un task di drop che, ovviamente, distruggerà il database e tutti i dati:
mix amnesia.drop -d Demo
È possibile distruggere sia il database che lo schema:
mix amnesia.drop -d Demo --schema
Avendo il database e lo schema in atto, è possibile eseguire varie operazioni sulla tabella. Ad esempio, crea un nuovo record:
Amnesia.transaction do will_smith =% Utente nome: "Will", cognome: "Smith", email: "[email protected]" |> User.write fine
Oppure ottieni un utente tramite id:
Amnesia.transaction do will_smith = User.read (1) end
Inoltre, puoi definire a Messaggio
tabella stabilendo una relazione con il Utente
tavolo con a ID utente
come chiave straniera:
Messaggio deftable, [: user_id,: content] do end
Le tabelle possono avere un sacco di funzioni di aiuto all'interno, ad esempio, per creare un messaggio o ottenere tutti i messaggi:
Utente deftable, [: id, autoincrement,: nome,: cognome,: email], indice: [: email] do def add_message (self, content) do% Messaggio user_id: self.id, content: content | > Message.write end def messages (self) do Message.read (self.id) end end
Ora puoi trovare l'utente, creare un messaggio per loro o elencare tutti i loro messaggi con facilità:
Amnesia.transaction do will_smith = User.read (1) will_smith |> User.add_message "ciao!" will_smith |> User.messages end
Abbastanza semplice, no? Alcuni altri esempi di utilizzo possono essere trovati sul sito ufficiale di Amnesia.
In questo articolo, abbiamo parlato del sistema di gestione del database Mnesia disponibile per Erlang ed Elixir. Abbiamo discusso i concetti principali di questo DBMS e abbiamo visto come creare uno schema, un database e tabelle, oltre a eseguire tutte le principali operazioni: creare, leggere, aggiornare e distruggere. Inoltre, hai imparato come lavorare con gli indici, come trasformare le tabelle e come usare il pacchetto Amnesia per semplificare il lavoro con i database.
Spero davvero che questo articolo sia stato utile e tu sei desideroso di provare anche Mnesia in azione. Come sempre, ti ringrazio per essere stato con me, e fino alla prossima volta!