Published using Google Docs
mnesia_17chap.txt
Updated automatically every 5 minutes

Глава 17

Mnesia: СУБД в Erlang-е

Suppose you want to write a multiuser game, make a new website,

or create an online payment system. You’ll probably need a database

management system (DBMS).

Предположим, вы захотели создать многопользовательскую игру, сделать новый веб-сайт, или разработать систему онлайн-платежей. Вероятно, вам в этом случае понадобится какая-либо СУБД.

Hidden in the thousands of files that appear on our disk when we down-

load Erlang is a complete DBMS called Mnesia. It is extremely fast, and

it can store any type of Erlang data structure.

Среди тысячи файлов, появившихся на диске после установки Erlang-а, присутствует (полная?) СУБД под названием Mnesia. Она исключительно быстра, и она может хранить структуры, состоящие из любых определенных в Erlang типов данных.

It’s also highly configurable. Database tables can be stored in RAM (for

speed) or on disk (for persistence), and the tables can be replicated on

different machines to provide fault-tolerant behavior.

Кроме того, Mnesia поддаётся детальной настройке и конфигурированию. Определённые таблицы могут храниться в RAM (для обеспечения высокой скорости доступа), другие - сохраняться на диск (выживая при перезагрузке узла), также таблицы могут реплицироваться на другие компьютеры в сети, что даёт возможность строить устойчивые к сбоям системы.

Давайте познакомимся со всем этим поближе.

17.1 Запрсы к базе данных.

17.1 Database Queries

Начнём, пожалуй, с изучения запросов к БД Mnesia. При ближайшем рассмотрении оказывается, что запросы Mnesia одновременно похожи и на SQL 1), и на (list comprehensions?), так что нам не придётся изучать так уж много нового. 2)

1) Очень известный язык, используемый для доступа к реляционным СУБД

2) На самом деле, не столь удивительно, что SQL и List comprehensions похожи. Обе этих вещи основаны на математической теории множеств.

Let’s start by looking at Mnesia queries. As we look through this, we

might be surprised to see that Mnesia queries look a lot like both SQL1

and list comprehensions, so there’s actually very little we need to learn

to get started.2

A popular language that is used to query relational databases.

In fact, it really isn’t that surprising that list comprehensions and SQL look a lot alike.

Both are based on mathematical set theory.

In all our examples, I’ll assume that we have created a database with

two tables called shop and cost. These tables contain the following data.

Во всех примерах я буду подразумевать, что вы создали базу данных с двумя таблицами с названиями shop и cost. Эти таблицы содержат следующие данные.

Таблица shop:

Item

apple

orange

pear

banana

potato

Quantity

20

100

200

420

2456

Cost

2.3

3.8

3.6

4.5

1.2

Таблица cost:

Name

apple

orange

pear

banana

potato

Price

1.5

2.4

2.2

1.5

0.6

Чтобы представить эти таблицы в подходящем для Mnesia виде, мы должны создать определения записей, которые бы описывали колонки в таблицах. Определения эти таковы:

To represent these tables in Mnesia, we need record definitions that

define the columns in the tables. These are as follows:

Download test_mnesia.erl

-record(shop, {item, quantity, cost}).

-record(cost, {name, price}).

Now comes a little black magic. I want to show you how queries work,

and I want you to follow along at home. But to do that I have to create

and populate the database for you. So, just for now, trust me. I’ve writ-

ten the initialization code in the file test_mnesia.erl. You can just run it

from within erl.

Теперь немного чёрной магии. Я собираюсь показать, как работают запросы, и зочу, чтобы вы могли повторить это все у себя дома. Но для этого я сначала должен создать и заполнить для вас базу данных. Так что поверьте мне на слово - в файле test_mnesia.erl я подготовил код, который производит инициализацию, и вы можете просто запустить его из оболочки erl.

1> c(test_mnesia).

{ok,test_mnesia}

2> test_mnesia:do_this_once().

=INFO REPORT==== 29-Mar-2007::20:33:12 ===

application: mnesia

exited: stopped

type: temporary

stopped

Now we can move on to our examples.

Теперь мы можем приступить к нашим примерам.

Selecting All Data in a Table

Выборка всех данных из таблицы

Here’s the code to select all the data in the shop table. (For those of you

who know SQL, each code fragment starts with a comment showing the

equivalent SQL to perform the corresponding operation.)

Так выглядит код для выборки всех данных из таблицы shop (для тех, кто знаком с SQL, каждый фрагмент кода начинается с комментария, где показан соответствующий задаче код SQL).

Download test_mnesia.erl

%% SQL equivalent

%% SELECT * FROM shop;

demo(select_shop) ->

do(qlc:q([X || X <- mnesia:table(shop)]));

The heart of the matter is the call to qlc:q, which compiles the query

(its parameter) into an internal form that is used to query the database.

Суть примера в вызове функции qlc:q, которая компилирует запрос (её параметр) во внутреннее представление, используемое для непосредственного выполнения запроса (?).

We pass the resulting query to a function called do( ), which is defined

toward the bottom of test_mnesia.

Мы передаем полученный запрос в функцию с названием do(), которая определена в модуле test_mnesia (ближе к концу). Она отвечает за выполнение запроса и возврат результата.

It is responsible for running the question and returning the result. To make all this easily callable from erl, we map it to the function demo(select_shop). (The entire listing for mne-

sia_test appears at the end of this chapter.)

We can run it as follows:

Для того, чтобы её можно было легко вызывать из оболочки erl, мы сопоставили этот код функции

 demo(select_shop) (Полный листинг модуля mnesia_test приведён в конце главы).

Мы можем вызвать её таким образом:

1> test_mnesia:start().

ok

2> test_mnesia:reset_tables().

{atomic, ok}

3> test_mnesia:demo(select_shop).

[{shop,orange,100,3.80000},

{shop,pear,200,3.60000},

{shop,banana,420,4.50000},

{shop,potato,2456,1.20000},

{shop,apple,20,2.30000}]

Note: The rows in the table can come out in any order.

(Примечание: строки в таблице могут оказаться в другом порядке).

The line that sets up the query in this example is as follows:

Строка, которая задает запрос, в этом примере выглядит так:

qlc:q([X || X <- mnesia:table(shop)])

Это очень похоже на (list comprehension?) (см. раздел 3.6,

List Comprehensions, на странице 61?).

На самом деле, qlc расшифровывается как "query list compre-

hensions". Это один из модулей, которые служат для доступа к данным в БД Mnesia.

This looks very much like a list comprehension (see Section 3.6, List

Comprehensions, on page 61). In fact, qlc stands for query list compre-

hensions. It is one of the modules we can use to access data in an

Mnesia database.

[X || X <- mnesia:table(shop)] means “the list of X such that X is taken from

the shop Mnesia table.” The values of X are Erlang shop records.

[X || X <- mnesia:table(shop)] означаеи “список X, таких, что X берется из таблицы Mnesia с названием "shop"”. Значения X - это Erlang-овские записи типа "shop".

Важно: аргумент функции qlc:q/1 должен представлять собой литерал list comprehension (?), а не что-либо, что вычисляется в такое выражение. Таким образом, например, вот такой код не будет эквивалентным тому, что написано в примере:

Var = [X || X <- mnesia:table(shop)],

qlc:q(Var)

Note: The argument of qlc:q/1 must be a list comprehension literal and

not something that evaluates to such an expression. So, for example,

the following code is not equivalent to the code in the example:

Var = [X || X <- mnesia:table(shop)],

qlc:q(Var)

++Projecting Data from a Table

++Выборка определенных колонок из таблицы

Here’s a query that selects the item and quantity columns from the shop

table.

Download test_mnesia.erl

%% SQL equivalent

%% SELECT item, quantity FROM shop;

demo(select_some) ->

do(qlc:q([{X#shop.item, X#shop.quantity} || X <- mnesia:table(shop)]));

4> test_mnesia:demo(select_some).

[{orange,100},{pear,200},{banana,420},{potato,2456},{apple,20}]

In the previous query, the values of X are records of type shop. If you

recall the record syntax described in Section 3.9, Records, on page 69,

you’ll remember that X#shop.item refers to the item field of the shop

record. So, the tuple {X#shop.item, X#shop.quantity} is a tuple of the item

and quantity fields of X.

Следующий запрос выбирает колонки item и quantity из таблицы shop.

%% SQL equivalent

%% SELECT item, quantity FROM shop;

demo(select_some) ->

do(qlc:q([{X#shop.item, X#shop.quantity} || X <- mnesia:table(shop)]));

4> test_mnesia:demo(select_some).

[{orange,100},{pear,200},{banana,420},{potato,2456},{apple,20}]

В предыдущем примере значения X были записями типа shop. Если вы вспомните синтаксис выражений с записями из раздела 3.9, Records, на странице 69, вы увидите, что X#shop.item соотвествует полю "item" записи типа shop. Таким образом, кортеж {X#shop.item, X#shop.quantity} состоит из двух полей записи X - "item" и "quantity".

++Conditionally Selecting Data from a Table

++Выборка из таблицы данных, удовлетворяющих условию

Here’s a query that lists all items in the shop table where the number

of items in stock is less than 250. Maybe we’ll use this query to decide

which items to reorder.

Download test_mnesia.erl

%% SQL equivalent

%%

SELECT shop.item FROM shop

%%

WHERE shop.quantity < 250;

demo(reorder) ->

do(qlc:q([X#shop.item || X <- mnesia:table(shop),

X#shop.quantity < 250

]));

5> test_mnesia:demo(reorder).

[orange,pear,apple]

Следующий запрос выберет все наименования товаров из таблицы shop, где количество товара на складе меньше, чем 250. Мы могли бы использовать этот вопрос, чтобы решить, какие товары нам надо дозаказать.

%% SQL equivalent

%%

SELECT shop.item FROM shop

%%

WHERE shop.quantity < 250;

demo(reorder) ->

do(qlc:q([X#shop.item || X <- mnesia:table(shop),

X#shop.quantity < 250

]));

5> test_mnesia:demo(reorder).

[orange,pear,apple]

Notice how the condition is described naturally as part of the list com-

prehension.

Обратите внимание, как наше условие естественно вписалось в list comprehension.

++Selecting Data from Two Tables (Joins)

++Выборка данных из двух таблиц (Join)

Now let’s suppose that we want to reorder an item only if there are fewer

than 250 items in stock and the item costs less than 2.0 currency units.

To do this, we need to access two tables. Here’s the query:

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

%% SQL equivalent

%% SELECT shop.item, shop.quantity, cost.name, cost.price

%% FROM shop, cost

%% WHERE shop.item = cost.name

%% AND cost.price < 2

%% AND shop.quantity < 250

demo(join) ->

do(qlc:q([X#shop.item || X <- mnesia:table(shop),

X#shop.quantity < 250,

Y <- mnesia:table(cost),

X#shop.item =:= Y#cost.name,

Y#cost.price < 2

])).

6> test_mnesia:demo(join).

[apple]

Здесь важным является соединение между наименованием товара в таблице shop и наименованием товара в таблице cost:

X#shop.item =:= Y#cost.name

The key here is the join between the name of the item in the shop table

and the name in the cost table:

X#shop.item =:= Y#cost.name

+ 17.2 Adding and Removing Data in the Database

Again, we’ll assume we have created our database and defined a shop

table. Now we want to add or remove a row from the table.

+ 17.2 Вставка и удаление данных в/из БД.

Мы опять предполагаем, что мы создали БД и определили таблицу shop. Теперь мы хотим добавить строку в таблицу, или наоборот удалить из таблицы.

++Adding a Row

We can add a row to the shop table as follows:

Download test_mnesia.erl

add_shop_item(Name, Quantity, Cost) ->

Row = #shop{item=Name, quantity=Quantity, cost=Cost},

F = fun() ->

mnesia:write(Row)

end,

mnesia:transaction(F).

This creates a shop record and inserts it into the table:

++Вставка строки

Мы можем добавить строку в таблицу shop следующим образом:

add_shop_item(Name, Quantity, Cost) ->

Row = #shop{item=Name, quantity=Quantity, cost=Cost},

F = fun() ->

mnesia:write(Row)

end,

mnesia:transaction(F).

Эта функция создаёт запись типа shop и вставляет её в таблицу.:

1> test_mnesia:start().

ok

2> test_mnesia:reset_tables().

{atomic, ok}

%% list the shop table

3> test_mnesia:demo(select_shop).

[{shop,orange,100,3.80000},

{shop,pear,200,3.60000},

{shop,banana,420,4.50000},

{shop,potato,2456,1.20000},

{shop,apple,20,2.30000}]

%% add a new row

4> test_mnesia:add_shop_item(orange, 236, 2.8).

{atomic,ok}

%% list the shop table again so we can see the change

5> test_mnesia:demo(select_shop).

[{shop,orange,236,2.80000},

{shop,pear,200,3.60000},

{shop,banana,420,4.50000},

{shop,potato,2456,1.20000},

{shop,apple,20,2.30000}]

Note: The primary key of the shop table is the first column in the table,

that is, the item field in the shop record. The table is of type “set” (see a

discussion of the set and bag types in Section 15.2, Types of Table, on

page 275). If the newly created record has the same primary key as an

existing row in the database table, it will overwrite that row; otherwise,

a new row will be created.

Примечание: первичным ключом в таблице shop является первая колонка таблицы, то есть, наименование товара (item) в записи shop. Таблица создана с типом "set" (см. обсуждение типов "set" и "bag" в разделе 15.2, Types of Table, стр. 275). Если вновь созданная запись имеет такое же значение первичного ключа, как у уже существующей в БД строки, она перезапишет эту строку, в противном случае в БД добавится новая строка.

++Removing a Row

To remove a row, we need to know the object ID (OID) of the row. This

is formed from the table name and the value of the primary key:

++Удаление строки

Чтобы удалить строку, нам нужно знать ID объекта (OID) для этой строки.

OID составляется из имени таблицы и значения первичного ключа:

remove_shop_item(Item) ->

Oid = {shop, Item},

F = fun() ->

mnesia:delete(Oid)

end,

mnesia:transaction(F).

6> test_mnesia:remove_shop_item(pear).

{atomic,ok}

%% list the table -- the pear has gone

7> test_mnesia:demo(select_shop).

[{shop,orange,236,2.80000},

{shop,banana,420,4.50000},

{shop,potato,2456,1.20000},

{shop,apple,20,2.30000}]

[{shop,orange,236,2.80000},

8> mnesia:stop().

ok

+ 17.3 Mnesia Transactions

When we added or removed data from the database or performed a

query, we wrote the code something like this:

do_something(...) ->

F = fun() ->

% ...

mnesia:write(Row)

% ... or ...

mnesia:delete(Oid)

% ... or ...

qlc:e(Q)

end,

mnesia:transaction(F)

+ 17.3 Транзакции в Mnesia

Когда ранее мы добавляли или удаляли строки в/из БД, или выполняли запрос к БД, мы писали что-то вроде такого:

do_something(...) ->

F = fun() ->

% ...

mnesia:write(Row)

% ... or ...

mnesia:delete(Oid)

% ... or ...

qlc:e(Q)

end,

mnesia:transaction(F)

F - это анонимная функция без аргументов.

F is a fun with zero arguments. Inside F we called some combination of

mnesia:write/1, mnesia:delete/1, or qlc:e(Q) (where Q is a query compiled

with qlc:q/1). Having built the fun, we call mnesia:transaction(F), which

evaluates the expression sequence in the fun.

Внутри F мы вызывали некое сочетание

mnesia:write/1, mnesia:delete/1, или qlc:e(Q) (где Q это запрос, предварительно скомпилированный при помощи qlc:q/1).

Why do we do this? What does the transaction mean? To answer this,

suppose we have two processes that try to simultaneously access the

same data. For example, suppose I have $10 in my bank account.

Now suppose two people try to simultaneously withdraw $8 from that

account. What I would like to happen is that one of these transactions

succeeds and the other fails.

Зачем мы так делали? Что вообще значит "транзакция"? Чтобы пояснить это, предположим, что у нас есть два процесса, которые одновременно пытаются получить доступ к одним и тем же данным. Например, у меня есть 10 долларов на банковском счете. Теперь предположим, что два человека пытаются одновременно взять по 8 долларов с этого счета. В даном случае я бы хотел, чтобы одно из этих снятий произошло успешно, а другое - не произошло.

This is exactly the guarantee that mnesia:transaction/1 provides. Either

all the reads and writes to the tables in the database within a particular

transaction succeed, or none of them does. If none of them does, the

transaction is said to fail. If the transaction fails, no changes will be

made to the database.

Именно эту гарантию предоставляет нам mnesia:transaction/1.

Либо все чтения и записи данных в БД в пределах одной конкретно взятой транзакции завершаются успешно, либо не завершается успешно ни одна из этих операций.

Если ни одна из составляющих транзакцию операций не успешна, мы говорим, что транзакция терпит неудачу. Если транзакция терпит неудачу, никаких изменений в БД не производится.

The strategy that Mnesia uses for this is a form of pessimistic locking.

Whenever the Mnesia transaction manager accesses a table, it tries

to lock the record or the entire table depending upon the context. If

it detects that this might lead to deadlock, it immediately aborts the

transaction and undoes any changes it has made.

Стратегия, которой при этом следует Mnesia, является разновидностью пессимистических блокировок.

Всякий раз, когда менеджер транзакций Mnesia обращается к таблице, он пытается заблокировать запись или всю таблицу, в зависимости от ституации. Если он обнаруживает, что блокировка может привести к deadlock-у (?), он немедленно отменяет транзакцию и откатывает все изменения, которые в пределах этой транзакции были сделаны.

If the transaction initially fails because some other process is accessing

the data, the system waits for a short time and retries the transaction.

One consequence of this is that the code inside the transaction fun

might be evaluated a large number of times.

Если транзакция с самого начала терпит неудачу по причине того, что другой процесс занял данные, система делает небольшую паузу и повторяет транзакцию.

Одним из результатов такого поведения является то, что код внутри анонимной функции транзакции может выполняться большое количество раз.

For this reason, the code inside a transaction fun should not do any-

thing that has any side effects. For example, if we were to write the

following:

F = fun() ->

...

io:format("reading ..." ), %% don't do this

...

end,

mnesia:transaction(F),

we might get a lot of output, since the fun might be retried many times.

Именно по этой причине анонимная функция транзакции не должна делать ничего такого, что приводит к побочным эффектам. Например, если бы мы написали что-то такое:

F = fun() ->

...

io:format("reading ..." ), %% don't do this

...

end,

mnesia:transaction(F),

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

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

Примечание 2: Никогда не пишите код, который явно перехватывает исключения

от функций доступа Mnesia (mnesia:write/1, mnesia:delete/1, и т.п.),

поскольку механизм транзакций Mnesia сам основан на том, что эти функции

выбрасывают исключения при неудаче. Если вы будете перехватывать исключения и обрабатывать их сами, вы сломаете механизм транзакций.

Note 1: mnesia:write/1 and mnesia:delete/1 should be called only inside a

fun that is processed by mnesia:transaction/1.

Note 2: You should never write code to explicitly catch exceptions in

the Mnesia access functions (mnesia:write/1, mnesia:delete/1, and so on)

since the Mnesia transaction mechanism itself relies upon these func-

tions throwing exceptions on failure. If you catch these exceptions and

try to process them yourself, you will break the transaction mechanism.

Aborting a Transaction

Near to our shop, there’s a farm. And the farmer grows apples. The

farmer loves oranges, and he pays for the oranges with apples. The

going rate is two apples for each orange. So, to buy N oranges, the

farmer pays 2*N apples.

Here’s a function that updates the database when the farmer buys

some oranges:

++Отмена транзакции

Около нашего магазина есть ферма, где выращивают яблоки. Фермер любит апельсины, и покупает их по бартеру, на яблоки. "Курс обмена" - два к одному, то есть, чтобы купить N апельсинов, фермер даёт нам 2*N яблок.

Так выглядит функция, которая обновляет БД, когда фермер покупает апельсины:

farmer(Nwant) ->

%% Nwant = Number of oranges the farmer wants to buy

F = fun() ->

%% find the number of apples

[Apple] = mnesia:read({shop,apple}),

Napples = Apple#shop.quantity,

Apple1 = Apple#shop{quantity = Napples + 2*Nwant},

%% update the database

mnesia:write(Apple1),

%% find the number of oranges

[Orange] = mnesia:read({shop,orange}),

NOranges = Orange#shop.quantity,

if

NOranges >= Nwant ->

N1 = NOranges - Nwant,

Orange1 = Orange#shop{quantity=N1},

%% update the database

mnesia:write(Orange1);

true ->

%% Oops -- not enough oranges

mnesia:abort(oranges)

end

end,

mnesia:transaction(F).

Этот код, честно говоря, глуповат, но я так написал специально, чтобы продемонстрировать работу транзакций. Сначала я изменяю количество яблок в БД. Это делается ДО того, как я проверяю количество доступных апельсинов. Это специально, чтобы показать, что изменения можно "откатить", если транзакция неудачна. В обычной ситуации я бы отложил запись количества яблок, пока не убедился бы в наличии достаточного количества апельсинов. Посмотрим на это в действии. Утром фермер приходит и покупает 50 апельсинов.

This code is written in a pretty stupid way because I want to show how

the transaction mechanism works. First, I update the number of apples

in the database. This is done before I check the number of oranges.

The reason I do this is to show that this change gets “undone” if the

transaction fails. Normally, I’d delay writing the orange and apple data

back to the database until I was sure I had enough oranges.

Let’s show this in operation. In the morning, the farmer comes in and

buys 50 oranges:

1> test_mnesia:start().

ok

2> test_mnesia:reset_tables().

{atomic, ok}

%% List the shop table

3> test_mnesia:demo(select_shop).

[{shop,orange,100,3.80000},

{shop,pear,200,3.60000},

{shop,banana,420,4.50000},

{shop,potato,2456,1.20000},

{shop,apple,20,2.30000}]

%% The farmer buys 50 oranges

%% paying with 100 apples

4> test_mnesia:farmer(50).

{atomic,ok}

%% Print the shop table again

5> test_mnesia:demo(select_shop).

[{shop,orange,50,3.80000},

{shop,pear,200,3.60000},

{shop,banana,420,4.50000},

{shop,potato,2456,1.20000},

{shop,apple,120,2.30000}]

In the afternoon the farmer wants to buy 100 more oranges (boy, does

this guy love oranges):

После обеда он приходит снова и хочет купить ещё 100 апельсинов (надо же. как он любит апельсины!)

6> test_mnesia:farmer(100).

{aborted,oranges}

7> test_mnesia:demo(select_shop).

[{shop,orange,50,3.80000},

{shop,pear,200,3.60000},

{shop,banana,420,4.50000},

{shop,potato,2456,1.20000},

{shop,apple,120,2.30000}]

Why Is the DBMS Called Mnesia?

The original name was Amnesia. One of our bosses didn’t like

the name. He said, “You can’t possibly call it Amnesia—you

can’t have a database that forgets things!” So we dropped

the A, and the name stuck.

Почему СУБД называется Mnesia?

Изначально её название было "Amnesia". Одному из наших боссов это не понравилось, он сказал: "Нельзя называть её Amnesia - у нас не должно быть базы данных, которая, судя по названию, забывает всё подряд!" Так что мы убрали букву "A", и название прижилось.

When the transaction failed (when we called mnesia:abort(Reason)), the

changes made by mnesia:write were undone. Because of this, the data-

base state was restored to how it was before we entered the transaction.

Когда транзакция отменилась (когда мы вызвали mnesia:abort(Reason)), те изменения, которые были сделаны при помощи mnesia:write, были также отменены. Таким образом, состояние СУБД было восстановлено таким, каким оно было до начала транзакции.

++Loading the Test Data

++Загрузка тестовых данных

Now we know how transactions work, so we can look at the code for

loading the test data.

The function test_mnesia:example_tables/0 is used to provide data to ini-

tialize the database tables. The first element of the tuple is the table

name. This is followed by the table data in the order given in the origi-

nal record definitions.

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

Функция test_mnesia:example_tables/0 задает данные, необходимые для инициализации таблиц. Первый элемент кортежа задает имя таблицы, следом идет содержимое строки, в том же порядке, в котором поля были описаны при определении Erlang-записи.

example_tables() ->

[%% The shop table

{shop, apple,

20,

{shop, orange, 100,

{shop, pear,

200,

{shop, banana, 420,

{shop, potato, 2456,

%% The cost table

{cost, apple,

1.5},

{cost, orange, 2.4},

{cost, pear,

2.2},

{cost, banana, 1.5},

{cost, potato, 0.6}

].

2.3},

3.8},

3.6},

4.5},

1.2},

Here’s the code that inserts data into Mnesia from the example tables:

Следующий код вставляет данные в Mnesia из таблиц примера.

reset_tables() ->

mnesia:clear_table(shop),

mnesia:clear_table(cost),

F = fun() ->

foreach(fun mnesia:write/1, example_tables())

end,

mnesia:transaction(F).

This just calls mnesia:write for each tuple in the list returned by exam-

ple_tables/1

Он всего лишь вызывает mnesia:write для каждого кортежа из списка, который возвращает example_tables/1

++The do() Function

The do function called by demo/1 is slightly more complex:

Download test_mnesia.erl

do(Q) ->

F = fun() -> qlc:e(Q) end,

{atomic, Val} = mnesia:transaction(F),

Val.

++Функция do()

Функция do, которую вызывает demo/1, чуть сложнее:

do(Q) ->

F = fun() -> qlc:e(Q) end,

{atomic, Val} = mnesia:transaction(F),

Val.

This calls qlc:e(Q) inside an Mnesia transaction. Q is a compiled QLC

query, and qlc:e(Q) evaluates the query and returns all answers to the

query in a list. The return value {atomic, Val} means that the transaction

succeeded with value Val. Val is the value of the transaction function.

Она вызывает qlc:e(Q) в рамках транзакции Mnesia. Q - это скомпилированный запрос QLC, а qlc:e(Q) выполняет запрос и возвращает все ответы на запрос в виде списка. Возвращаемое значение {atomic, Val} означает, что транзакция завершилась успешно со значением Val. Val - это значение, возвращаемое анонимной функцией транзакции.

17.4 Storing Complex Data in Tables

If you’re a C programmer, how would you store a C struct in a SQL

database? Or if you’re a Java programmer, how would you store an

object in a SQL database? The answer—with great difficulty.

One of the disadvantages of using a conventional DBMS is that there

are a limited number of data types you can store in a table column.

You can store an integer, a string, a float, and so on. But if you want to

store a complex object, then you’re in trouble.

Mnesia is designed to store Erlang data structures. In fact, you can

store any Erlang data structure you want in an Mnesia table.

To illustrate this, we’ll suppose that a number of architects want to

store their designs in an Mnesia database. To start with, we must define

a record to represent their designs:

17.4 Сохранение сложных типов данных в Mnesia.

Если вы программируете на C, то как бы вы сохранили структуру C (struct) в базе данных SQL? Или, если вы программируете на Java, как бы вы стали сохранять в такой же базе объект? Ответ - с изрядными сложностями. Одной из проблем при использовании привычных СУБД является ограниченность встроенных типов колонок. Вы можете хранить в колонке БД целое число, строку, число с плавающей точкой, и т.п. Но если вы хотите сохранить сложный объект, это проблема.

Mnesia разработана так, чтобы хранить структуры данных Erlang. На самом деле, вы можете сохранять в таблице Mnesia любую структуру данных, которую можно создать в Erlang. Чтобы проиллюстрировать это, предположим, что некоторое количество архитекторов хотят сохранять в Mnesia свои чертежи. Для начала мы должны определить запись (record), которая будет представлять собой чертёж.

-record(design, {id, plan}).

Then we can define a function that adds some designs to the database:

Затем мы пишем функцию, которая добавляет несколько чертежей в БД:

Download test_mnesia.erl

add_plans() ->

D1 = #design{id

plan

D2 = #design{id

plan

=

=

=

=

{joe,1},

{circle,10}},

fred,

{rectangle,10,5}},

D3 = #design{id

= {jane,{house,23}},

plan = {house,

[{floor,1,

[{doors,3},

{windows,12},

{rooms,5}]},

{floor,2,

[{doors,2},

{rooms,4},

{windows,15}]}]}},

F = fun() ->

mnesia:write(D1),

mnesia:write(D2),

mnesia:write(D3)

end,

mnesia:transaction(F).

Now we can add some designs to the database:

Теперь мы можем добавить несколько чертежей в БД

1> test_mnesia:start().

ok

2> test_mnesia:add_plans().

{atomic,ok}

Now we have some plans in the database. We can extract these with the

following access function:

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

get_plan(PlanId) ->

F = fun() -> mnesia:read({design, PlanId}) end,

mnesia:transaction(F).

3> test_mnesia:get_plan(fred).

{atomic,[{design,fred,{rectangle,10,5}}]}

4> test_mnesia:get_plan({jane, {house,23}}).

{atomic,[{design,{jane,{house,23}},

{house,[{floor,1,[{doors,3},

{windows,12},

{rooms,5}]},

{floor,2,[{doors,2},

{rooms,4},

{windows,15}]}]}}]}

As you can see, both the database key and the extracted record can be arbitrary Erlang terms.

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

In technical terms, we say there is no “impedance mismatch” between

the data structures in the database and the data structures in our

programming language. This means that inserting and deleting complex

data structures into the database is very fast.

Говоря техническим языком, между структурами данных в БД и структурами в нашем языке программирования нет "разности сопротивлений". Это означает, что вставка и удаление сложных структур данных в/из БД работают очень быстро.

Fragmented Tables

Mnesia supports “fragmented tables.” (horizontal partitioning

in database terminology). This is designed for implementing

extremely large tables. The tables are split into fragments that

are stored on different machines. The fragments are themselves

Mnesia tables. The fragments can be replicated, have indexes,

and so on, as for any other table.

Refer to the Mnesia User’s Guide for more details.

Фрагментированные таблицы

Mnesia поддерживает "фрагментированные таблицы" (горизонтальное партицирование, если пользоваться терминологией СУБД). Это сделано для поддержки чрезвычайно больших таблиц. Таблицы разделены на фрагменты, которые сохраняются на разных компьютерах. Фрагменты сами по себе являются таблицами Mnesia. Фрагменты могут быть реплицируемы, могут иметь индексы, и тому подобное, как любые другие таблицы.

Обратитесь к документации Mnesia User Guide за дальнейшей информацией.

17.5 Table Types and Location

We can configure Mnesia tables in many different ways. First, tables

can be in RAM or on disk (or both). Second, tables can be located on a

single machine or replicated on several machines.

When we design our tables, we must think about the type of data we

want to store in the tables. Here are the properties of the tables:

17.5 Типы и расположения таблиц.

Мы можем настраивать таблицы Mnesia многими разными способами. Во-первых, таблицы могут храниться в RAM, на диске, и одновременно в RAM+на диске. Во-вторых, таблицы могут располагаться на одной машине или реплицироваться на несколько машин.

Когда мы проектируем нашу БД, мы должны думать о том, какого типа данные мы хотим хранить в тех или иных таблицах. Свойства таблицы могут быть такими:

RAM tables

These are very fast. The data in them is transient so it will be lost

if the machine crashes or when you stop the DBMS.

RAM tables

Эти таблицы работают очень быстро. Данные в них непостоянны, так как теряются при сбое или перезагрузке компьютера, или когда вы останавливаете СУБД.

Disk tables

Disk tables should survive a crash (provided the disk is not phys-

ically damaged).

Disk tables

Хранимые на диске таблицы должны выживать при сбоях (при условии, что диск физически не пострадал).

When an Mnesia transaction writes to a table and the table is

stored on disk, what actually happens is that the transaction data

is first written to a disk log. This disk log grows continuously, and

at regular intervals the information in the disk log is consolidated

with the other data in the database, and the entry in the disk log

is cleared. If the system crashes, then the next time the system

is restarted, the disk log is checked for consistency, and any out-

standing entries in the log are added to the database before the

database is made available. Once a transaction has succeeded,

the data should have been properly written to the disk log, and if

the system fails after this, then when the system is next restarted,

changes made in the transaction should survive the crash.

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

If the system crashes during a transaction, then the changes made

to the database should be lost.

Если же система упала во время транзакции, то все изменения, сделанные в пределах этой транзакции, должны быть потеряны.

Before using a RAM table, you need to perform some experiments to

see whether the entire table will fit into physical memory. If the RAM

tables don’t fit into physical memory, the system will page a lot, which

will be bad for performance.

Прежде чем использовать таблицы в RAM, нужно экспериментально убедиться, что вся таблица убирается в физически доступный объём памяти. Если таблица не влезает в физическую память, система будет активно использовать файл подкачки, а это обычно убивает производительность.

Since RAM tables are transient, we need to ask ourselves the ques-

tion, Does it matter whether all the data in our RAM table is lost? If

the answer is yes, we will need to replicate the RAM table on disk or

replicate on a second machine (as a RAM or disk table, or both).

Поскольку таблицы RAM непостоянны, мы должны спросить себя - будет ли плохо, если все данные в нашей таблице RAM вдруг исчезнут? Если ответ "да", то нам нужно реплицировать нашу таблицу на диск, или реплицировать её на другой компьютер (как таблицу RAM, как дисковую таблицу, или RAM+диск).

Creating Tables

To create a table, we call mnesia:create_table(Name, ArgS), where ArgS is

a list of {Key,Val} tuples. create_table returns {atomic, ok} if the table was

successfully created; otherwise, it returns {aborted, Reason}.

Some of the most common arguments to create_table are as follows:

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

Для того, чтобы создать таблицу, мы вызываем mnesia:create_table(Name, ArgS),

где ArgS - это список кортежей вида {Key,Val}.

create_table возвращает {atomic, ok}, если таблица была успешно создана; если нет - возвращает {aborted, Reason}.

Вот некоторые часто используемые аргументы для создания таблиц:

Name

This is the name of the table (an atom). By convention, it is the

name of an Erlang record—the table rows will be instances of this

record.

Name

Задаёт имя создаваемой таблицы (atom). По соглашению, это также имя записи Erlang (record) - строки в таблице будут являться экземплярами этой записи.

{type, Type}

This specifies the type of the table. Type is one of set, ordered_set,

or bag. These types have the same meaning as described in Sec-

tion 15.2, Types of Table, on page 275.

{type, Type}

Задаёт тип таблицы. Тип может быть одним из следующих: set, ordered_set,

или bag. Эти типы имеют такое же значение, как описано в разделе 15.2, Types of Table, на странице 275.

{disc_copies, NodeList}

NodeList is a list of the Erlang nodes where disk copies of the table

will be stored. When we use this option, the system will also create

a RAM copy of the table on the node where we performed this

operation.

It is possible to have a replicated table of type disc_copies on one

node and to have the same table stored as a different table type

on a different node. This is desirable if we want the following:

{disc_copies, NodeList}

NodeList задаёт список узлов Erlang, на которых будут храниться дисковые копии создаваемой таблицы. Когда мы используем эту опцию, система также создаёт RAM-копию таблицы на том узле, на котором было выполнено создание таблицы.

В принципе, допустимо иметь реплицируемую таблицу типа disc_copies на одном узле, плюс иметь ту же таблицу с другим типом на другом узле. Это делается, если мы хотим добиться следующего:

1. Read operations to be very fast and performed from RAM

2. Write operations to be performed to persistent storage

1. Чтобы операции чтения были очень быстрыми и выполнялись из RAM

2. При этом операции записи выполнялись бы в постоянное хранилище.

{ram_copies, NodeList}

NodeList is a list of the Erlang nodes where RAM copies of the table

will be stored.

{ram_copies, NodeList}

NodeList задаёт список узлов Erlang, на которых будут храниться копии в RAM.

{disc_only_copies, NodeList}

NodeList is a list of the Erlang nodes where disk-only copies of data

are stored. These tables have no RAM replicas and are slower to

access.

{disc_only_copies, NodeList}

NodeList задаёт список узлов Erlang, на которых будут храниться копии данных в режиме только-на-диск. Такие таблицы не имеют реплики в RAM, и, соответственно, доступ к ним медленнее.

{attributes, AtomList}

This is a list of the column names of the values in a particular

table. Note that to create a table containing the Erlang record xxx,

we can use the syntax {attribute, record_info(fields, xxx)} (alternatively

we can specify an explicit list of record field names).

{attributes, AtomList}

AtomList задаёт список имен колонок в данной таблице. Заметим, чтобы создать таблицу, содержащую экземпляры записи (record) с именем xxx, мы можем использовать синтаксис:

{attribute, record_info(fields, xxx)} (конечно, по-другому, можно явно указать список имен).

Note: There are more options to create_table than I have shown here.

Refer to the manual page for mnesia for details of all the options.

Примечание: у функции create_table есть и другие опции, здесь я показал не все. За более точной информацией обратитесь к документации по Mnesia.

Common Combinations of Table Attributes

In all the following, we’ll assume that Attrs is an {attributes,...} tuple.

Here are some common table configuration options that cover the most

common cases:

++ Часто используемые комбинации атрибутов таблиц

Во всех дальнейших примерах мы будем предполагать, что Attrs является кортежем вида {attributes,...}.

Приведём здесь некоторые часто используемые опции настройки таблиц, покрывающих большинство типовых случаев:

mnesia:create_table(shop, [Attrs])

• RAM resident on a single node.

• If the node crashes, the table will be lost.

• Fastest of all methods.

• Table must fit in memory.

mnesia:create_table(shop, [Attrs])

• Таблица хранится в RAM на единственном узле.

• Если узел падает, таблица будет потеряна.

• Самый быстрый из всех методов.

• Таблица должна помещаться в физическую память.

mnesia:create_table(shop,[Attrs,{disc_copies,[node()]}])

• RAM + disk copy on a single node.

• If the node crashes, the table will be recovered from disk.

• Fast reads, slower writes.

• The table should fit in memory.

mnesia:create_table(shop,[Attrs,{disc_copies,[node()]}])

• Таблица располагается в RAM + копия на диске, на единственном узле.

• Если узел падает, таблица будет восстановлена с диска.

• Быстрые операции чтения, запись более медленная.

• Таблица должна помещаться в физическую память.

mnesia:create_table(shop, [Attrs,{disc_only_copies,[node()]}])

• Disk-only copy on a single node.

• Large tables don’t have to fit in memory.

• Slower than with RAM replicas.

mnesia:create_table(shop, [Attrs,{disc_only_copies,[node()]}])

• Копия только-на-диске на единственном узле.

• Большие таблицы не обязаны помещаться полностью в память.

• Доступ медленнее, чем к таблицам с репликой в RAM.

mnesia:create_table(shop,

[Attrs,{ram_copies,[node(),someOtherNode()]})

• RAM resident table on two nodes.

• If both nodes crash, the table will be lost.

• The table must fit into memory.

• The table can be accessed on either node.

mnesia:create_table(shop,

[Attrs,{ram_copies,[node(),someOtherNode()]})

• Таблица хранится в RAM на двух разных узлах.

• Если оба узла падают, таблица будет потеряна.

• Таблица должна помещаться в физическую память.

• К таблице можно обращаться с любого из этих двух узлов.

mnesia:create_table(shop,

[Attrs, {disc_copies, [node(),someOtherNode()]}])

• Disk copies on two nodes.

• Can resume on either node.

• Survives double crash.

mnesia:create_table(shop,

[Attrs, {disc_copies, [node(),someOtherNode()]}])

• Дисковые копии на двух узлах.

• Таблица может быть восстановлена на любом из этих двух узлов.

• Таблица выживает после двойного сбоя.

++Table Behavior

When a table is replicated across several Erlang nodes, it is synchro-

nized as far as possible. If one node crashes, the system will still work,

but the number of replicas will be reduced. When the crashed node

comes back online, it will resynchronize with the other nodes where

the replicas are kept.

++Table Behavior

Когда таблица реплицируется на несколько узлов Erlang, она синхронизируется, пока это возможно.

Если один узел падает, система продолжает работать, но количество реплик уменьшается. Когда упавший узел возвращается в строй, он "догонит" состояние таблицы, обратившись к другим узлам, где хранятся актуальные реплики.

Note: Mnesia may become overloaded if the nodes running Mnesia stop

functioning. If you are using a laptop that goes to sleep, when it re-

starts, Mnesia might become temporarily overloaded and produce a

number of warning messages. We can ignore these messages.

Заметка: Mnesia может оказаться перегруженной, если узлы, на которых она запущена, прекращают работать.Если вы используете ноутбук, который "засыпает" при неактивности, то при "просыпании" Mnesia может оказаться временно перегруженной и выдавать множество сообщений о предупреждениях. На эти предупреждения можно не обращать внимания.

7.6 Creating the Initial Database

++7.6 Первоначальное создание базы данных

Here is a session that creates an Mnesia database. You need to do this

only once.

Вот так выглядят команды, создающие базу данных Mnesia database. Вам нужно выполнить это лишь один раз.

$ erl

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

ok

2> init:stop().

ok

$ ls

Mnesia.nonode@nohost

mnesia:create_schema(NodeList) initiates a new Mnesia database on all

the nodes in NodeList (which must be a list of valid Erlang nodes).

In our case, we gave the node list as [node()], that is, the current

node. Mnesia is initialized and creates a directory structure called Mne-

sia.nonode@nohost to store the database. Then we exit from the Erlang

shell and issue the operating system’s ls command to verify this.

Вызов функции mnesia:create_schema(NodeList) инициализирует новую базу данных Mnesia

на всех узлах из списка NodeList (он должен содержать список реально сушествующих узлов Erlang).

В нашем случае мы задаём список узлов, как [node()], то есть, содержащим лишь один, текущий, узел.

Mnesia инициализируется и создаёт на диске структуру каталогов с названием Mnesia.nonode@nohost, где физически хранит базу данных. Мы можем выйти из оболочки и выполнить команду ls, чтобы проверить это.

If we repeat the exercise with a distributed node called joe, we get the

following:

$ erl -name joe

(joe@doris.myerl.example.com) 1> mnesia:create_schema([node()]).

mnesia:create_schema([node()]).

ok

Если мы повторим это упражнение на запущенном в распределённом режиме узле с именем joe, получим такой результат:

$ erl -name joe

(joe@doris.myerl.example.com) 1> mnesia:create_schema([node()]).

mnesia:create_schema([node()]).

ok

2> init:stop().

ok

$ ls

Mnesia.joe@doris.myerl.example.com

Or we can point to a specific database when we start Erlang:

Иначе, мы можем указать путь к нужной нам конкретной базе данных при старте Erlang:

$ erl -mnesia dir '"/home/joe/some/path/to/Mnesia.company"'

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

ok

2> init:stop().

ok

/home/joe/some/path/to/Mnesia.company is the name of the directory in

which the database will be stored.

/home/joe/some/path/to/Mnesia.company - это путь к директории, где будет храниться база данных.

7.7 The Table Viewer

The table viewer is a GUI for viewing Mnesia and ETS tables. The com-

mand tv:start() starts the table viewer. You’ll see the initial display screen

similar to Figure 17.1. To see the tables in Mnesia, you have to select

View > Tab. We can see the table viewer displaying the shop table in

Figure 17.2, on the next page.

7.7 The Table Viewer

Просмотрщик таблиц представляет собой GUI для просмотра таблиц Mnesia, а также таблиц ETS.

Команда tv:start() запускает table viewer. Вы увидите начальный экран наподобие того, что приведен на рисунке 17.1. Чтобы увидеть таблицы Mnesia, вам нужно выбрать в меню View > Tab.

На рисунке 17.2 мы видим, как table viewer показывает нам таблицу "shop" (на следующей странице).

7.8 Digging Deeper

I hope I’ve whetted your appetite for Mnesia. Mnesia is a very power-

ful DBMS. It has been in production use in a number of demanding

telecom applications delivered by Ericsson since 1998.

+7.8 Digging Deeper

Я надеюсь, мне удалось пробудить ваш аппетит к Mnesia. Mnesia является очень мощной СУБД. Она работает в промышленном использовании в ряде весьма требовательных телекоммуникационных систем, выпускаемых компанией Ericsson с 1998 года.

Since this is a book about Erlang and not Mnesia, I can’t really do

any more than just give a few examples of the most common ways to

use Mnesia. The techniques I’ve shown in this chapter are those I use

myself. I don’t actually use (or understand) much more than what I’ve

shown you. But with what I’ve shown you, you can have a lot of fun

and build some pretty sophisticated applications.

Поскольку вы читаете книгу об Erlang, а не о СУБД Mnesia, мне придётся ограничиться лишь

несколькими примерами наиболее типовых способов использования Mnesia.

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

Я бы не сказал, что использую (и понимаю) сильно больше того, что я уже показал вам.

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

The main areas I’ve omitted are as follows:

• Backup and recovery: Mnesia has a range of options for config-

uring backup operations allowing for different types of disaster

recovery.

• Dirty operations: Mnesia allows a number of dirty operations

(dirty_read, dirty_write, ...). These are operations that are performed

outside a transaction context. These are very dangerous opera-

tions that can be used if you know that your application is single-

threaded or under other special circumstances. Dirty operations

are used for efficiency reasons.

• SNMP tables: Mnesia has a built-in SNMP table type. This makes

implementing SNMP management systems very easy.

Перечислю главные области, которые я не включил в эту главу:

• Резервирование и восстановление: у Mnesia есть ряд опций для настройки операций резервного копирования, подходящих для различных способов восстановления.

• "грязные" операции: Mnesia может выполнять ряд "грязных" операций

(dirty_read, dirty_write, ...). Это операции, которые выполняются

в обход транзакционного контекста.

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

"Грязные" операции используются в целях повышения эффективности.

• Таблицы SNMP: Mnesia предоставляет встроенный тип таблиц SNMP.

Это делает реализацию систем управления SNMP довольно простой.

The definitive reference to Mnesia is the Mnesia User’s Guide available

from the main Erlang distribution site (see Appendix C, on page 399). In

addition, the examples subdirectory in the Mnesia distribution

(/usr/local/lib/erlang/lib/mnesia-X.Y.Z/examples on my machine) has some

Mnesia examples.

Исчерпывающий справочник по СУБД Mnesia - это Mnesia User’s Guide, его можно найти на главном сайте дистрибутивов Erlang (см Приложение C, на стр. 399).

Дополнительно, поддиректория examples, включенная в дистрибутив Mnesia

(/usr/local/lib/erlang/lib/mnesia-X.Y.Z/examples на моей машине) содержит ряд примеров использования Mnesia.

7.9 Listings

Download test_mnesia