Conserva tutto con elisir e mnesia

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:

  • Come creare uno schema Mnesia e avviare l'intero sistema.
  • Quali tipi di tabella sono disponibili e come crearli.
  • Come eseguire le operazioni CRUD e qual è la differenza tra le funzioni "dirty" e "transazionale".
  • Come modificare tabelle e aggiungere indici secondari.
  • Come utilizzare il pacchetto Amnesia per semplificare il lavoro con database e tabelle.

Iniziamo, dobbiamo?

Introduzione alla Mnesia

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.

Creazione di uno schema e tabelle

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!

Scrivi operazioni

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.

Leggi le operazioni

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]

Elimina operazioni

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 ()])

Operazioni di lettura più complesse

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.

  • In primo luogo, abbiamo il : 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.
  • Successivamente, c'è una lista di funzioni di guardia che dovrebbero essere applicate ai parametri dati. :<, :"$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à".
  • infine, [: "$$"] 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.

Modifica delle tabelle

Esecuzione di trasformazioni

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.

Aggiunta di indici

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. 

Usando l'amnesia

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.

Conclusione

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!