View Source Transactions and Other Access Contexts
This section describes the Mnesia
transaction system and the transaction
properties that make Mnesia
a fault-tolerant, distributed Database Management
System (DBMS).
This section also describes the locking functions, including table locks and sticky locks, as well as alternative functions that bypass the transaction system in favor of improved speed and reduced overhead. These functions are called "dirty operations". The use of nested transactions is also described. The following topics are included:
- Transaction properties, which include atomicity, consistency, isolation, and durability
- Locking
- Dirty operations
- Record names versus table names
- Activity concept and various access contexts
- Nested transactions
- Pattern matching
- Iteration
Transaction Properties
Transactions are important 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 that is run as a
transaction is called a Functional Object (Fun), and this code can read, write,
and delete Mnesia
records. The Fun is evaluated as a transaction that either
commits or terminates. If a transaction succeeds in executing the Fun, it
replicates the action on all nodes involved, or terminates if an error occurs.
The following example shows a transaction that 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 function raise/2
contains a Fun made up of four code lines. 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:
- The transaction handler ensures that a Fun, which is placed inside a transaction, does not interfere with operations embedded in other transactions when it executes a series of operations on tables.
- The transaction handler ensures that either all operations in the transaction are performed successfully on all nodes atomically, or the transaction fails without permanent effect on any node.
- The
Mnesia
transactions have four important properties, called Atomicity, Consistency, Isolation, and Durability (ACID). These properties are described in the following sections.
Atomicity
Atomicity means that database changes that are executed by a transaction take effect on all nodes involved, or on none of the nodes. That is, the transaction either succeeds entirely, or it fails entirely.
Atomicity is important when it is needed to write atomically more than one
record in the same transaction. The function raise/2
, shown in the previous
example, writes one record only. The function insert_emp/3
, shown in the
program listing in Getting Started, writes
the record employee
as well as employee relations, such as at_dep
and
in_proj
, into the database. If this latter code is run inside a transaction,
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 applications, it is important that a series of write operations are
performed atomically inside a transaction. The atomicity property ensures that a
transaction takes effect on all nodes, or none.
Consistency
The consistency property ensures that a transaction always leaves the DBMS in a
consistent state. For example, Mnesia
ensures that no inconsistencies occur if
Erlang, Mnesia
, or the computer crashes while a write operation is in
progress.
Isolation
The isolation property ensures that transactions that execute on different nodes
in a network, and access and manipulate the same data records, do not interfere
with each other. The isolation property makes it possible to execute the
function raise/2
concurrently. A classical problem in concurrency control
theory is the "lost update problem".
The isolation property is in particular useful if the following circumstances occur where an employee (with employee number 123) and two processes (P1 and P2) are concurrently trying to raise the salary for the employee:
- Step 1: The initial value of the employees salary is, for example, 5. Process P1 starts to execute, reads the employee record, and adds 2 to the salary.
- Step 2: Process P1 is for some reason pre-empted and process P2 has the opportunity to run.
- Step 3: Process P2 reads the record, adds 3 to the salary, and finally writes a new employee record with the salary set to 8.
- Step 4: Process P1 starts 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 execute two or more processes concurrently that 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 can be written as if they had sole access to the data.
Durability
The durability property ensures that changes made to the DBMS by a transaction are permanent. Once a transaction is committed, all changes made to the database are durable, that is, they are written safely to disc and do not become corrupted and do not disappear.
Note
The described durability feature does not entirely apply to situations where
Mnesia
is configured as a "pure" primary memory database.
Locking
Different transaction managers employ different strategies to satisfy the
isolation property. Mnesia
uses the standard technique of two phase locking.
That is, locks are set on records before they are read or written. Mnesia
uses
the following lock types:
- Read locks. A read lock is set on one replica of a record before it can be read.
- Write locks. Whenever a transaction writes to a record, write locks are first set on all replicas of that particular record.
- Read table locks. If a transaction traverses an entire table in search for a
record that satisfies some particular property, it is most inefficient to set
read locks on the records one by one. It is also memory consuming, as the read
locks themselves can take up considerable space if the table is large.
Therefore,
Mnesia
can set a read lock on an entire table. - Write table locks. If a transaction writes many records to one table, a write lock can be set on the entire table.
- Sticky locks. These are write locks that stay in place at a node after the transaction that initiated the lock has terminated.
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 need 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 is evaluated once more.
It is therefore 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 can write the text "Trying to write ... "
1000 times to the
terminal. However, Mnesia
guarantees that each 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 that 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. It is
therefore 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 can bring the whole system to a standstill, as other
transactions that execute in other processes, or on other nodes, are forced to
wait for the defective transaction.
If a transaction terminates abnormally, Mnesia
automatically releases the
locks held by the transaction.
Up to now, examples of a number of functions that can be used inside a
transaction have been shown. The following list shows the simplest Mnesia
functions that work with transactions. Notice that these functions must be
embedded in a transaction. If no enclosing transaction (or other enclosing
Mnesia
activity) exists, they all fail.
- mnesia:transaction(Fun) -> {aborted, Reason} |{atomic, Value}
executes one transaction with the functional object
Fun
as the single parameter. mnesia:read({Tab, Key}) -> transaction abort | RecordList reads all records with
Key
as key from tableTab
. This function has the same semantics regardless of the location ofTable
. If the table is of typebag
,read({Tab, Key})
can return an arbitrarily long list. If the table is of typeset
, the list is either of length one or[]
.mnesia:wread({Tab, Key}) -> transaction abort | RecordList behaves the same way as the previously listed function
read/1
, except that it acquires a write lock instead of a read lock. To execute a transaction that reads a record, modifies the record, and then writes the record, it is slightly more efficient to set the write lock immediately. When amnesia:read/1
is issued, followed by amnesia: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 writes a record into the database. Argument
Record
is an instance of a record. The function returnsok
, or terminates the transaction if an error occurs.mnesia:delete({Tab, Key}) -> transaction abort | ok deletes all records with the given key.
mnesia:delete_object(Record) -> transaction abort | ok deletes records with the OID
Record
. Use this function to delete only some records in a table of typebag
.
Sticky Locks
As previously stated, the locking strategy used by Mnesia
is to lock one
record when reading a record, and lock all replicas of a record when writing a
record. However, some applications use Mnesia
mainly for its fault-tolerant
qualities. These applications can be configured with one node doing all the
heavy work, and a standby node that is ready to take over if the main node
fails. Such applications can benefit from using sticky locks instead of the
normal locking scheme.
A sticky lock is a lock that stays in place at a node, after the transaction that first acquired the lock has terminated. To illustrate this, assume that the following transaction is executed:
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 the following:
- One network RPC (two messages) to acquire the write lock
- Three network messages to execute the two-phase commit protocol
If sticky locks are used, the code must first be changed as follows:
F = fun() ->
mnesia:s_write(#foo{a = kalle})
end,
mnesia:transaction(F).
This code uses the function s_write/1 instead of the
function write/1 The function s_write/1
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 a sticky lock is set on node
N1
, this lock then sticks to node N1
. The next time you try to set a sticky
lock on the same record at node N1
, Mnesia
detects that the lock is already
set and do no network operation to acquire the lock.
It is more efficient to set a local lock than it is to set a networked lock. Sticky locks can therefore benefit an application that uses a replicated table and perform most of the work on only one of the nodes.
If a record is stuck at node N1
and you try to set a sticky lock for the
record on node N2
, the record must be unstuck. This operation is expensive and
reduces performance. The unsticking is done automatically if you issue
s_write/1
requests at N2
.
Table Locks
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 need to code these operations.
However, transactions that read and write many records in a specific table
execute more efficiently if the transaction is started by setting a table lock
on this table. This blocks other concurrent transactions from the table. The
following two functions are used to set explicit table locks for read and write
operations:
- mnesia:read_lock_table(Tab) sets a read lock on
table
Tab
. - mnesia:write_lock_table(Tab) sets a write lock
on table
Tab
.
Alternative syntax for acquisition of table locks is as follows:
mnesia:lock({table, Tab}, read)
mnesia:lock({table, Tab}, write)
The matching operations in Mnesia
can either lock the entire table or only a
single record (when the key is bound in the pattern).
Global Locks
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 LockItem
on all nodes in the node list.
Dirty Operations
In many applications, the overhead of processing a transaction can result in a loss of performance. Dirty operation are short cuts that bypass much of the processing and increase the speed of the transaction.
Dirty operation are often useful, 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. Mnesia
has therefore
functions that manipulate tables without using transactions. This alternative to
processing is known as a dirty operation. However, notice the trade-off in
avoiding the overhead of transaction processing:
- The atomicity and the isolation properties of
Mnesia
are lost. - The isolation property is compromised, because other Erlang processes, which use transaction to manipulate the data, do not get the benefit of isolation if dirty operations simultaneously are used to read and write records from the same table.
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 ensures a certain level of consistency. For example, dirty operations cannot 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. Even if the following functions are executed inside a transaction no
locks are acquired. The following functions are available:
mnesia:dirty_read({Tab, Key}) reads one or more records from
Mnesia
.mnesia:dirty_write(Record) writes the record
Record
.mnesia:dirty_delete({Tab, Key}) deletes one or more records with key
Key
.mnesia:dirty_delete_object(Record) is the dirty operation alternative to the function delete_object/1.
mnesia:dirty_first(Tab) returns the "first" key in table
Tab
.Records in
set
orbag
tables are not sorted. However, there is a record order that is unknown to the user. This means that a table can be traversed by this function with the functionmnesia:dirty_next/2
.If there are no records in the table, this function returns the atom
'$end_of_table'
. It is not recommended to use this atom as the key for any user records.mnesia:dirty_next(Tab, Key) returns the "next" key in 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 that can be used to read the actual record.The behavior is undefined if any process performs a write operation on the table while traversing the table with the function dirty_next/2 This is because
write
operations on aMnesia
table can lead to internal reorganizations of the table itself. This is an implementation detail, but remember that the dirty functions are low-level functions.mnesia:dirty_last(Tab) works exactly like
mnesia:dirty_first/1
but returns the last object in Erlang term order for the table typeordered_set
. For all other table types,mnesia:dirty_first/1
andmnesia:dirty_last/1
are synonyms.mnesia:dirty_prev(Tab, Key) works exactly like
mnesia:dirty_next/2
but returns the previous object in Erlang term order for the table typeordered_set
. For all other table types,mnesia:dirty_next/2
andmnesia:dirty_prev/2
are synonyms.The behavior of this function is undefined if the table is written on while being traversed. The function mnesia:read_lock_table(Tab) can 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 adds
Val
and the counter whereVal
is a positive or negative integer.Mnesia
has no special counter records. However, records of the form{TabName, Key, Integer}
can be used as counters, and can be persistent.Transaction-protected updates of counter records are not possible.
There are two significant differences when using this function instead of reading the record, performing the arithmetic, and writing the record:
- It is much more efficient.
- The function dirty_update_counter/2 is
performed as an atomic operation although it is not protected by a
transaction. Therefore no table update is lost if two processes
simultaneously execute the function
dirty_update_counter/2
.
mnesia:dirty_match_object(Pat) is the dirty equivalent of
mnesia:match_object/1
.mnesia:dirty_select(Tab, Pat) is the dirty equivalent of
mnesia:select/2
.mnesia:dirty_index_match_object(Pat, Pos) is the dirty equivalent of
mnesia:index_match_object/2
.mnesia:dirty_index_read(Tab, SecondaryKey, Pos) is the dirty equivalent of
mnesia:index_read/3
.mnesia:dirty_all_keys(Tab) is the dirty equivalent of
mnesia:all_keys/1
.
Record Names versus Table Names
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, however, does not
necessarily have to be the same as the table name, although this is the case in
most of the examples in this User's Guide. If a table is created without
property record_name
, the following code ensures that all records in the
tables have the same name as the table:
mnesia:create_table(subscriber, [])
However, if the table is created with an explicit record name as argument, as shown in the following example, subscriber records can be stored 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).
To access such tables, simplified access functions (as described earlier) cannot
be used. For example, writing a subscriber record into a table requires the
function mnesia:write/3
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 simple code illustrates the relationship between the simplified access functions used in most of the 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).
Activity Concept and Various Access Contexts
As previously described, a Functional Object (Fun) performing table access operations, as listed here, can be passed on as arguments to the function mnesia:transaction/1,2,3:
- mnesia:write/3 (write/1, s_write/1)
mnesia:delete/3
(mnesia:delete/1
,mnesia:s_delete/1
)mnesia:delete_object/3
(mnesia:delete_object/1
,mnesia:s_delete_object/1
)mnesia:read/3
(mnesia:read/1
,mnesia:wread/1
)- mnesia:match_object/2 (
mnesia:match_object/1
) - mnesia:select/3 (
mnesia:select/2
) mnesia:foldl/3
(mnesia:foldl/4
,mnesia:foldr/3
,mnesia:foldr/4
)mnesia:all_keys/1
mnesia:index_match_object/4
(mnesia:index_match_object/2
)mnesia:index_read/3
mnesia:lock/2
(mnesia:read_lock_table/1
,mnesia:write_lock_table/1
)mnesia:table_info/2
These functions are performed in a transaction context involving mechanisms, such as locking, logging, replication, checkpoints, subscriptions, and commit protocols. However, the same function can also be evaluated in other activity contexts.
The following activity access contexts are currently supported:
transaction
sync_transaction
async_dirty
sync_dirty
ets
By passing the same "fun" as argument to the function
[mnesia:sync_transaction(Fun [, Args])](mnesia:sync_transaction/1
) it is
performed in synced transaction context. Synced transactions wait until all
active replicas has committed the transaction (to disc) before returning from
the mnesia:sync_transaction
call. Using sync_transaction
is useful in the
following cases:
- When an application executes on several nodes and wants 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.
- When a combining transaction writes with "dirty_reads", that is, the functions
dirty_match_object
,dirty_read
,dirty_index_read
,dirty_select
, and so on. - When an application performs frequent or voluminous updates that can overload
Mnesia
on other nodes.
By passing the same "fun" as argument to the function [mnesia:async_dirty(Fun
[, Args])](mnesia:async_dirty/1
), it is performed in dirty context. The
function calls are mapped to the corresponding dirty functions. This still
involves logging, replication, and subscriptions but no locking, local
transaction storage, or commit protocols are involved. Checkpoint retainers are
updated but updated "dirty". Thus, they are updated asynchronously. The
functions wait for the operation to be performed on one node but not the others.
If the table resides locally, no waiting occurs.
By passing the same "fun" as an argument to the function [mnesia:sync_dirty(Fun
[, Args])](mnesia:sync_dirty/1
), it is performed in almost the same context
as the function mnesia:async_dirty/1,2. The difference
is that the operations are performed synchronously. The caller waits for the
updates to be performed on all active replicas. Using mnesia:sync_dirty/1,2
is
useful in the following cases:
- When an application executes on several nodes and wants 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.
- When an application performs frequent or voluminous updates that can overload
Mnesia
on the nodes.
To check if your code is executed within a transaction, use the function
mnesia:is_transaction/0
. It returns true
when called inside a transaction
context, otherwise false
.
Mnesia
tables with storage type RAM_copies
and disc_copies
are implemented
internally as ets
tables. Applications can 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])](mnesia:ets/1
), it is performed but in a raw
context. The operations are performed directly on the local ets
tables,
assuming that the local storage type is RAM_copies
and that the table is not
replicated on other nodes.
Subscriptions are not triggered and no checkpoints are updated, but this
operation is blindingly fast. Disc resident tables are not to be updated with
the ets
function, as the disc is not updated.
The Fun can also be passed as an argument to the function
mnesia:activity/2,3,4, which enables use of customized
activity access callback modules. It can either be obtained directly by stating
the module name as argument, or implicitly by use of configuration parameter
access_module
. A customized callback module can be used for several purposes,
such as providing triggers, integrity constraints, runtime statistics, or
virtual tables.
The callback module does not have to access real Mnesia
tables, it is free to
do whatever it wants as long as the callback interface is fulfilled.
Appendix B, Activity Access Callback Interface provides the
source code, mnesia_frag.erl
, for one alternative implementation. The
context-sensitive function mnesia:table_info/2
can be used to provide virtual
information about a table. One use of this is to perform QLC
queries within an
activity context with a customized callback module. By providing table
information about table indexes and other QLC
requirements, QLC
can be used
as a generic query language to access virtual tables.
QLC queries can be performed in all these activity contexts (transaction
,
sync_transaction
, async_dirty
, sync_dirty
, and ets
). The ets
activity
only works if the table has no indexes.
Note
The function
mnesia:dirty_*
always executes withasync_dirty
semantics regardless of which activity access contexts that are started. It can even start contexts without any enclosing activity access context.
Nested Transactions
Transactions can be nested in an arbitrary fashion. A child transaction must run
in the same process as its parent. When a child transaction terminates, the
caller of the child transaction gets return value {aborted, Reason}
and any
work performed by the child is erased. If a child transaction commits, the
records written by the child are 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 update performed by a nested transaction is only
propagated in such a manner so that the parent of the nested transaction sees
the updates. No final commitment is done until the top-level transaction
terminates. So, although a nested transaction returns {atomic, Val}
, if the
enclosing parent transaction terminates, the entire nested operation terminates.
The ability to have nested transaction with identical semantics as top-level
transaction makes it easier to write library functions that manipulate Mnesia
tables.
Consider a function that adds a subscriber to a telephony system:
add_subscriber(S) ->
mnesia:transaction(fun() ->
case mnesia:read( ..........
This function needs to be called as a transaction. Assume that you wish to write
a function that both calls the function add_subscriber/1
and is in itself
protected by the context of a transaction. By calling add_subscriber/1
from
within another transaction, a nested transaction is created.
Also, different activity access contexts can be mixed while nesting. However,
the dirty ones (async_dirty
, sync_dirty
, and ets
) inherit the transaction
semantics if they are called inside a transaction and thus grab locks and use
two or three phase commit.
Example:
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).
Pattern Matching
When the function mnesia:read/3
cannot be used, Mnesia
provides the
programmer with several functions for matching records against a pattern. The
most useful ones are the following:
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 match a Pattern
against all records in table Tab
. In a
mnesia:select call, Pattern
is a part of
MatchSpecification
described in the following. It is not necessarily performed
as an exhaustive search of the entire table. By using indexes and bound values
in the key of the pattern, the actual work done by the function can be condensed
into a few hash lookups. Using ordered_set
tables can 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>'
behave as Erlang variables,
that is, they match anything, bind the first occurrence, and match the coming
occurrences of that variable against the bound value.
Use 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, as this makes the code more vulnerable to future changes of the record definition.
Example:
Wildpattern = mnesia:table_info(employee, wild_pattern),
%% Or use
Wildpattern = #employee{_ = '_'},
For the employee table, the wild pattern looks as follows:
{employee, '_', '_', '_', '_', '_',' _'}.
To constrain the match, it is needed to replace some of the '_'
elements. The
code for matching out all female employees looks as follows:
Pat = #employee{sex = female, _ = '_'},
F = fun() -> mnesia:match_object(Pat) end,
Females = mnesia:transaction(F).
The match function can also be used to check the equality of different attributes. For example, to find all employees with an employee number 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 cannot express constraints other
than equality. To find the names of the male employees on the second floor:
MatchHead = #employee{name='$1', sex=male, room_no={'$2', '_'}, _='_'},
Guard = [{'>=', '$2', 220},{'<', '$2', 230}],
Result = '$1',
mnesia:select(employee,[{MatchHead, Guard, [Result]}])
The function select
can be used to add more constraints and create output that
cannot be done with mnesia:match_object/3
.
The second argument to select
is a MatchSpecification
. A
MatchSpecification
is a list of MatchFunction
s, where each MatchFunction
consists of a tuple containing {MatchHead, MatchCondition, MatchBody}
:
MatchHead
is the same pattern as used inmnesia:match_object/3
described earlier.MatchCondition
is a list of extra constraints applied to each record.MatchBody
constructs the return values.
For details about the match specifications, see "Match Specifications in Erlang"
in ERTS User's Guide. For more information, see the
ets
and dets
manual pages in STDLIB
.
The functions select/4 and select/1
are used to get a limited number of results, where Continuation
gets the next
chunk of results. Mnesia
uses NObjects
as a recommendation only. Thus, more
or less results than specified with NObjects
can be returned in the result
list, even the empty list can be returned even if there are more results to
collect.
Warning
There is a severe performance penalty in using
mnesia:select/1,2,3,4
after any modifying operation is done on that table in the same transaction. That is, avoid usingmnesia:write/1
ormnesia:delete/1
beforemnesia:select
in the same transaction.
If the key attribute is bound in a pattern, the match operation is 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 indexes (see Indexing) if the function
mnesia:match_object is used.
QLC queries can also be used to search Mnesia
tables. By using the function
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}
:
lock
specifies whetherMnesia
is to acquire a read or write lock on the table.n_objects
specifies how many results are to be returned in each chunk to QLC.traverse
specifies which functionMnesia
is to use to traverse the table. Defaultselect
is used, but by using{traverse, {select, MatchSpecification}}
as an option to mnesia:table/2 the user can specify its own view of the table.
If no options are specified, a read lock is acquired, 100 results are returned
in each chunk, and select
is used to traverse the table, that is:
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.
Iteration
Mnesia
provides the following functions that iterate 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. Fun
takes two arguments, the first is a record from the
table, and the second is the accumulator. Fun
returns a new accumulator.
The first time Fun
is applied, Acc0
is the second argument. The next time
Fun
is called, the return value from the previous call is used as the second
argument. The term the last call to Fun
returns is the return value of the
function mnesia:foldl/3
or mnesia:foldr/3
.
The difference between these functions is the order the table is accessed for
ordered_set
tables. For other table types the functions are equivalent.
LockType
specifies what type of lock that is to be acquired for the iteration,
default is read
. If records are written or deleted during the iteration, a
write lock is to be acquired.
These functions can be used to find records in a table when it is impossible to
write constraints for the function mnesia:match_object/3
, or when you want to
perform some action on certain records.
For example, finding all the employees who have a salary less than 10 can look as follows:
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).
To raise the salary to 10 for everyone with a salary less than 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).
Many nice things can be done with the iterator functions but take some caution about performance and memory use 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 generates much unnecessary network traffic.
Mnesia
also provides some functions that make it possible for the user to
iterate over the table. The order of the iteration is unspecified if the table
is not of type ordered_set
:
mnesia:first(Tab) -> Key | transaction abort
mnesia:last(Tab) -> Key | transaction abort
mnesia:next(Tab,Key) -> Key | transaction abort
mnesia:prev(Tab,Key) -> Key | transaction abort
mnesia:snmp_get_next_index(Tab,Index) -> {ok, NextIndex} | endOfTable
The order of first
/last
and next
/prev
is only valid for ordered_set
tables, they are synonyms for other tables. When the end of the table is
reached, the special key '$end_of_table'
is returned.
If records are written and deleted during the traversal, use the function
mnesia:foldl/3 or mnesia:foldr/3 with a
write
lock. Or the function mnesia:write_lock_table/1
when using first
and
next
.
Writing or deleting in transaction context creates a local copy of each modified
record. Thus, modifying each record in a large table uses much memory. Mnesia
compensates for every written or deleted record during the iteration in a
transaction context, which can reduce the performance. If possible, avoid
writing or deleting records in the same transaction before iterating over the
table.
In dirty context, that is, sync_dirty
or async_dirty
, the modified records
are not stored in a local copy; instead, each record is updated separately. This
generates much network traffic if the table has a replica on another node and
has all the other drawbacks that dirty operations have. Especially for commands
mnesia:first/1
and mnesia:next/2
, the same drawbacks as described previously
for mnesia:dirty_first/1
and mnesia:dirty_next/2
applies, that is, no
writing to the table is to be done during iteration.