Fork me on GitHub

Mnesia

Mnesia — это распределённая система управления базами данных реального времени.

Содержание

Обзор

Mnesia — это система управления базами данных (СУБД), поставляемая вместе с Erlang Runtime System. Естественно, мы можем использовать её в Elixir. Благодаря реляционной гибридной объектной модели данных Mnesia подходит для распределённых приложений любого масштаба.

Когда использовать?

Вопрос выбора конкретной технологии зачастую сбивает с толку. Если вы ответите “да” на любой из следующих вопросов, то это хороший знак, что стоит использовать Mnesia, а не ETS или DETS.

Схема

Mnesia — это часть ядра Erlang, а не Elixir, поэтому мы обращаемся к ней через двоеточие (см. урок: Взаимодействие с Erlang):


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

# или если вам по душе стиль Elixir

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

В этом уроке мы будем использовать второй способ при работе с Mnesia API. Mnesia.create_schema/1 инициализирует новую пустую схему и передаёт список нод. В данном случае мы передаём ноду, связанную с нашей IEx-сессией.

Ноды

После выполнения команды Mnesia.create_schema([node()]) в текущей директории вы увидите папку с именем [email protected]. Наверняка, вам интересно, что же значит [email protected], потому что мы не обсуждали это ранее. Давайте посмотрим:

$ 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

Когда мы из командной строки передаём опцию --help в IEx, мы видим список доступных опций. Среди них есть --name и --sname, используемые для изменения информации о нодах. Нода — это виртуальная машина Erlang, управляющая своими связями, сборкой мусора, планированием процессов, памятью и прочим. По умолчанию нода называется [email protected].

$ iex --name [email protected]

Erlang/OTP 20 [erts-8.0.1] [source] [64-bit] [smp:4:4] [async-threads:10] [hipe] [kernel-poll:false] [dtrace]

Interactive Elixir (1.3.4) - press Ctrl+C to exit (type h() ENTER for help)
iex([email protected])> Node.self
:"[email protected]"

Теперь, как вы видите, текущая нода — это атом с именем :"[email protected]". Если мы снова выполним Mnesia.create_schema([node()]), то мы увидим ещё одну папку с именем [email protected]. Цель всего этого довольно проста: ноды в Erlang используются для связи с другими нодами, чтобы разделять информацию и ресурсы. Это работает не только в рамках одной машины, но также через LAN и интернет.

Запуск Mnesia

Мы закончили базовую настройку нашей базы и теперь можем запустить Mnesia с помощью команды Mnesia.start/0.

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

Важно помнить, что при использовании Mnesia на двух и более нодах функцию Mnesia.start/1 надо выполнить на каждой из них.

Создание таблиц

Для создания таблиц используется функция Mnesia.create_table/2. Сейчас мы создадим таблицу Person и передадим ключевой список с её схемой.

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

Мы объявили колонки, используя атомы :id, :name, и :job. После выполнения Mnesia.create_table/2 возвращает один из следующих ответов:

В частности, если таблица уже существует, ответ будет в виде {:already_exists, table}. Таким образом, если мы попробуем пересоздать таблицу, то получим следующее:

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

“Грязный” способ

Сначала рассмотрим “грязный” способ чтения и записи. Как правило, его не используют, потому что он не гарантирует результат. Однако он поможет нам комфортно работать с Mnesia во время обучения. Добавим несколько записей в таблицу 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

Для получения записей воспользуемся 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})
[]

При запросе несуществующей записи Mnesia вернёт пустой список.

Транзакции

Транзакции используются для инкапсуляции чтения и записи в базу. Транзакции — важная часть проектирования отказоустойчивых распределённых систем. Транзакция — механизм, с помощью которого серия операций в базе данных выполняется как единый функциональный блок. Для начала создадим анонимную функцию data_to_write и передадим её в 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}

Судя по ответу, данные были записаны в таблицу Person. Чтобы убедиться, запросим эти данные другой транзакцией. Для чтения мы используем Mnesia.read/1 изнутри анонимной функции.

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"}]}

Для редактирования достаточно вызвать Mnesia.write/1 с ключом уже существующей записи. Например, обновление записи Hans выглядело бы так:

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

Использование индексов

Mnesia поддерживает создание индексов на неключевых колонках и позволяет запрашивать данные на их основе. Например, можно добавить индекс для колонки :job таблицы Person:

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

Возвращаемый результат идентичен результату функции Mnesia.create_table/2:

В частности, если индекс уже существует, ответ будет в виде {:already_exists, table, attribute_index}. Таким образом, если мы попробуем пересоздать индекс, то получим следующее:

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

После успешного создания индекса можно использовать его для чтения данных. Например, получим список директоров:

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

Match и select

Mnesia поддерживает сложные запросы данных из таблиц в виде сопоставления и функций для выборки.

Функция Mnesia.match_object/1 возвращает все записи, соответствующие образцу. Если на колонках существуют индексы, она может использовать их для повышения эффективности запроса. Для колонок, не участвующих в сопоставлении, используется :_.

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

Функция Mnesia.select/2 позволяет составлять запросы с использованием операторов и функций Elixir или Erlang. Например, выберем записи, у которых ключ больше 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"]]}

Разберём по частям. Первый атрибут — таблица Person, второй — кортеж вида {match, [guard], [result]}:

Для более подробной информации можно обратиться к документации Erlang Mnesia для функции select/2.

Инициализация и миграция

У любого ПО наступает момент обновления и миграции существующих данных. Например, во второй версии приложения может понадобиться добавить колонку :age в таблицу Person. Мы не можем пересоздать существующую таблицу Person, но можем её изменить. Для этого мы используем функцию Mnesia.table_info/2, чтобы получить информацию о текущей структуре таблицы, и функцию Mnesia.transform_table/3, чтобы привести её в новый вид.

Нижеприведённый код делает это по следующему принципу:

Функция Mnesia.transform_table/3 принимает имя таблицы, функцию, трансформирующую структуру таблицы, и новый список атрибутов.

iex> 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.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

Поделиться