Мнезия

Мнезия — система управления базами данных (СУБД), разработанная специально для Эрланга и входящая в его стандартную поставку. То же: Mnesia.

Как и любая другая СУБД, Мнезия обеспечивает базовый набор возможных действий с информацией:

Часто эти четыре базовые операции обозначают как акроним CRUD (Create, Read, Update, Delete).

Мнезия работает внутри ERTS, являясь её органичной частью (её нельзя запустить как-то отдельно). Благодаря этой органичности мы можем сохранить в своей базе данных (БД) абсолютно любой терм Эрланга (любой тип данных), например фунтерм или какой-нибудь список кортежей, состоящих из атомов и карт, без дополнительных усилий. Также благодаря этой органичности мы имеем в Эрланге весьма скоростной инструмент по работе с данными.

Отказоустойчивость изначально была главной ценностью при разработке Erlang/OTP, это коснулось и Мнезии. Наша БД может работать в распределённом режиме, на нескольких нодах, находящихся на разных машинах. Если с одной машиной случится что-нибудь нехорошее, база данных всё равно продолжит работать.

Физически база данных Мнезии может находиться или в оперативной памяти, или на жёстком диске, или и там и там. Первый вариант — самый скоростной, однако при перезагрузке Системы мы рискуем потерять все данные. Для хранения данных в оперативной памяти Мнезия использует ETS, на диске — DETS. Данные хранилища термов можно использовать и вне Мнезии.

У Мнезии свой язык запросов (не SQL, как пример).

Мнезия поддерживает транзакции — механизм безопасного обновления БД.

Как вспоминал Джо Армстронг, изначально новая СУБД получила название Amnesia. Однако одному из боссов это название не понравилось, потому что СУБД создаются для того, чтобы помнить, а не забывать. Поэтому просто убрали букву “A” в начале.

Создание базы данных

Это надо сделать только один раз. Создание БД сводится к созданию схемы.

1> mnesia:create_schema([node()]).
ok
2> q().
ok
$ ls
Mnesia.nonode@nohost

mnesia:create_schema/1 в качестве аргумента принимает список нод. Мы пока только тренируемся, поэтому указали лишь текущую ноду, причём не распределённую, что видно по названию nonode@nohost.

Создание БД, как мы видим, имело физические последствия — на диске в текущем каталоге был создан каталог нашей БД под названием Mnesia.nonode@nohost.

Название БД зависит от имени ноды. Попробуем запустить полноценную распределённую ноду и снова создать БД.

$ erl -name b@a.s
(b@a.s)1> mnesia:create_schema([node()]).
ok
(b@a.s)2> q().
ok
$ ls
Mnesia.b@a.s  Mnesia.nonode@nohost

Как видим, в текущем каталоге была создана ещё одна БД, с другим именем.

Внутри обоих каталогов пока лишь один небольшой файл под названием FALLBACK.BUP.

В дальнейшем, когда мы снова запустим ноду, настройки БД будут автоматически прочитаны из текущего каталога. Но это не всегда удобно и безопасно — надо следить за тем, в каком каталоге операционки мы находимся при запуске нашей Системы. Лучше явно указать при запуске ноды, где находятся настройки БД.

$ erl -mnesia dir '"/path/to/mnesia"'
1> mnesia:create_schema([node()]).
ok

В нашем примере файл FALLBACK.BUP будет создан именно в /path/to/mnesia. Если данный путь уже существует, но в нём нет настроек, ошибки при создании не будет. Однако если БД уже определена, это будет already_exists:

1> mnesia:create_schema([node()]).
{error,{nonode@nohost,{already_exists,nonode@nohost}}}

Запуск и остановка Мнезии

Мнезия это приложение, и если мы собрались им пользоваться, его надо запустить.

1> mnesia:start().
ok

Если это приложение нам больше не нужно, мы его можем остановить.

2> mnesia:stop().
=INFO REPORT==== 22-Jul-2025::15:37:48.526009 ===
    application: mnesia
    exited: stopped
    type: temporary

stopped

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

Для хранения однородных данных (например, списка пользователей или товаров нашего магазина) используют таблицы. Строка этой таблицы — некая отдельная сущность, которую мы описываем (например, пользователь или товар). Столбец — та или иная характеристика этой сущности (например, год рождения или цена).

При работе с Мнезией принято опираться на тип данных запись. Запись, напомню, это такой способ оформления кортежа — более наглядный в тексте программы. Пример такого терма: #user{id=1,name="Alex"}. user — это имя записи, остальное — поля, относящиеся к конкретной сущности (пользователю). С помощью этих полей удобно описывать столбцы таблицы.

Поэтому перед созданием таблицы надо определить в модуле эти записи с помощью -record.

Прежде чем создать первую таблицу, надо сначала придумать, что именно мы будем в ней хранить. Возьмём самый простой вариант — будем описывать содержимое нашей личной библиотеки. У каждой книги будет лишь два поля: автор и название. Поэтому в начале модуля мы так определим запись:

-record(book, {author, title}).

Создаётся таблица с помощью функции mnesia:create_table/2.

mnesia:create_table(book, [{attributes, record_info(fields, book)}]),

Следуем помнить, что перед созданием таблицы должна быть уже создана схема, а также запущена Мнезия. Джо Армстронг в своей книге пишет, что после создании таблицы Мнезию надо перезапустить. Чтобы не забыть про это можно в свои модули добавлять специальную функцию, инициализирующую БД, запускающую Мнезию, создающую таблицы и останавливающую Мнезию. Пример такого модуля:

-module(mydb).
-record(book, {author, title}).
-export([do_once/0, add_book/2]).

do_once() ->
    mnesia:create_schema([node()]),
    mnesia:start(),
    mnesia:create_table(book, [{attributes, record_info(fields, book)}]),
    mnesia:stop().

add_book(Author, Title) ->
    Row = #book{author=Author, title=Title},
    Ft = fun() ->
                mnesia:write(Row)
        end,
    mnesia:transaction(Ft).

Что делает функция add_book, понятно из названия. Детали мы рассмотрим в следующем параграфе.

Добавление данных в таблицу

Работа с БД требует осторожности. Надо понимать, что обращаться к данным конкурентно могут сразу несколько акторов. В целях безопасности в Мнезии все операции с БД (в т.ч. чтение) должны проходить через механизм транзакций. Транзакция — атомарное (неразрывное) действие, гарантирующее, что данные и их связность не будет нарушена одновременным доступом со стороны нескольких акторов.

Транзакцию реализует функция mnesia:transaction(Ft), где Ft это фунтерм, выступающий в качестве аргумента. Он является своего рода планом действий. В нём, конечно, может быть несколько выражений, но в нашем примере только одно: mnesia:write(Row).

Row имеет тип данных запись. В нашем случае запись хранит сведения об авторе и названии книги. Эти данные поступают в функцию add_book/2 и привязываются к переменным Author и Title.

Этого примера мне, кажется, на первое время хватит для того, чтобы опробовать добавление данных в таблицу и чтение из неё. Предполагается, что остальное я буду делать из оболочки. Опробуем же…

$ erl +pc unicode -mnesia dir '"/path/to/mnesia"'
1> mydb:do_once().
=INFO REPORT==== 22-Jul-2025::19:28:47.423230 ===
    application: mnesia
    exited: stopped
    type: temporary

stopped
2> mnesia:start().
ok
3> mydb:add_book("М.А. Булгаков", "Мастер и Маргарита").
{atomic,ok}
4> mydb:add_book("Л.Н. Толстой", "Война и мир").
{atomic,ok}
5> mydb:add_book("Т. Драйзер", "Финансист").
{atomic,ok}
6> mydb:add_book("Л.Н. Толстой", "Анна Каренина").
{atomic,ok}
7> Q = qlc:q([X || X <- mnesia:table(book)]).
{qlc_handle,{qlc_lc,#Fun<erl_eval.43.113135111>,
                    {qlc_opt,false,false,-1,any,[],any,524288,allowed}}}
8> Ft = fun() -> qlc:e(Q) end.
#Fun<erl_eval.43.113135111>
9> mnesia:transaction(Ft).
{atomic,[{book,"Л.Н. Толстой","Анна Каренина"},
         {book,"Т. Драйзер","Финансист"},
         {book,"М.А. Булгаков","Мастер и Маргарита"}]}
10> 

Добавление данных и их прочтение прошло успешно, если не считать того, что «Война и мир» куда-то пропала (об этом позже).

Ключ +pc unicode я использовал, чтобы в конце результат поиска отобразился кириллицей, а не юникод-номерами.

В функции mydb:do_once в конце Мнезия останавливается. Поэтому я в оболочке её снова запустил.

Далее я занёс в БД четыре книги, по каждому действию был получен результат {atomic,ok}. Именно этот терм возвращает mnesia:transaction в случае успеха. Но хочется как-то воочию убедиться, что данные в БД и правда добавлены. Значит, придётся их как-то прочитать…

Чтение данных из таблицы

Читать данные из Мнезии удобно модулем qlc. Он позволяет при запросах использовать удобную и гибкую семантику задания списка. Эту семантику можно наблюдать в следующей строке:

7> Q = qlc:q([X || X <- mnesia:table(book)]).

Литерал [X || X <- mnesia:table(book)] в данном случае не является стандартным выражением Эрланга, то есть он не вычисляется в какой-либо терм перед тем, как стать аргументом у qlc:q/1. Следует не забывать про это исключение из правил, потому что рано или поздно может возникнуть желание привязать результат “выражения” к переменной, а потом уже эту переменную передать в качестве аргумента функции qlc:1. Сделать это можно, но результат будет другой.

qlc:q возвращает обработчик (handler), который в дальнейшем используется функцией qlc:e/1. В целях повышения эффективности эта функция компилирует запрос, в дальнейшем этот скомпилированный запрос можно использовать много раз. Если таблица поменялась, то и результат будет выводиться разный.

Поиск по БД тоже должен осуществляться с помощью транзакций, поэтому мы предварительно создаём фунтерм, а потом его передаём в качестве аргумента для mnesia:transaction/1.

8> Ft = fun() -> qlc:e(Q) end.
#Fun<erl_eval.43.113135111>
9> mnesia:transaction(Ft).

Вот такая замысловатая «матрёшка». Однако к этому легко привыкнуть.

Проверим скорость добавления в БД и чтения из неё. Заодно убедимся, что скомпилированный запрос можно использовать многократно и получать при этом разный результат.

15> timer:tc(mydb, add_book, ["А. Дюма", "Три мушкетёра"]).
{151,{atomic,ok}}
16> timer:tc(mnesia, transaction, [Ft], microsecond).
{197,
 {atomic,[{book,"А. Дюма","Три мушкетёра"},
          {book,"Л.Н. Толстой","Анна Каренина"},
          {book,"Т. Драйзер","Финансист"},
          {book,"М.А. Булгаков","Мастер и Маргарита"}]}}

На добавление новой записи ушло 151 микросекунд, чтение — 197. Заодно мы увидели непредсказуемый порядок вывода.

Семантика задания списка, конечно, позволяет делать и более сложные вещи, нежели вывести список всех записей. Можно вывести, например, только одно поле записи. Допустим, мы желаем получить список лишь названий книг. Добавим соответствующую функцию в наш модуль:

get_titles() ->
    Q = qlc:q([X#book.title || X <- mnesia:table(book)]),
    Ft = fun() -> qlc:e(Q) end,
    {atomic,L} = mnesia:transaction(Ft),
    L.

Чтобы qlc:q заработала, надо вставить заголовочный файл qlc.hrl (в начале модуля):

-include_lib("stdlib/include/qlc.hrl").

Компилируем и запускаем в оболочке:

23> mydb:get_titles().
["Три мушкетёра","Анна Каренина","Финансист",
 "Мастер и Маргарита"]

Можно, конечно, выбрать какое-то подмножество из таблицы, согласно произвольному условию.

get_short() ->
    Q = qlc:q([X || X <- mnesia:table(book),
            length(X#book.title) < 10]),
    Ft = fun() -> qlc:e(Q) end,
    {atomic,L} = mnesia:transaction(Ft),
    L.

Эта функция ищет все книги, длина заголовка которых менее десяти букв.

25> mydb:get_short().
[{book,"Т. Драйзер","Финансист"}]

В общем виде задание списка выглядит так:

[ Выражение || Квалификатор1, Квалификатор2... ]

Квалификаторы бывают двух видов:

X <- mnesia:table(book) в нашем примере это генератор. length(X#book.title) < 10 — фильтр.

Семантика задания списка позволяет в одном запросе обращаться сразу к нескольким таблицам. Схематично это выглядит примерно так:

Изменение данных

Изменение данных зависит от типа таблицы. В зависимости от того, может ли быть несколько записей с одинаковым первичным ключом или нет, а также от наличия сортировки Мнезия поддерживает три типа данных:

Первичный ключ это первый столбец таблицы. В нашем примере это author. И тут понятен корень нашей ошибки, когда мы где-то потеряли одну книгу Л.Н. Толстого. При создании таблицы мы не задали её тип (аттрибут type), поэтому получили дефолтный — set. Поэтому когда мы добавили вторую книгу Л.Н. Толстого, так получилось, что это вышло не добавление новой записи, а изменение уже имеющейся (ведь в set и ordered_set может быть лишь одна запись с данным первичным ключом).

В дальнейшем мы исправим эту свою ошибку. Можно добавить поле uid (универсальный идентификатор книги), а можно просто изменить тип таблицы на bag.

Таким образом, для таблиц set и ordered_set для изменения данных достаточно просто добавить новую запись с таким же первичным ключом. Для таблицы bag придётся сначала удалить старую запись и потом добавить новую. Лучше это сделать атомарной операцией, чтобы ни на мгновение в нашей базе данных запись, относящаяся к одной сущности, не задвоилась. Например, в нашей БД может образоваться две записи, относящиеся к одной книге: «Война и мирр» и «Война и мир».

Удаление данных

Удалить запись можно с помощью функции mnesia:delete(Oid). Oid здесь это идентификатор объекта (строки в таблице). Он является кортежем {ИМЯ_ТАБЛИЦЫ, ПЕРВИЧНЫЙ_КЛЮЧ}. Если мы в нашем примере ошиблись в написании фамилии Толстого, удалить эту запись можно с помощью команды mnesia:delete({book, "Л.Н. Толстои"}).

Ft = fun() -> mnesia:delete({book, "Л.Н. Толстои"}) end,
{atomic,_} = mnesia:transaction(Ft),

Если тип таблицы bag, то записей с данным первичным ключом может быть несколько. Допустим, мы ошиблись в инициалах автора и забили в БД что автор “Войны и мира” — А.Н. Толстой. Однако в нашей библиотеке есть книги другого замечательного писателя — А.Н. Толстого. Команда mnesia:delete({book, "А.Н. Толстой"}) удалит не только неправильную запись, но и правильные.

Для этих случаев есть функция mnesia:delete_object (см. документацию).

Исправление ошибок

Мы пробежались по CRUD, теперь настало время исправить выявленные ошибки. Первая — то ли надо добавить числовой uid книгам, то ли просто изменить тип таблицы на bag. Раз мы экспериментируем и просто неохота добавлять ещё одно поле, попробуем второе.

Вторая ошибка — при создании таблицы мы не добавили ей опции disc_copies, в результате при перезагрузке Системы все внесённые данные были потеряны, потому что они располагались лишь в RAM. Мы собираемся пользоваться своей БД годами, а не мимолётно, поэтому придётся добавить опцию disc_copies.

Чтобы проверить, как работает сложный поиск сразу по двум таблицам, нам придётся создать ещё одну. Пусть это будет список авторов: его ФИО, страна, год рождения, год смерти. Имея такую дополнительную таблицу, мы можем делать такие запросы: найти все книги, написанные в такой-то стране, найти все книги, написанные авторами, родившимися в таком-то веке.

-module(mydb).
-record(book, {author, title}).
-record(author, {name, country, birth, death}).
-export([do_once/0, add_book/2, add_author/4, get_titles/0, get_short/0, test/0, get_books/0, get_authors/0, by_country/1]).
-include_lib("stdlib/include/qlc.hrl").

do_once() ->
    mnesia:create_schema([node()]),
    mnesia:start(),
    mnesia:create_table(book, [{attributes, record_info(fields,book)},{type,bag},{disc_copies,[node()]}]),
    mnesia:create_table(author, [{attributes, record_info(fields,author)},{type,set},{disc_copies,[node()]}]),
    mnesia:stop().

test() ->
    mnesia:start(),
    timer:sleep(100),
    add_book("М.А. Булгаков", "Мастер и Маргарита"),
    add_book("Л.Н. Толстой", "Анна Каренина"),
    add_book("Л.Н. Толстой", "Война и мирр"),
    del_row(#book{author="Л.Н. Толстой",title="Война и мирр"}), % исправляем
    add_book("Л.Н. Толстой", "Война и мир"),
    add_book("Т. Драйзер", "Финансист"),
    add_book("А.Н. Толстой", "Хождение по мукам"),
    add_author("М.А. Булгаков", "СССР", 1891, 1940),
    add_author("Л.Н. Толстой", "Российская империя", 1828, 1940),
    add_author("Л.Н. Толстой", "Российская империя", 1828, 1910), % исправляем
    add_author("Т. Драйзер", "США", 1871, 1945),
    add_author("А.Н. Толстой", "СССР", 1883, 1945).

add_book(Author, Title) ->
    Row = #book{author=Author, title=Title},
    Ft = fun() ->
                mnesia:write(Row)
        end,
    mnesia:transaction(Ft).

add_author(Name,Country,Birth,Death) ->
    Row = #author{name=Name,country=Country,birth=Birth,death=Death},
    Ft = fun() ->
                mnesia:write(Row)
        end,
    mnesia:transaction(Ft).

get_books() ->
    Q = qlc:q([X || X <- mnesia:table(book)]),
    Ft = fun() -> qlc:e(Q) end,
    {atomic,L} = mnesia:transaction(Ft),
    L.

get_authors() ->
    Q = qlc:q([X || X <- mnesia:table(author)]),
    Ft = fun() -> qlc:e(Q) end,
    {atomic,L} = mnesia:transaction(Ft),
    L.

by_country(Country) ->
    Q = qlc:q([ {Y#book.author,Y#book.title} || X <- mnesia:table(author),
                    X#author.country == Country,
                    Y <- mnesia:table(book),
                    Y#book.author == X#author.name
              ]),
    Ft = fun() -> qlc:e(Q) end,
    {atomic,L} = mnesia:transaction(Ft),
    L.

get_titles() ->
    Q = qlc:q([X#book.title || X <- mnesia:table(book)]),
    Ft = fun() -> qlc:e(Q) end,
    {atomic,L} = mnesia:transaction(Ft),
    L.

get_short() ->
    Q = qlc:q([X || X <- mnesia:table(book), length(X#book.title) < 10]),
    Ft = fun() -> qlc:e(Q) end,
    {atomic,L} = mnesia:transaction(Ft),
    L.

del_row(Obj) ->
    Ft = fun() -> mnesia:delete_object(Obj) end,
    {atomic,L} = mnesia:transaction(Ft),
    L.

У таблицы book будет тип bag, у author — set. Обеим таблица выставили опцию {disc_copies,[node()]}. Второй элемент кортежа у этой опции — список нод, на которых мы желаем, чтобы создавалась дисковая копия.

На этот раз я решил не заносить книги в БД вручную из оболочки, а сделал для этого функцию test. Эта функция начинается с запуска Мнезии. Какое-то время после её запуска таблицы ещё продолжают загружаться, поэтому если сразу после команды mnesia:start() попросить добавить книгу, будет ошибка. Пришлось вставить паузу на 100 мс.

Преднамеренно при добавлении книг я совершил две описки, которые тут же исправляются. Первая ошибка (“Война и мирр”) случилась в таблице типа bag. Я могу, конечно, добавить правильный вариант, но старый останется. Поэтому я сначала удаляю первую запись. Для удобства и наглядности я написал свою функцию del_row/1. В качестве аргумента она принимает объект, который надо удалить. Для этого надо представить удаляемую запись целиком: #book{author="Л.Н. Толстой",title="Война и мирр"}. mnesia:delete_object/1 получит именно этот аргумент.

Вторую описку (1940, а не 1910) легче исправить, ведь здесь данные в таблице типа set. Поэтому просто добавляем новую запись с корректной информацией.

Далее идёт ряд функций. Мы их будем дёргать потом в оболочке, когда данные будут добавлены в БД. В частности я написал функцию by_country/1. Принимая страну, она должна выводить все книги, написанные авторами из этой страны. В задании списка мы видим четыре выражения подряд:

X <- mnesia:table(author),
X#author.country == Country,
Y <- mnesia:table(book),
Y#book.author == X#author.name

Первое это генератор, он предоставляет список всех авторов. Второе — фильтр, он оставляет в списке авторов только тех, кто относится к заданной стране. Далее идёт второй генератор, он предоставляет список всех книг. На этом этапе образуются всевозможные пары из элементов первого списка (отфильтрованного) и второго списка. Четвёртое выражение это фильтр, он среди списка пар оставляет только те, в которых ФИО авторов совпадают.

Проверим работу изменённого модуля. Поскольку мы собрались менять схему БД, начнём с того, что всё удалим, что было в текущем каталоге Мнезии.

$ rm /path/to/mnesia/*
$ erl +pc unicode -mnesia dir '"/path/to/mnesia"'
1> mydb:do_once().
=INFO REPORT==== 24-Jul-2025::18:26:17.596855 ===
    application: mnesia
    exited: stopped
    type: temporary

stopped
2> mydb:test().
{atomic,ok}
3> mydb:get_books().
[{book,"А.Н. Толстой","Хождение по мукам"},
 {book,"Л.Н. Толстой","Анна Каренина"},
 {book,"Л.Н. Толстой","Война и мир"},
 {book,"Т. Драйзер","Финансист"},
 {book,"М.А. Булгаков","Мастер и Маргарита"}]
4> mydb:get_authors().
[{author,"А.Н. Толстой","СССР",1883,1945},
 {author,"Л.Н. Толстой","Российская империя",1828,1910},
 {author,"Т. Драйзер","США",1871,1945},
 {author,"М.А. Булгаков","СССР",1891,1940}]
5> mydb:by_country("СССР").
[{"А.Н. Толстой","Хождение по мукам"},
 {"М.А. Булгаков","Мастер и Маргарита"}]
6> mydb:get_titles().
["Хождение по мукам","Анна Каренина","Война и мир",
 "Финансист","Мастер и Маргарита"]
7> mydb:get_short().
[{book,"Т. Драйзер","Финансист"}]
8> q().
ok
$ erl +pc unicode -mnesia dir '"/path/to/mnesia"'
1> mnesia:start().
ok
2> mydb:get_titles().
["Хождение по мукам","Анна Каренина","Война и мир",
 "Финансист","Мастер и Маргарита"]

Всё работает как и задумано. Описки были исправлены. Функция mydb:by_country/1 работает верно. В конце мы перезагрузили Систему, запустили Мнезию и получили результат, сохранённый на диске.

После наших манипуляций в каталоге Мнезии образовались следующие файлы:

author.DCD
book.DCD
DECISION_TAB.LOG
LATEST.LOG
schema.DAT

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

mnesia:create_table/2 в качестве первого аргумента принимает атом-имя создаваемой таблицы. Лучше, чтобы он совпадал с названием записи, определённой в начале модуля. В нашем примере это book и author. Второй аргумент — список опций.

Опция {attributes, AtomList} нужна для определения названия столбцов таблицы. В нашем примере она выглядела так: {attributes, record_info(fields,book)}. Второе поле кортежа должно быть списком атомов, это и будут обозначения столбцов. Мы могли бы написать [author,title], ведь мы желаем, чтобы в таблице book были именно такие столбцы. Однако использовали более надёжный вариант: record_info(fields,book). record_info/2 — функция, которая автоматически генерируется при компилировании, если модуль содержит определения записей. Если у неё первый аргумент fields, она вернёт список полей для записи, указанной вторым аргументом.

Другая возможная опция — {access_mode, Atom}. Режим доступа может быть read_only и read_write.

Уже знакомя опция {disc_copies, NodeList} определяет, на каких нодах Системы на диске должна храниться копия данной таблицы.

{disc_only_copies, NodeList} определяет, на каких нодах Системы должна быть только дисковая копия таблицы (то есть в RAM её не будет). Взаимодействие с таблицей будет медленным, однако оперативная память не будет занята.

{index, Intlist} поможет оптимизировать скорость доступа к данным. Если эта опция указана, то будет создана дополнительная индексная таблица для указанных в Intlist столбцов таблицы. Первый столбец является первичным ключом, он и так в любом случае индексируется. При желании можно индексировать и другие столбцы.

Таблицы могут быть гигантскими, и если мы часто перезагружаем Систему, нам, может быть, захочется, чтобы какие-то таблицы загружались быстрее. Этой цели служит опция {load_order, Integer}. По умолчанию второй элемент кортежа 0. Если поставить 1 или другое целое число, данная таблица будет иметь приоритет. Если у другой таблицы число окажется ещё выше, у той будет ещё больший приоритет при загрузке.

Опция {majority, Flag} нужна для обеспечения согласованности реплик таблицы, находящихся на разных нодах. Может быть так, что какое-то время некоторые реплики будут недоступны (из-за недоступности нод). Если флаг установлен в true, изменения в таблицу будут приняты только в том случае, когда есть возможность изменить большинство реплик таблицы. Этот принцип не касается «грязных операций».

Опция {ram_copies, NodeList} отвечает за список нод, на которых таблицы будут размещены в RAM.

{record_name, Name} нужна для того, чтобы определить атом, который будет первым у любой записи в таблице. По умолчанию — название самой таблицы.

{snmp, SnmpStruct} нужна для контроля через SNMP.

{storage_properties, [{Backend, Properties}]} нужна для тонкой настройки хранилища термов (ETS или DETS). Например, в настройках можно указать compressed.

Для определения типа таблицы — set, oredered_set или bag — используется опция {type, Type}. К сожалению, в настоящее время ordered_set не может быть использована вместе с disc_only_copies.

{local_content, Bool} — не знаю.

Визуализация

Есть возможность визуально исследовать нашу БД, посмотреть информацию о таблицах, заглянуть в данные и даже исправить их. Для этих целей можно воспользоваться Обсервером.

4> observer:start().

Появится графическое окно с разной информацией о нашей Системе. Следует выбрать пункт “Table Viewer”. Сначала мы увидим имеющиеся ETS-таблицы. Чтобы перейти к таблицам Мнезии, надо выбрать View -> Mnesia Tables.

Обсервер, просмотрщик таблиц

Выбрав какую-нибудь таблицу, с помощью правого клика можем увидеть свойства созданной таблицы. С помощью двойного клика по названию таблицы или через тот же правый клик можем посмотреть содержание таблицы — её данные.

Обсервер, просмотр данных

Выбрав ту или иную строку в таблице, мы можем сделать по ней двойной клик или воспользоваться Edit -> Edit Object, чтобы перейти к редактированию сохранённого терма.

Обсервер, редактирование данных

Бэкап Мнезии

Для бэкапа и восстановления из него есть две функции: mnesia:backup/1 и mnesia:restore/2. Первая — создаёт файл (один), в котором сохраняется вся информация о БД: схема, таблицы, данные. Второй — восстанавливает из файла БД. Простой пример создания и восстановления:

7> mnesia:backup("/tmp/mnesia.bup").
ok
8> q().
ok
$ rm -rf /path/to/mnesia/*
$ erl +pc unicode -mnesia dir '"/path/to/mnesia"'
1> mnesia:create_schema([node()]).
ok
2> mnesia:start().
ok
3> mnesia:restore("/tmp/mnesia.bup", [{default_op,recreate_tables}]).
{atomic,[author,book]}
4> observer:start().

Сначала создаётся файл бэкапа, после чего мы выходим из оболочки, чтобы из операционки удалить все файлы, относящиеся к Мнезии. Затем снова заходим и первым делом снова создаём схему. Потом запускаем Мнезию и делаем восстановление. После этого запускаем Обсервер, чтобы проверить, что всё правильно восстановилось.

Грязные операции с БД

Транзакции обеспечивают максимальную надёжность при работе с данными: при добавлении новых данных, чтении, изменении, удалении. Однако на стадии разных экспериментов порой не хочется заморачиваться с транзакциями, хочется по-быстрому, например, посмотреть, какие сейчас в таблице есть записи согласно такому-то ключу. В таких случаях, когда торопимся и/или полностью уверены в отсутствии дурных последствий, можно использовать так называемые грязные операции. Все они совершаются в обход механизма транзакций.

Такие функции можно различить по характерному префиксу dirty_:

5> h(mnesia, dirty_
Functions
dirty_all_keys,               dirty_delete,                 dirty_delete_object,          
dirty_first,                  dirty_index_match_object,     dirty_index_read,             
dirty_last,                   dirty_match_object,           dirty_next,                   
dirty_prev,                   dirty_read,                   dirty_rpc,                    
dirty_sel_init,               dirty_select,                 dirty_slot,                   
dirty_update_counter,         dirty_write,

Стандартные функции по работе с БД, такие как qlc:e/1, вне механизма транзакций мы даже не сможем нормально вызвать:

5> Q = qlc:q([X || X <- mnesia:table(book)]).
{qlc_handle,{qlc_lc,#Fun<erl_eval.43.113135111>,
                    {qlc_opt,false,false,-1,any,[],any,524288,allowed}}}
6> qlc:e(Q).
** exception exit: {aborted,no_transaction}
     in function  mnesia:abort/1 (mnesia.erl:685)
     in call from qlc:call/4 (qlc.erl:4362)
     in call from qlc:table_handle/3 (qlc.erl:3219)
     in call from qlc:setup_le/3 (qlc.erl:2984)
     in call from qlc:eval/2 (qlc.erl:367)

Если мы торопимся, придётся работать именно с грязными функциями (и из-за префикса dirty_ нас будут терзать муки совести).

11> mnesia:dirty_read(book, "А.Н. Толстой").
[#book{author = "А.Н. Толстой",title = "Хождение по мукам"}]
12> mnesia:dirty_write(book, #book{author="А.Н. Толстой",title="Аэлита"}).
ok
13> mnesia:dirty_read(book, "А.Н. Толстой").
[#book{author = "А.Н. Толстой",title = "Хождение по мукам"},
 #book{author = "А.Н. Толстой",title = "Аэлита"}]

Работа с термами

Ключевое достоинство Мнезии — органичная работа с термами. На всех этапах работы с данными (создание, чтение, изменение, удаление) мы можем работать с термом именно как с термом, сохраняя привычный стиль работы.

Допустим, продолжая заносить книги в нашу БД, мы натолкнулись на проблему. Есть у нас книги писателя М. Горького. Он, как известно, писал и был популярен как в до-, так и послереволюционный период. К какому государству его отнести: Российской империи или СССР? Отнесём мы его к СССР — а потом будет искать книги писателей дореволюционного периода, а М. Горького среди них не окажется. Или наоборот.

Самый плохой вариант — написать, что страна у него “Российская империя, СССР”. Это правда, но потом надо будет что-то придумывать с поиском в строках. Это сильно усложнит концепцию нашей БД и к тому же замедлит поиск по ней.

Второй вариант — добавить в таблицу author столбец country2, специально предназначенный для тех авторов, которые успели поработать в другом государстве. У тех же авторов, которые творили только в одном государстве, в графе country2 будет стоять прочерк. Этот вариант лучше первого (своей строгостью), однако неприятен усложнением концепции БД. Теперь придётся искать не по одному столбцу, а по двум (причём по сути одну и ту же информацию). Ещё обидно то, что рано или поздно возникнет писатель, успевший пожить в трёх-четырёх государствах (например, И.А. Бунин), и тогда придётся заново решать старую задачу.

Третий вариант — создать дополнительную таблицу. В первом столбце будет ФИО автора, а во втором — государства, гражданином которых он успел побывать. Если одно гражданство было — одна строка, два — две, три — три… Это отличное, строгое решение, но, конечно, придётся добавить таблицу и тем самым опять усложнить концепцию БД. Опять же, потом у нас может возникнуть потребность занести в БД не одно ФИО, а два или три. Например, может захотеться, чтобы Горький был записан и как “М. Горький”, и как “А.М. Горький”. То же самое может быть и с названием книг. Например, у известной книги В.О. Богомолова “В августе сорок четвёртого” есть ещё четыре альтернативных названия: “Момент истины” и др. Вот и получается, что для этих двух столбцов (author и title) потребуется создать по таблице — для альтернативных обозначений.

Четвёртый вариант характерен именно для Эрланга и Мнезии. Мы не создаём дополнительных (служебных) столбцов или таблиц, мы просто вместо одного терма вроде строки “М. Горький” задаём чуть более сложный терм — вроде списка ["М. Горький", "А.М. Горький"]. Это не потребует никаких изменений в структуре БД. Да, скорее всего, нам придётся где-то в программе учесть, что некоторые писатели представлены как ФИО, а некоторые как список ФИО, но это изменения не очень сложные. Зато наша БД останется элегантно стройной.

Не всегда, конечно, следует идти путём усложнения термов. Часто будет удобным и эффективным именно создание дополнительных таблиц. Однако возможность усложнять термы для программиста это дополнительная степень свободы. Нам теперь совсем не обязательно «на каждый чих» создавать в БД новые столбцы и таблицы.

К тому же нет особой нужды продумывать типы данных в таблицах. Мы просто создаём таблицу, заполняем её и решаем проблемы по мере возникновения. Допустим, в нашей библиотеке есть “Илиада” Гомера и это единственный автор, год рождения которого нам неизвестен. Мы не скованы обязательством держать в столбце birth только числа (как это у других авторов: 1828, 1871 и т.д.). Дойдя до Гомера, мы можем просто указать ему, например, кортеж {around,-850}. Если нашей программе в каком-то месте это не понравится, мы быстро найдём это и адаптируем код под новую ситуацию.

Другой пример. Мы дошли до книги “Афоризмы Козьмы Пруткова”. Как известно, К. Прутков это вымышленный автор, маска, за которой скрывались четверо писателей. Среди них — А.К. Толстой, его книга “Князь Серебряный” тоже есть в нашей библиотеке. Нам хочется как-то отразить, причём максимально строго, эти обстоятельства в нашей БД. При этом, конечно, новые столбцы и новые таблицы совсем не хочется заводить. Мы просто на ходу придумываем новый терм, который максимально строго описывает эту деликатную ситуацию. Например, что-то вроде такого: ["К. Прутков", {fake,["А.К. Толстой","Ал-й М. Жемчужников","В.М. Жемчужников","Ал-др М. Жемчужников"]}]. Для нас К. Прутков — тоже автор, пусть и вымышленный. Возможно, при поиске книг по автору мы зададим именно “К. Прутков”, поэтому мы его сохранили в списке авторов. Вторым в списке идёт уже коллективный автор в виде кортежа: метки fake и списка реальных авторов. Мы ещё не знаем, как реализуем поиск по этому кортежу, но, очевидно, сделать это будет относительно несложно.

Это безусловно удобно — на всех этапах работать с табличными данными как с полноценными термами Эрланга. Можно, например, прямо в генераторе провести декомпозицию сложного терма с помощью паттерна:

32> Q4 = qlc:q([X || #book{author=[_,{fake,X}]} <- mnesia:table(book)]).
{qlc_handle,{qlc_lc,#Fun<erl_eval.43.113135111>,
                    {qlc_opt,false,false,-1,any,[],any,524288,allowed}}}
33> Ft4 = fun() -> qlc:e(Q4) end.
#Fun<erl_eval.43.113135111>
34> mnesia:transaction(Ft4).
{atomic,[["А.К. Толстой","Ал-й М. Жемчужников",
          "В.М. Жемчужников","Ал-др М. Жемчужников"]]}

В данном примере нас интересовали лишь авторы, участвовавшие в создании фейковых коллективных авторов, поэтому генератор и отдаёт лишь список таких авторов для конкретной книги, если, конечно, книга написана таким коллективом.

Документация

mnesia — Мнезия на официальном сайте Эрланга.

h(mnesia).

h(qlc).


Copyright © 2025-2026 Алексей Карманов