This chapter describes the Mnesia transaction system and the transaction properties which make Mnesia a fault tolerant, distributed database management system.
Also covered in this chapter are the locking functions, including table locks and sticky locks, as well as alternative functions which bypass the transaction system in favor of improved speed and reduced overheads. These functions are called "dirty operations". We also describe the usage of nested transactions. This chapter contains the following sections:
Transactions are an important tool when designing fault tolerant, distributed systems. A Mnesia transaction is a mechanism by which a series of database operations can be executed as one functional block. The functional block which is run as a transaction is called a Functional Object (Fun), and this code can read, write, or delete Mnesia records. The Fun is evaluated as a transaction which either commits, or aborts. If a transaction succeeds in executing Fun it will replicate the action on all nodes involved, or abort if an error occurs.
The following example shows a transaction which raises the salary of certain employee numbers.
raise(Eno, Raise) -> F = fun() -> [E] = mnesia:read(employee, Eno, write), Salary = E#employee.salary + Raise, New = E#employee{salary = Salary}, mnesia:write(New) end, mnesia:transaction(F).
The transaction raise(Eno, Raise) - >
contains a Fun
made up of four lines of code. This Fun is called by the statement
mnesia:transaction(F)
and returns a value.
The Mnesia transaction system facilitates the construction of reliable, distributed systems by providing the following important properties:
Atomicity means that database changes which are executed by a transaction take effect on all nodes involved, or on none of the nodes. In other words, the transaction either succeeds entirely, or it fails entirely.
Atomicity is particularly important when we want to
atomically write more than one record in the same
transaction. The raise/2
function, shown as an example
above, writes one record only. The insert_emp/3
function,
shown in the program listing in Chapter 2, writes the record
employee
as well as employee relations such as
at_dep
and in_proj
into the database. If we run
this latter code inside a transaction, then the transaction
handler ensures that the transaction either succeeds completely,
or not at all.
Mnesia is a distributed DBMS where data can be replicated on several nodes. In many such applications, it is important that a series of write operations are performed atomically inside a transaction. The atomicity property ensures that a transaction take effect on all nodes, or none at all.
Consistency. This transaction property ensures that a transaction always leaves the DBMS in a consistent state. For example, Mnesia ensures that inconsistencies will not occur if Erlang, Mnesia or the computer crashes while a write operation is in progress.
Isolation. This transaction property ensures that transactions which execute on different nodes in a network, and access and manipulate the same data records, will not interfere with each other.
The isolation property makes it possible to concurrently execute
the raise/2
function. A classical problem in concurrency control
theory is the so called "lost update problem".
The isolation property is extremely useful if the following circumstances occurs where an employee (with an employee number 123) and two processes, (P1 and P2), are concurrently trying to raise the salary for the employee. The initial value of the employees salary is, for example, 5. Process P1 then starts to execute, it reads the employee record and adds 2 to the salary. At this point in time, process P1 is for some reason preempted and process P2 has the opportunity to run. P2 reads the record, adds 3 to the salary, and finally writes a new employee record with the salary set to 8. Now, process P1 start to run again and writes its employee record with salary set to 7, thus effectively overwriting and undoing the work performed by process P2. The update performed by P2 is lost.
A transaction system makes it possible to concurrently execute two or more processes which manipulate the same record. The programmer does not need to check that the updates are synchronous, this is overseen by the transaction handler. All programs accessing the database through the transaction system may be written as if they had sole access to the data.
Durability. This transaction property ensures that changes made to the DBMS by a transaction are permanent. Once a transaction has been committed, all changes made to the database are durable - i.e. they are written safely to disc and will not be corrupted or disappear.
![]() |
The durability feature described does not entirely apply to situations where Mnesia is configured as a "pure" primary memory database. |
Different transaction managers employ different strategies to satisfy the isolation property. Mnesia uses the standard technique of two-phase locking. This means that locks are set on records before they are read or written. Mnesia uses five different kinds of locks.
Mnesia employs a strategy whereby functions such as
mnesia:read/1
acquire the necessary locks dynamically as
the transactions execute. Mnesia automatically sets and releases
the locks and the programmer does not have to code these
operations.
Deadlocks can occur when concurrent processes set and release locks on the same records. Mnesia employs a "wait-die" strategy to resolve these situations. If Mnesia suspects that a deadlock can occur when a transaction tries to set a lock, the transaction is forced to release all its locks and sleep for a while. The Fun in the transaction will be evaluated one more time.
For this reason, it is important that the code inside the Fun given to
mnesia:transaction/1
is pure. Some strange results can
occur if, for example, messages are sent by the transaction
Fun. The following example illustrates this situation:
bad_raise(Eno, Raise) -> F = fun() -> [E] = mnesia:read({employee, Eno}), Salary = E#employee.salary + Raise, New = E#employee{salary = Salary}, io:format("Trying to write ... ~n", []), mnesia:write(New) end, mnesia:transaction(F).
This transaction could write the text "Trying to write
... "
a thousand times to the terminal. Mnesia does guarantee,
however, that each and every transaction will eventually run. As a
result, Mnesia is not only deadlock free, but also livelock
free.
The Mnesia programmer cannot prioritize one particular transaction to execute before other transactions which are waiting to execute. As a result, the Mnesia DBMS transaction system is not suitable for hard real time applications. However, Mnesia contains other features that have real time properties.
Mnesia dynamically sets and releases locks as
transactions execute, therefore, it is very dangerous to execute code with
transaction side-effects. In particular, a receive
statement inside a transaction can lead to a situation where the
transaction hangs and never returns, which in turn can cause locks
not to release. This situation could bring the whole system to a
standstill since other transactions which execute in other
processes, or on other nodes, are forced to wait for the defective
transaction.
If a transaction terminates abnormally, Mnesia will automatically release the locks held by the transaction.
We have shown examples of a number of functions that can be used inside a transaction. The following list shows the simplest Mnesia functions that work with transactions. It is important to realize that these functions must be embedded in a transaction. If no enclosing transaction (or other enclosing Mnesia activity) exists, they will all fail.
mnesia:transaction(Fun) -> {aborted, Reason} |{atomic,
Value}
. This function executes one transaction with the
functional object Fun
as the single parameter.
mnesia:read({Tab, Key}) -> transaction abort |
RecordList
. This function reads all records with Key
as key from table Tab
. This function has the same semantics
regardless of the location of Table
. If the table is of
type bag
, the read({Tab, Key})
can return an arbitrarily
long list. If the table is of type set
, the list is
either of length one, or []
.
mnesia:wread({Tab, Key}) -> transaction abort |
RecordList
. This function behaves the same way as the
previously listed read/1
function, except that it
acquires a write lock instead of a read lock. If we execute a
transaction which reads a record, modifies the record, and then
writes the record, it is slightly more efficient to set the
write lock immediately. In cases where we issue a
mnesia:read/1
, followed by a mnesia:write/1
, the
first read lock must be upgraded to a write lock when the write
operation is executed.
mnesia:write(Record) -> transaction abort |
ok
. This function writes a record into the database. The
Record
argument is an instance of a record. The function
returns ok
, or aborts the transaction if an error should
occur.
mnesia:delete({Tab, Key}) -> transaction abort | ok
. This
function deletes all records with the given key.
mnesia:delete_object(Record) -> transaction abort |
ok
. This function deletes records with object id
Record
. This function is used when we want to delete only
some records in a table of type bag
.
As previously stated, the locking strategy used by Mnesia is to lock one record when we read a record, and lock all replicas of a record when we write a record. However, there are applications which use Mnesia mainly for its fault-tolerant qualities, and these applications may be configured with one node doing all the heavy work, and a standby node which is ready to take over in case the main node fails. Such applications may benefit from using sticky locks instead of the normal locking scheme.
A sticky lock is a lock which stays in place at a node after the transaction which first acquired the lock has terminated. To illustrate this, assume that we execute the following transaction:
F = fun() -> mnesia:write(#foo{a = kalle}) end, mnesia:transaction(F).
The foo
table is replicated on the two nodes N1
and N2
.
Normal locking requires:
If we use sticky locks, we must first change the code as follows:
F = fun() -> mnesia:s_write(#foo{a = kalle}) end, mnesia:transaction(F).
This code uses the s_write/1
function instead of the
write/1
function. The s_write/1
function sets a
sticky lock instead of a normal lock. If the table is not
replicated, sticky locks have no special effect. If the table is
replicated, and we set a sticky lock on node N1
, this
lock will then stick to node N1
. The next time we try to
set a sticky lock on the same record at node N1
, Mnesia
will see that the lock is already set and will not do a network
operation in order to acquire the lock.
It is much more efficient to set a local lock than it is to set a networked lock, and for this reason sticky locks can benefit application that use a replicated table and perform most of the work on only one of the nodes.
If a record is stuck at node N1
and we try to set a
sticky lock for the record on node N2
, the record must be
unstuck. This operation is expensive and will reduce performance. The unsticking is
done automatically if we issue s_write/1
requests at
N2
.
Mnesia supports read and write locks on whole tables as a complement to the normal locks on single records. As previously stated, Mnesia sets and releases locks automatically, and the programmer does not have to code these operations. However, transactions which read and write a large number of records in a specific table will execute more efficiently if we start the transaction by setting a table lock on this table. This will block other concurrent transactions from the table. The following two function are used to set explicit table locks for read and write operations:
mnesia:read_lock_table(Tab)
Sets a read lock on
the table Tab
mnesia:write_lock_table(Tab)
Sets a write lock on
the table Tab
Alternate syntax for acquisition of table locks is as follows:
mnesia:lock({table, Tab}, read) mnesia:lock({table, Tab}, write)
The matching operations in Mnesia may either lock the entire table or just a single record (when the key is bound in the pattern).
Write locks are normally acquired on all nodes where a replica of the table resides (and is active). Read locks are acquired on one node (the local one if a local replica exists).
The function mnesia:lock/2
is intended to support
table locks (as mentioned previously)
but also for situations when locks need to be
acquired regardless of how tables have been replicated:
mnesia:lock({global, GlobalKey, Nodes}, LockKind) LockKind ::= read | write | ...
The lock is acquired on the LockItem on all Nodes in the nodes list.
In many applications, the overhead of processing a transaction may result in a loss of performance. Dirty operation are short cuts which bypass much of the processing and increase the speed of the transaction.
Dirty operation are useful in many situations, for example in a datagram routing application where Mnesia stores the routing table, and it is time consuming to start a whole transaction every time a packet is received. For this reason, Mnesia has functions which manipulate tables without using transactions. This alternative to processing is known as a dirty operation. However, it is important to realize the trade-off in avoiding the overhead of transaction processing:
The major advantage of dirty operations is that they execute much faster than equivalent operations that are processed as functional objects within a transaction.
Dirty operations
are written to disc if they are performed on a table of type
disc_copies
, or type disc_only_copies
. Mnesia also
ensures that all replicas of a table are updated if a
dirty write operation is performed on a table.
A dirty operation will ensure a certain level of consistency. For example, it is not possible for dirty operations to return garbled records. Hence, each individual read or write operation is performed in an atomic manner.
All dirty functions execute a call to exit({aborted,
Reason})
on failure. The following functions are available:
mnesia:dirty_read({Tab, Key})
. This function reads
record(s) from Mnesia.
mnesia:dirty_write(Record)
. This function writes
the record Record
mnesia:dirty_delete({Tab, Key})
. This function deletes
record(s) with the key Key
.
mnesia:dirty_delete_object(Record)
This function is
the dirty operation alternative to the function
delete_object/1
mnesia:dirty_first(Tab)
. This function returns the
"first" key in the table Tab
.
Records in set
or bag
tables are not sorted.
However, there is
a record order which is not known to the user.
This means that it is possible to traverse a table by means of
this function in conjunction with the dirty_next/2
function.
'$end_of_table'
. It is not
recommended to use this atom as the key for any user
records.
mnesia:dirty_next(Tab, Key)
. This function returns
the "next" key in the table Tab
. This function makes it
possible to traverse a table and perform some operation on all
records in the table. When the end of the table is reached the
special key '$end_of_table'
is returned. Otherwise, the
function returns a key which can be used to read the actual
record.
dirty_next/2
function. This is because write
operations on a Mnesia table may lead to internal reorganizations
of the table itself. This is an implementation detail, but remember
the dirty functions are low level
functions. mnesia:read_lock_table(Tab)
may be used to
ensure that no transaction protected writes are performed during
the iteration.
mnesia:dirty_last(Tab)
This function works exactly as
mnesia:dirty_first/1
but returns the last object in
Erlang term order for the ordered_set
table type. For
all other table types, mnesia:dirty_first/1
and
mnesia:dirty_last/1
are synonyms.
mnesia:dirty_prev(Tab, Key)
This function works exactly as
mnesia:dirty_next/2
but returns the previous object in
Erlang term order for the ordered_set table type. For
all other table types, mnesia:dirty_next/2
and
mnesia:dirty_prev/2
are synonyms.
mnesia:dirty_slot(Tab, Slot)
dirty_next/2
function. A table has a
number of slots that range from zero to some unknown upper
bound. The function dirty_slot/2
returns the special
atom '$end_of_table'
when the end of the table is
reached.
mnesia:read_lock_table(Tab)
may be used to
ensure that no transaction protected writes are performed
during the iteration.
mnesia:dirty_update_counter({Tab,Key}, Val)
.
Counters are positive integers with a value greater than or
equal to zero. Updating a counter will add the Val
and
the counter where Val
is a positive or negative integer.
{TabName, Key,
Integer}
can be used as counters, and can be
persistent.
dirty_update_counter/2
function is
performed as an atomic operation although it is not protected by
a transaction. Accordingly, no table update is lost if two
processes simultaneously execute the
dirty_update_counter/2
function.
mnesia:dirty_match_object(Pat)
. This function is
the dirty equivalent of mnesia:match_object/1
.
mnesia:dirty_select(Tab, Pat)
. This function is
the dirty equivalent of mnesia:select/2
.
mnesia:dirty_index_match_object(Pat, Pos)
. This
function is the dirty equivalent of
mnesia:index_match_object/2
.
mnesia:dirty_index_read(Tab, SecondaryKey, Pos)
. This
function is the dirty equivalent of mnesia:index_read/3
.
mnesia:dirty_all_keys(Tab)
. This function is the
dirty equivalent of mnesia:all_keys/1
.
In Mnesia, all records in a table must have the same name. All
the records must be instances of the same
record type. The record name does however not necessarily be
the same as the table name. Even though that it is the case in
the most of the examples in this document. If a table is created
without the record_name
property the code below will
ensure all records in the tables have the same name as the table:
mnesia:create_table(subscriber, [])
However, if the table is is created with an explicit record name as argument, as shown below, it is possible to store subscriber records in both of the tables regardless of the table names:
TabDef = [{record_name, subscriber}], mnesia:create_table(my_subscriber, TabDef), mnesia:create_table(your_subscriber, TabDef).
In order to access such
tables it is not possible to use the simplified access functions
as described earlier in the document. For example,
writing a subscriber record into a table requires a
mnesia:write/3
function instead of the simplified functions
mnesia:write/1
and mnesia:s_write/1
:
mnesia:write(subscriber, #subscriber{}, write) mnesia:write(my_subscriber, #subscriber{}, sticky_write) mnesia:write(your_subscriber, #subscriber{}, write)
The following simplified piece of code illustrates the relationship between the simplified access functions used in most examples and their more flexible counterparts:
mnesia:dirty_write(Record) -> Tab = element(1, Record), mnesia:dirty_write(Tab, Record). mnesia:dirty_delete({Tab, Key}) -> mnesia:dirty_delete(Tab, Key). mnesia:dirty_delete_object(Record) -> Tab = element(1, Record), mnesia:dirty_delete_object(Tab, Record) mnesia:dirty_update_counter({Tab, Key}, Incr) -> mnesia:dirty_update_counter(Tab, Key, Incr). mnesia:dirty_read({Tab, Key}) -> Tab = element(1, Record), mnesia:dirty_read(Tab, Key). mnesia:dirty_match_object(Pattern) -> Tab = element(1, Pattern), mnesia:dirty_match_object(Tab, Pattern). mnesia:dirty_index_match_object(Pattern, Attr) Tab = element(1, Pattern), mnesia:dirty_index_match_object(Tab, Pattern, Attr). mnesia:write(Record) -> Tab = element(1, Record), mnesia:write(Tab, Record, write). mnesia:s_write(Record) -> Tab = element(1, Record), mnesia:write(Tab, Record, sticky_write). mnesia:delete({Tab, Key}) -> mnesia:delete(Tab, Key, write). mnesia:s_delete({Tab, Key}) -> mnesia:delete(Tab, Key, sticky_write). mnesia:delete_object(Record) -> Tab = element(1, Record), mnesia:delete_object(Tab, Record, write). mnesia:s_delete_object(Record) -> Tab = element(1, Record), mnesia:delete_object(Tab, Record. sticky_write). mnesia:read({Tab, Key}) -> mnesia:read(Tab, Key, read). mnesia:wread({Tab, Key}) -> mnesia:read(Tab, Key, write). mnesia:match_object(Pattern) -> Tab = element(1, Pattern), mnesia:match_object(Tab, Pattern, read). mnesia:index_match_object(Pattern, Attr) -> Tab = element(1, Pattern), mnesia:index_match_object(Tab, Pattern, Attr, read).
As previously described, a functional object (Fun) performing
table access operations as listed below may be
passed on as arguments to the function
mnesia:transaction/1,2,3
:
These functions will be performed in a
transaction context involving mechanisms like locking, logging,
replication, checkpoints, subscriptions, commit protocols
etc.However, the same function may also be
evaluated in other activity contexts.
The following activity access contexts are currently supported:
By passing the same "fun" as argument to the function
mnesia:sync_transaction(Fun [, Args])
it will be performed
in synced transaction context. Synced transactions waits until all
active replicas has committed the transaction (to disc) before
returning from the mnesia:sync_transaction call. Using
sync_transaction is useful for applications that are executing on
several nodes and want to be sure that the update is performed on
the remote nodes before a remote process is spawned or a message
is sent to a remote process, and also when combining transaction
writes with dirty_reads. This is also useful in situations where
an application performs frequent or voluminous updates which may
overload Mnesia on other nodes.
By passing the same "fun" as argument to the function
mnesia:async_dirty(Fun [, Args])
it will be performed in
dirty context. The function calls will be mapped to the
corresponding dirty functions. This will still involve logging,
replication and subscriptions but there will be no locking,
local transaction storage or commit protocols involved.
Checkpoint retainers will be updated but will be updated
"dirty". Thus, they will be updated asynchronously. The
functions will wait for the operation to be performed on one
node but not the others. If the table resides locally no waiting
will occur.
By passing the same "fun" as an argument to the function
mnesia:sync_dirty(Fun [, Args])
it will be performed in
almost the same context as mnesia:async_dirty/1,2
. The
difference is that the operations are performed
synchronously. The caller will wait for the updates to be
performed on all active replicas. Using sync_dirty is useful for
applications that are executing on several nodes and want to be
sure that the update is performed on the remote nodes before a remote
process is spawned or a message is sent to a remote process. This
is also useful in situations where an application performs frequent or
voluminous updates which may overload Mnesia on other
nodes.
Mnesia tables with storage type RAM_copies and disc_copies
are implemented internally as "ets-tables" and
it is possible for applications to access the these tables
directly. This is only recommended if all options have been weighed
and the possible outcomes are understood. By passing the earlier
mentioned "fun" to the function
mnesia:ets(Fun [, Args])
it will be performed but in a very raw
context. The operations will be performed directly on the
local ets tables assuming that the local storage type are
RAM_copies and that the table is not replicated on other
nodes. Subscriptions will not be triggered nor
checkpoints updated, but this operation is blindingly fast. Disc resident
tables should not be updated with the ets-function since the
disc will not be updated.
The Fun may also be passed as an argument to the function
mnesia:activity/2,3,4
which enables usage of customized
activity access callback modules. It can either be obtained
directly by stating the module name as argument or implicitly
by usage of the access_module
configuration parameter. A
customized callback module may be used for several purposes,
such as providing triggers, integrity constraints, run time
statistics, or virtual tables.
The callback module does
not have to access real Mnesia tables, it is free to do whatever
it likes as long as the callback interface is fulfilled.
In Appendix C "The Activity Access Call Back Interface" the source
code for one alternate implementation is provided
(mnesia_frag.erl). The context sensitive function
mnesia:table_info/2
may be used to provide virtual
information about a table. One usage of this is to perform
QLC
queries within an activity context with a
customized callback module. By providing table information about
table indices and other QLC
requirements,
QLC
may be used as a generic query language to
access virtual tables.
QLC queries may be performed in all these activity contexts (transaction, sync_transaction, async_dirty, sync_dirty and ets). The ets activity will only work if the table has no indices.
![]() |
The mnesia:dirty_* function always executes with async_dirty semantics regardless of which activity access contexts are invoked. They may even invoke contexts without any enclosing activity access context. |
Transactions may be nested in an arbitrary fashion. A child transaction
must run in the same process as its parent. When a child transaction
aborts, the caller of the child transaction will get the
return value {aborted, Reason}
and any work performed
by the child will be erased. If a child transaction commits, the
records written by the child will be propagated to the parent.
No locks are released when child transactions terminate. Locks
created by a sequence of nested transactions are kept until
the topmost transaction terminates. Furthermore, any updates
performed by a nested transaction are only propagated
in such a manner so that the parent of the nested transaction
sees the updates. No final commitment will be done until
the top level transaction is terminated.
So, although a nested transaction returns {atomic, Val}
,
if the enclosing parent transaction is aborted, the entire
nested operation is aborted.
The ability to have nested transaction with identical semantics as top level transaction makes it easier to write library functions that manipulate mnesia tables.
Say for example that we have a function that adds a new subscriber to a telephony system:
add_subscriber(S) -> mnesia:transaction(fun() -> case mnesia:read( ..........
This function needs to be called as a transaction.
Now assume that we wish to write a function that
both calls the add_subscriber/1
function and
is in itself protected by the context of a transaction.
By simply calling the add_subscriber/1
from within
another transaction, a nested transaction is created.
It is also possible to mix different activity access contexts while nesting, but the dirty ones (async_dirty,sync_dirty and ets) will inherit the transaction semantics if they are called inside a transaction and thus it will grab locks and use two or three phase commit.
add_subscriber(S) -> mnesia:transaction(fun() -> %% Transaction context mnesia:read({some_tab, some_data}), mnesia:sync_dirty(fun() -> %% Still in a transaction context. case mnesia:read( ..) ..end), end). add_subscriber2(S) -> mnesia:sync_dirty(fun() -> %% In dirty context mnesia:read({some_tab, some_data}), mnesia:transaction(fun() -> %% In a transaction context. case mnesia:read( ..) ..end), end).
When it is not possible to use mnesia:read/3
Mnesia
provides the programmer with several functions for matching
records against a pattern. The most useful functions of these are:
mnesia:select(Tab, MatchSpecification, LockKind) -> transaction abort | [ObjectList] mnesia:select(Tab, MatchSpecification, NObjects, Lock) -> transaction abort | {[Object],Continuation} | '$end_of_table' mnesia:select(Cont) -> transaction abort | {[Object],Continuation} | '$end_of_table' mnesia:match_object(Tab, Pattern, LockKind) -> transaction abort | RecordList
These functions matches a Pattern
against all records in
table Tab
. In a mnesia:select
call Pattern
is
a part of MatchSpecification
described below. It is not
necessarily performed as an exhaustive search of the entire
table. By utilizing indices and bound values in the key of the
pattern, the actual work done by the function may be condensed
into a few hash lookups. Using ordered_set
tables may reduce the
search space if the keys are partially bound.
The pattern provided to the functions must be a valid record,
and the first element of the provided tuple must be the
record_name
of the table. The special element '_'
matches any data structure in Erlang (also known as an Erlang
term). The special elements '$<number>'
behaves as Erlang
variables i.e. matches anything and binds the first occurrence and
matches the coming occurrences of that variable against the bound value.
Use the function mnesia:table_info(Tab, wild_pattern)
to obtain a basic pattern which matches all records in a table
or use the default value in record creation.
Do not make the pattern hard coded since it will make your code more
vulnerable to future changes of the record definition.
Wildpattern = mnesia:table_info(employee, wild_pattern), %% Or use Wildpattern = #employee{_ = '_'},
For the employee table the wild pattern will look like:
{employee, '_', '_', '_', '_', '_',' _'}.
In order to constrain the match you must replace some
of the '_'
elements. The code for matching out
all female employees, looks like:
Pat = #employee{sex = female, _ = '_'}, F = fun() -> mnesia:match_object(Pat) end, Females = mnesia:transaction(F).
It is also possible to use the match function if we want to check the equality of different attributes. Assume that we want to find all employees which happens to have a employee number which is equal to their room number:
Pat = #employee{emp_no = '$1', room_no = '$1', _ = '_'}, F = fun() -> mnesia:match_object(Pat) end, Odd = mnesia:transaction(F).
The function mnesia:match_object/3
lacks some important
features that mnesia:select/3
have. For example
mnesia:match_object/3
can only return the matching records,
and it can not express constraints other then equality.
If we want to find the names of the male employees on the second floor
we could write:
MatchHead = #employee{name='$1', sex=male, room_no={'$2', '_'}, _='_'}, Guard = [{'>=', '$2', 220},{'<', '$2', 230}], Result = '$1', mnesia:select(employee,[{MatchHead, Guard, [Result]}])
Select can be used to add additional constraints and create
output which can not be done with mnesia:match_object/3
.
The second argument to select is a MatchSpecification
.
A MatchSpecification
is list of MatchFunctions
, where
each MatchFunction
consists of a tuple containing
{MatchHead, MatchCondition, MatchBody}
. MatchHead
is the same pattern used in mnesia:match_object/3
described above. MatchCondition
is a list of additional
constraints applied to each record, and MatchBody
is used
to construct the return values.
A detailed explanation of match specifications can be found in the Erts users guide: Match specifications in Erlang , and the ets/dets documentations may provide some additional information.
The functions select/4
and select/1
are used to
get a limited number of results, where the Continuation
are used to get the next chunk of results. Mnesia uses the
NObjects
as an recommendation only, thus more or less
results then specified with NObjects
may be returned in
the result list, even the empty list may be returned despite there
are more results to collect.
![]() |
There is a severe performance penalty in using
|
If the key attribute is bound in a pattern, the match operation
is very efficient. However, if the key attribute in a pattern is
given as '_'
, or '$1'
, the whole employee
table must be searched for records that match. Hence if the table is
large, this can become a time consuming operation, but it can be
remedied with indices (refer to Chapter 5: Indexing) if
mnesia:match_object
is used.
QLC queries can also be used to search Mnesia tables. By
using mnesia:table/[1|2]
as the generator inside a QLC
query you let the query operate on a mnesia table. Mnesia
specific options to mnesia:table/2
are {lock, Lock},
{n_objects,Integer} and {traverse, SelMethod}. The lock
option specifies whether mnesia should acquire a read or write
lock on the table, and n_objects
specifies how many
results should be returned in each chunk to QLC. The last option is
traverse
and it specifies which function mnesia should
use to traverse the table. Default select
is used, but by using
{traverse, {select, MatchSpecification}}
as an option to
mnesia:table/2
the user can specify it's own view of the
table.
If no options are specified a read lock will acquired and 100 results will be returned in each chunk, and select will be used to traverse the table, i.e.:
mnesia:table(Tab) -> mnesia:table(Tab, [{n_objects,100},{lock, read}, {traverse, select}]).
The function mnesia:all_keys(Tab)
returns all keys in a
table.
Mnesia provides a couple of functions which iterates over all the records in a table.
mnesia:foldl(Fun, Acc0, Tab) -> NewAcc | transaction abort mnesia:foldr(Fun, Acc0, Tab) -> NewAcc | transaction abort mnesia:foldl(Fun, Acc0, Tab, LockType) -> NewAcc | transaction abort mnesia:foldr(Fun, Acc0, Tab, LockType) -> NewAcc | transaction abort
These functions iterate over the mnesia table Tab
and
apply the function Fun
to each record. The Fun
takes two arguments, the first argument is a record from the
table and the second argument is the accumulator. The
Fun
return a new accumulator.
The first time the Fun
is applied Acc0
will
be the second argument. The next time the Fun
is called
the return value from the previous call, will be used as the
second argument. The term the last call to the Fun returns
will be the return value of the fold[lr]
function.
The difference between foldl
and foldr
is the
order the table is accessed for ordered_set
tables,
for every other table type the functions are equivalent.
LockType
specifies what type of lock that shall be
acquired for the iteration, default is read
. If
records are written or deleted during the iteration a write
lock should be acquired.
These functions might be used to find records in a table
when it is impossible to write constraints for
mnesia:match_object/3
, or when you want to perform
some action on certain records.
For example finding all the employees who has a salary below 10 could look like:
find_low_salaries() -> Constraint = fun(Emp, Acc) when Emp#employee.salary < 10 -> [Emp | Acc]; (_, Acc) -> Acc end, Find = fun() -> mnesia:foldl(Constraint, [], employee) end, mnesia:transaction(Find).
Raising the salary to 10 for everyone with a salary below 10 and return the sum of all raises:
increase_low_salaries() -> Increase = fun(Emp, Acc) when Emp#employee.salary < 10 -> OldS = Emp#employee.salary, ok = mnesia:write(Emp#employee{salary = 10}), Acc + 10 - OldS; (_, Acc) -> Acc end, IncLow = fun() -> mnesia:foldl(Increase, 0, employee, write) end, mnesia:transaction(IncLow).
A lot of nice things can be done with the iterator functions but some caution should be taken about performance and memory utilization for large tables.
Call these iteration functions on nodes that contain a replica of the
table. Each call to the function Fun
access the table and if the table
resides on another node it will generate a lot of unnecessary
network traffic.
If records are written and deleted during the traversal, use
mnesia:fold[lr]/4
with a write
lock.
Writing or deleting in transaction context creates a local
copy of each modified record, so modifying each record in
a large table uses a lot of memory. In dirty context, i.e.
sync_dirty
or async_dirty
, the modified records
are not stored in a local copy; instead, each record is updated
separately. This generates a lot of network traffic if the
table has a replica on another node and has all the other
drawbacks that dirty operations have.