5 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
5.1 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.
The described durability feature does not entirely apply to situations where Mnesia
is configured as a "pure" primary memory database.
5.2 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 objectFun
as the single parameter. -
mnesia:read({Tab, Key}) -> transaction abort | RecordList
reads all records withKey
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 functionread/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. ArgumentRecord
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 OIDRecord
. 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 tableTab
. -
mnesia:write_lock_table(Tab)
sets a write lock on tableTab
.
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.
5.3 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 fromMnesia
. -
mnesia:dirty_write(Record)
writes the recordRecord
. -
mnesia:dirty_delete({Tab, Key})
deletes one or more records with keyKey
. -
mnesia:dirty_delete_object(Record)
is the dirty operation alternative to the functiondelete_object/1
. -
mnesia:dirty_first(Tab)
returns the "first" key in tableTab
.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 tableTab
. 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 becausewrite
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 likemnesia: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 likemnesia: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 addsVal
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 funcion
dirty_update_counter/2
is performed as an atomic operation although it is not protected by a transaction. Therfore no table update is lost if two processes simultaneously execute the functiondirty_update_counter/2
.
-
mnesia:dirty_match_object(Pat)
is the dirty equivalent ofmnesia:match_object/1
. -
mnesia:dirty_select(Tab, Pat)
is the dirty equivalent ofmnesia:select/2
. -
mnesia:dirty_index_match_object(Pat, Pos)
is the dirty equivalent ofmnesia:index_match_object/2
. -
mnesia:dirty_index_read(Tab, SecondaryKey, Pos)
is the dirty equivalent ofmnesia:index_read/3
. -
mnesia:dirty_all_keys(Tab)
is the dirty equivalent ofmnesia:all_keys/1
.
5.4 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).
5.5 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])
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])
, 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])
, 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])
, 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.
The function mnesia:dirty_*
always executes with async_dirty
semantics regardless of which activity access contexts that are started. It can even start contexts without any enclosing activity access context.
5.6 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).
5.7 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.
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 using mnesia:write/1
or mnesia:delete/1
before mnesia: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 tomnesia: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.
5.8 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.
© 2010–2021 Ericsson AB
Licensed under the Apache License, Version 2.0.