Мнезия — система управления базами данных (СУБД), разработанная специально для Эрланга и входящая в его стандартную поставку. То же: 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 Алексей Карманов