Do you want to pick up from where you left of?
Take me there

Mnesia

Mnesia é um sistema de gestão de banco de dados em tempo real de alto tráfego.

Visão Geral

Mnesia é um sistema de banco de dados (DBMS) que vem acompanhado da Runtime do Erlang, e por isso podemos utilizar naturalmente com Elixir. O modelo de dados híbrido relacional e de objeto do Mnesia é o que o torna adequado para o desenvolvimento de aplicações distribuídas de qualquer escala.

Quando Usar

Quando usar uma determinada peça de tecnologia é muitas vezes um exercício confuso. Se você puder responder “sim” a qualquer uma das seguintes perguntas, então esta é uma boa indicação para usar Mnesia ao invés do ETS ou DETS.

Schema

Como Mnesia faz parte do núcleo do Erlang, ao invés de Elixir, temos que acessá-lo com a sintaxe de dois pontos (Ver lição: Erlang Interoperability) como a seguir:


iex> :mnesia.create_schema([node()])

# or if you prefer the Elixir feel...

iex> alias :mnesia, as: Mnesia
iex> Mnesia.create_schema([node()])

Para esta lição, vamos tomar a última abordagem quando se trabalha com a API Mnesia. Mnesia.create_schema/1 inicializa um novo schema vazio e passa em uma lista de nós. Neste caso, estamos passando o nó associado com a nossa sessão IEx.

Nós

Uma vez que executar o comandoMnesia.create_schema([node()]) via IEx, você deve ver uma pasta chamada Mnesia.nonode@nohost ou similar no seu diretório de trabalho atual. Você pode estar se perguntando o que o nonode@nohost significa já que não nos deparamos com isso antes. Vamos dar uma olhada.

$ iex --help
Usage: iex [options] [.exs file] [data]

  -v                Prints version
  -e "command"      Evaluates the given command (*)
  -r "file"         Requires the given files/patterns (*)
  -S "script"       Finds and executes the given script
  -pr "file"        Requires the given files/patterns in parallel (*)
  -pa "path"        Prepends the given path to Erlang code path (*)
  -pz "path"        Appends the given path to Erlang code path (*)
  --app "app"       Start the given app and its dependencies (*)
  --erl "switches"  Switches to be passed down to Erlang (*)
  --name "name"     Makes and assigns a name to the distributed node
  --sname "name"    Makes and assigns a short name to the distributed node
  --cookie "cookie" Sets a cookie for this distributed node
  --hidden          Makes a hidden node
  --werl            Uses Erlang's Windows shell GUI (Windows only)
  --detached        Starts the Erlang VM detached from console
  --remsh "name"    Connects to a node using a remote shell
  --dot-iex "path"  Overrides default .iex.exs file and uses path instead;
                    path can be empty, then no file will be loaded

** Options marked with (*) can be given more than once
** Options given after the .exs file or -- are passed down to the executed code
** Options can be passed to the VM using ELIXIR_ERL_OPTIONS or --erl

Quando passamos a opção --help para IEx a partir da linha de comando, nos é apresentado todas as opções possíveis. Podemos ver que existe opção --name e --sname para atribuição de informações para nós. Um nó é apenas uma Máquina Virtual do Erlang lidando com suas próprias comunicações, garbage collection, processamento agendado, memória e muito mais. O nó está sendo nomeado como nonode@nohost simplesmente por padrão.

$ iex --name learner@elixirschool.com

Erlang/OTP {{ site.erlang.OTP }} [erts-{{ site.erlang.erts }}] [source] [64-bit] [smp:4:4] [async-threads:10] [hipe] [kernel-poll:false] [dtrace]

Interactive Elixir ({{ site.elixir.version }}) - press Ctrl+C to exit (type h() ENTER for help)
iex(learner@elixirschool.com)> Node.self
:"learner@elixirschool.com"

Como podemos ver agora, o nó que estamos executando é um átomo chamado :"learner@elixirschool.com". Se nós executarmos Mnesia.create_schema([node()]) novamente, nós iremos ver que ela criou uma outra pasta chamada Mnesia.learner@elixirschool.com. O propósito disto é bem simples. Nós em Erlang são usados para conectar a outros nós para compartilhar (distribuir) informação e recursos. Isto não tem que estar limitado a mesma máquina e pode comunicar através de LAN, internet, etc.

Iniciando Mnesia

Agora que temos o conhecimento básico e criação do banco de dados, estamos agora em posição de iniciar o Mnesia DBMS com o comando Mnesia.start/0.

iex> alias :mnesia, as: Mnesia
iex> Mnesia.create_schema([node()])
:ok
iex> Mnesia.start()
:ok

A função Mnesia.start/0 é assíncrona. Ela inicia a inicialização das tabelas existentes e retorna o átomo :ok. Caso seja necessário realizar algumas ações em uma tabela existente logo após iniciar o Mnesia, precisaremos chamar a função Mnesia.wait_for_tables/2. Ela irá suspender o chamador até que as tabelas sejam inicializadas. Veja o exemplo na seção Inicialização de dados e migração.

Vale a pena manter em mente que quando executando um sistema distribuído com dois ou mais nós participando, a função Mnesia.start/1 deve ser executada em todos os nós participantes.

Criando Tabelas

A função Mnesia.create_table/2 é usada para criar tabelas dentro do nosso banco de dados. Abaixo criamos uma tabela chamada Person, e em seguida, passamos uma lista de palavra-chave definindo o schema da tabela.

iex> Mnesia.create_table(Person, [attributes: [:id, :name, :job]])
{:atomic, :ok}

Nós definimos as colunas usando os átomos :id, :name, e :job. O primeiro átomo (neste caso, :id) é a chave primária. Pelo menos um atributo adicional é necessário.

Quando executamos Mnesia.create_table/2, ele irá retornar qualquer uma das seguinte respostas:

Em particular, se a tabela já existir a razão será na forma {:already_exists, table}, e se nós tentarmos criar esta tabela uma segunda vez nós teremos:

iex> Mnesia.create_table(Person, [attributes: [:id, :name, :job]])
{:aborted, {:already_exists, Person}}

A Maneira Suja

Primeiro de tudo, vamos olhar para a maneira suja de leitura e escrita em uma tabela no Mnesia. Isso geralmente deve ser evitado, pois o sucesso não é garantido, mas deve nos ajudar a aprender e tornar-se confortável trabalhando com Mnesia. Vamos adicionar algumas entradas para nossa tabela Person.

iex> Mnesia.dirty_write({Person, 1, "Seymour Skinner", "Principal"})
:ok

iex> Mnesia.dirty_write({Person, 2, "Homer Simpson", "Safety Inspector"})
:ok

iex> Mnesia.dirty_write({Person, 3, "Moe Szyslak", "Bartender"})
:ok

… e para recuperar as entradas podemos usar Mnesia.dirty_read/1:

iex> Mnesia.dirty_read({Person, 1})
[{Person, 1, "Seymour Skinner", "Principal"}]

iex> Mnesia.dirty_read({Person, 2})
[{Person, 2, "Homer Simpson", "Safety Inspector"}]

iex> Mnesia.dirty_read({Person, 3})
[{Person, 3, "Moe Szyslak", "Bartender"}]

iex> Mnesia.dirty_read({Person, 4})
[]

Se nós tentarmos consultar um registro que não existe, Mnesia irá responder com uma lista vazia.

Transações

Tradicionalmente nós usamos transações para encapsular nossas leituras no nosso banco de dados. Transações são uma parte importante para concepção de sistemas altamente distribuídos e tolerantes a falhas. Uma transação no Mnesia é um mecanismo através do qual uma série de operações da base de dados pode ser executada como um bloco funcional. Primeiro criamos uma função anônima, neste caso data_to_write e em seguida, passamos esta função para Mnesia.transaction.

iex> data_to_write = fn ->
...>   Mnesia.write({Person, 4, "Marge Simpson", "home maker"})
...>   Mnesia.write({Person, 5, "Hans Moleman", "unknown"})
...>   Mnesia.write({Person, 6, "Monty Burns", "Businessman"})
...>   Mnesia.write({Person, 7, "Waylon Smithers", "Executive assistant"})
...> end
#Function<20.54118792/0 in :erl_eval.expr/5>

iex> Mnesia.transaction(data_to_write)
{:atomic, :ok}

Com base neste mensagem de transação, podemos seguramente assumir que nós escrevemos os dados para a nossa tabela Person. Vamos usar uma transação para ler a partir do banco de dados agora para ter certeza. Usaremos Mnesia.read/1 para ler a partir do banco de dados, mais uma vez de dentro de uma função anônima.

iex> data_to_read = fn ->
...>   Mnesia.read({Person, 6})
...> end
#Function<20.54118792/0 in :erl_eval.expr/5>

iex> Mnesia.transaction(data_to_read)
{:atomic, [{Person, 6, "Monty Burns", "Businessman"}]}

Note que se você quiser atualizar dados, somente precisa chamar Mnesia.write/1 com a mesma chave de um registro existente. Portanto, para atualizar o registro para Hans, você pode fazer o seguinte:

iex> Mnesia.transaction(
...>   fn ->
...>     Mnesia.write({Person, 5, "Hans Moleman", "Ex-Mayor"})
...>   end
...> )

Usando índices

Mnesia suporta índices em colunas não chaves, e dados podem ser então consultados usando estes índices. Dessa forma podemos adicionar um índice na coluna :job da tabela Person:

iex> Mnesia.add_table_index(Person, :job)
{:atomic, :ok}

O resultado é similar ao retornado por Mnesia.create_table/2:

Em particular, se o índice já existir, a razão será na forma {:already_exists, table, attribute_index}. Assim, se tentarmos adicionar este índice uma segunda vez teremos:

iex> Mnesia.add_table_index(Person, :job)
{:aborted, {:already_exists, Person, 4}}

Uma vez que o índice tenha sido criado com sucesso, nós podemos usá-lo para buscar uma lista de todos os diretores:

iex> Mnesia.transaction(
...>   fn ->
...>     Mnesia.index_read(Person, "Principal", :job)
...>   end
...> )
{:atomic, [{Person, 1, "Seymour Skinner", "Principal"}]}

Combinação e seleção

Mnesia suporta consultas complexas para recuperar dados de uma tabela na forma de combinação e funções de seleção ad-hoc.

A função Mnesia.match_object/1 retorna todos os registros que combinem com o padrão informado. Se qualquer coluna na tabela tiver índices, estes podem ser usados para tornar a busca mais eficiente. Use o átomo especial :_ para identificar colunas que não devem participar da combinação.

iex> Mnesia.transaction(
...>   fn ->
...>     Mnesia.match_object({Person, :_, "Marge Simpson", :_})
...>   end
...> )
{:atomic, [{Person, 4, "Marge Simpson", "home maker"}]}

A função Mnesia.select/2 permite que você especifique uma consulta customizada usando qualquer operador ou função da linguagem Elixir (ou Erlang, para esse efeito). Vamos olhar um exemplo que seleciona todos os registros que contém uma chave que é maior que 3:

iex> Mnesia.transaction(
...>   fn ->
...>     Mnesia.select(Person, [{{Person, :"$1", :"$2", :"$3"}, [{:>, :"$1", 3}], [:"$$"]}])
...>   end
...> )
{:atomic, [[7, "Waylon Smithers", "Executive assistant"], [4, "Marge Simpson", "home maker"], [6, "Monty Burns", "Businessman"], [5, "Hans Moleman", "unknown"]]}

Vamos analisar isto. O primeiro atributo é a tabela, Person, e o segundo atributo é uma tripla da forma {match, [guard], [result]}:

Para mais detalhes, veja a documentação para select/2 do Mnesia Erlang.

Inicialização de dados e migração

A cada evolução de software, virá a hora quando você precisará atualizar o software e migrar os dados armazenados em seu banco de dados. Por exemplo, talvez precisaremos adicionar a coluna :age em nossa tabela Person na v2 da nossa aplicação. Nós não podemos criar a tabela Person uma vez que ela já foi criada, mas podemos transformá-la. Para isso precisamos saber quando transformar, o qual podemos fazer quando estamos criando a tabela. Para fazer isto, podemos usar a função Mnesia.table_info/2 para buscar a estrutura atual da tabela e a função Mnesia.transform_table/3 para transformá-la na nova estrutura.

O código abaixo faz isto através da implementação da seguinte lógica:

Se estivermos executando alguma ação nas tabelas existentes logo após iniciar o Mnesia com Mnesia.start/0, essas tabelas podem não ser inicializadas e acessíveis. Nesse caso, devemos usar a função Mnesia.wait_for_tables/2. Ela suspenderá o processo atual até que as tabelas sejam inicializadas ou até que o tempo limite seja atingido.

A função Mnesia.transform_table/3 recebe como atributos o nome da tabela, a função que transforma o registro do formato antigo para o novo, e a lista de novos atributos.

case Mnesia.create_table(Person, [attributes: [:id, :name, :job, :age]]) do
  {:atomic, :ok} ->
    Mnesia.add_table_index(Person, :job)
    Mnesia.add_table_index(Person, :age)
  {:aborted, {:already_exists, Person}} ->
    case Mnesia.table_info(Person, :attributes) do
      [:id, :name, :job] ->
        Mnesia.wait_for_tables([Person], 5000)
        Mnesia.transform_table(
          Person,
          fn ({Person, id, name, job}) ->
            {Person, id, name, job, 21}
          end,
          [:id, :name, :job, :age]
          )
        Mnesia.add_table_index(Person, :age)
      [:id, :name, :job, :age] ->
        :ok
      other ->
        {:error, other}
    end
end
Caught a mistake or want to contribute to the lesson? Edit this lesson on GitHub!