查看源代码 入门

本节通过一个示例数据库介绍 Mnesia。该示例在后续章节中被引用,其中修改示例以说明各种程序结构。本节通过示例说明以下强制性过程

  • 启动 Erlang 会话。
  • 指定 Mnesia 目录,数据库将存储在该目录中。
  • 初始化一个新的数据库模式,该模式具有一个属性,用于指定数据库将在哪个或哪些节点上运行。
  • 启动 Mnesia
  • 创建和填充数据库表。

首次启动 Mnesia

本节提供了一个简化的 Mnesia 系统启动演示。来自 Erlang shell 的对话如下

unix> erl -mnesia dir '"/tmp/funky"'
Erlang (BEAM) emulator version 4.9

Eshell V4.9  (abort with ^G)
1>
1> mnesia:create_schema([node()]).
ok
2> mnesia:start().
ok
3> mnesia:create_table(funky, []).
{atomic,ok}
4> mnesia:info().
---> Processes holding locks <---
---> Processes waiting for locks <---
---> Pending (remote) transactions <---
---> Active (local) transactions <---
---> Uncertain transactions <---
---> Active tables <---
funky          : with 0 records occupying 269 words of mem
schema         : with 2 records occupying 353 words of mem
===> System info in version "1.0", debug level = none <===
opt_disc. Directory "/tmp/funky" is used.
use fall-back at restart = false
running db nodes = [nonode@nohost]
stopped db nodes = []
remote           = []
ram_copies       = [funky]
disc_copies      = [schema]
disc_only_copies = []
[{nonode@nohost,disc_copies}] = [schema]
[{nonode@nohost,ram_copies}] = [funky]
1 transactions committed, 0 aborted, 0 restarted, 1 logged to disc
0 held locks, 0 in queue; 0 local transactions, 0 remote
0 transactions waits for other nodes: []
ok

在此示例中,执行以下操作

  • 步骤 1: 从 UNIX 提示符启动 Erlang 系统,并使用标志 -mnesia dir '"/tmp/funky"',该标志指示在哪个目录中存储数据。
  • 步骤 2: 通过评估 mnesia:create_schema([node()]),在本地节点上初始化一个新的空模式。该模式包含有关数据库的常规信息。稍后将详细说明。
  • 步骤 3: 通过评估 mnesia:start() 启动 DBMS。
  • 步骤 4: 通过评估表达式 mnesia:create_table(funky, []),创建一个名为 funky 的第一个表。该表被赋予默认属性。
  • 步骤 5: 评估 mnesia:info(),以在终端上显示有关数据库状态的信息。

示例

Mnesia 数据库组织为一组表。每个表都填充了实例(Erlang 记录)。一个表还具有多个属性,例如位置和持久性。

数据库

此示例演示如何创建名为 Company 的数据库以及以下图表所示的关系

---
title: Company Entity-Relation Diagram
---

erDiagram
    Dept {
        atom id
        string name
    }

    Employee {
        int emp_no
        string name
        int salary
        atom sex
        int phone
        tuple room_no

    }

    Project {
        atom name
        int number
    }

    Dept ||--|| Employee: Manager
    Employee }|--|| Dept: At_dep
    Employee }|--|{ Project: in_proj

数据库模型如下

  • 有三个实体:部门、员工和项目。
  • 这些实体之间存在三种关系
    1. 一个部门由一名员工管理,因此存在 manager 关系。
    2. 一名员工在某个部门工作,因此存在 at_dep 关系。
    3. 每个员工都参与多个项目,因此存在 in_proj 关系。

定义结构和内容

首先,将记录定义输入到名为 company.hrl 的文本文件中。此文件为示例数据库定义以下结构

-record(employee, {emp_no,
                   name,
                   salary,
                   sex,
                   phone,
                   room_no}).

-record(dept, {id,
               name}).

-record(project, {name,
                  number}).


-record(manager, {emp,
                  dept}).

-record(at_dep, {emp,
                 dept_id}).

-record(in_proj, {emp,
                  proj_name}).

该结构定义了数据库中的六个表。在 Mnesia 中,函数 mnesia:create_table(Name, ArgList) 创建表。Name 是表名。

注意

当前版本的 Mnesia 不要求表名与记录名相同,请参阅 记录名与表名

例如,使用函数 mnesia:create_table(employee, [{attributes, record_info(fields, employee)}]) 创建员工表。表名 employee 与在 ArgList 中指定的记录名称匹配。表达式 record_info(fields, RecordName) 由 Erlang 预处理器处理,并计算为包含记录的不同字段名称的列表。

程序

以下 shell 交互启动 Mnesia 并初始化 Company 数据库的模式

% erl -mnesia dir '"/ldisc/scratch/Mnesia.Company"'
Erlang (BEAM) emulator version 4.9

Eshell V4.9  (abort with ^G)
1> mnesia:create_schema([node()]).
ok
2> mnesia:start().
ok

以下程序模块创建并填充先前定义的表

-include_lib("stdlib/include/qlc.hrl").
-include("company.hrl").

init() ->
    mnesia:create_table(employee,
                        [{attributes, record_info(fields, employee)}]),
    mnesia:create_table(dept,
                        [{attributes, record_info(fields, dept)}]),
    mnesia:create_table(project,
                        [{attributes, record_info(fields, project)}]),
    mnesia:create_table(manager, [{type, bag},
                                  {attributes, record_info(fields, manager)}]),
    mnesia:create_table(at_dep,
                         [{attributes, record_info(fields, at_dep)}]),
    mnesia:create_table(in_proj, [{type, bag},
                                  {attributes, record_info(fields, in_proj)}]).

程序说明

以下命令和函数用于启动 Company 数据库

  • % erl -mnesia dir '"/ldisc/scratch/Mnesia.Company"'。这是一个 UNIX 命令行条目,用于启动 Erlang 系统。标志 -mnesia dir Dir 指定数据库目录的位置。系统响应并等待进一步输入,提示符为 1>
  • mnesia:create_schema([node()])。此函数的格式为 mnesia:create_schema(DiscNodeList),并启动一个新的模式。在此示例中,创建了一个仅使用一个节点的非分布式系统。在 定义模式 中完整地解释了模式。
  • mnesia:start()。此函数启动 Mnesia,并在 启动 Mnesia 中进行完整说明。

继续与 Erlang shell 的对话,将产生以下结果

3> company:init().
{atomic,ok}
4> mnesia:info().
---> Processes holding locks <---
---> Processes waiting for locks <---
---> Pending (remote) transactions <---
---> Active (local) transactions <---
---> Uncertain transactions <---
---> Active tables <---
in_proj        : with 0 records occuping 269 words of mem
at_dep         : with 0 records occuping 269 words of mem
manager        : with 0 records occuping 269 words of mem
project        : with 0 records occuping 269 words of mem
dept           : with 0 records occuping 269 words of mem
employee       : with 0 records occuping 269 words of mem
schema         : with 7 records occuping 571 words of mem
===> System info in version "1.0", debug level = none <===
opt_disc. Directory "/ldisc/scratch/Mnesia.Company" is used.
use fall-back at restart = false
running db nodes = [nonode@nohost]
stopped db nodes = []
remote           = []
ram_copies       =
    [at_dep,dept,employee,in_proj,manager,project]
disc_copies      = [schema]
disc_only_copies = []
[{nonode@nohost,disc_copies}] = [schema]
[{nonode@nohost,ram_copies}] =
    [employee,dept,project,manager,at_dep,in_proj]
6 transactions committed, 0 aborted, 0 restarted, 6 logged to disc
0 held locks, 0 in queue; 0 local transactions, 0 remote
0 transactions waits for other nodes: []
ok

创建了一组表。函数 mnesia:create_table(Name, ArgList) 创建所需的数据库表。ArgList 可用的选项在 创建新表 中进行了解释。

函数 company:init/0 创建表。两个表的类型为 bag。这是 manager 关系以及 in_proj 关系。这被解释为:一个员工可以管理多个部门,一个员工可以参与多个项目。但是,at_dep 关系是 set,因为一个员工只能在一个部门工作。在此数据模型中,存在 1 对 1 (set) 和 1 对多 (bag) 的关系示例。

mnesia:info() 现在指示数据库有七个本地表,其中六个是用户定义的表,一个为模式。已经提交了六个事务,因为在创建表时运行了六个成功的事务。

要编写一个将员工记录插入数据库的函数,必须插入 at_dep 记录和一组 in_proj 记录。检查用于完成此操作的以下代码

insert_emp(Emp, DeptId, ProjNames) ->
    Ename = Emp#employee.name,
    Fun = fun() ->
                  mnesia:write(Emp),
                  AtDep = #at_dep{emp = Ename, dept_id = DeptId},
                  mnesia:write(AtDep),
                  mk_projs(Ename, ProjNames)
          end,
    mnesia:transaction(Fun).


mk_projs(Ename, [ProjName|Tail]) ->
    mnesia:write(#in_proj{emp = Ename, proj_name = ProjName}),
    mk_projs(Ename, Tail);
mk_projs(_, []) -> ok.
  • insert_emp/3 参数如下
    1. Emp 是员工记录。
    2. DeptId 是员工工作的部门的标识。
    3. ProjNames 是员工参与的项目名称列表。

函数 insert_emp/3 创建一个函数对象 (Fun)。Fun 作为单个参数传递给函数 mnesia:transaction(Fun)。这意味着 Fun 作为事务运行,具有以下属性

  • Fun 要么成功,要么失败。
  • 操作相同数据记录的代码可以并发运行,而不同的进程彼此不干扰。

该函数可以按如下方式使用

Emp = #employee{emp_no = 104732,
                name = klacke,
                salary = 7,
                sex = male,
                phone = 98108,
                room_no = {221, 015}},
insert_emp(Emp, 'B/SFR', [Erlang, mnesia, otp]).

注意

有关 Funs 的信息,请参阅系统文档中“Erlang 参考手册”中的“Fun 表达式”。。

初始数据库内容

在插入名为 klacke 的员工后,数据库具有以下记录

emp_nonamesalarysexphoneroom_no
104732klacke7male98108{221, 015}

表:employee 数据库记录

employee 记录具有 Erlang 记录/元组表示 {employee, 104732, klacke, 7, male, 98108, {221, 015}}

empdept_name
klackeB/SFR

表:at_dep 数据库记录

at_dep 记录具有 Erlang 元组表示 {at_dep, klacke, 'B/SFR'}

empproj_name
klackeErlang
klackeotp
klackemnesia

表:in_proj 数据库记录

in_proj 记录具有 Erlang 元组表示 {in_proj, klacke, 'Erlang', klacke, 'otp', klacke, 'mnesia'}

表中的行和 Mnesia 记录之间没有区别。这两个概念是相同的,并且在本用户指南中互换使用。

Mnesia 表由 Mnesia 记录填充。例如,元组 {boss, klacke, bjarne} 是一个记录。此元组中的第二个元素是键。要唯一地标识一个表,需要键和表名。术语对象标识符 (OID) 有时用于二元元组 {Tab, Key}。记录 {boss, klacke, bjarne} 的 OID 是二元元组 {boss, klacke}。元组的第一个元素是记录的类型,第二个元素是键。OID 可以指向零个、一个或多个记录,具体取决于表类型是 set 还是 bag

还可以插入记录 {boss, klacke, bjarne}。此记录包含对数据库中尚不存在的另一个员工的隐式引用。Mnesia 不强制执行此操作。

向数据库添加记录和关系

将更多记录添加到 Company 数据库后,结果可以是以下记录

employees:

{employee, 104465, "Johnson Torbjorn",   1, male,  99184, {242,038}}.
{employee, 107912, "Carlsson Tuula",     2, female,94556, {242,056}}.
{employee, 114872, "Dacker Bjarne",      3, male,  99415, {221,035}}.
{employee, 104531, "Nilsson Hans",       3, male,  99495, {222,026}}.
{employee, 104659, "Tornkvist Torbjorn", 2, male,  99514, {222,022}}.
{employee, 104732, "Wikstrom Claes",     2, male,  99586, {221,015}}.
{employee, 117716, "Fedoriw Anna",       1, female,99143, {221,031}}.
{employee, 115018, "Mattsson Hakan",     3, male,  99251, {203,348}}.

dept:

{dept, 'B/SF',  "Open Telecom Platform"}.
{dept, 'B/SFP', "OTP - Product Development"}.
{dept, 'B/SFR', "Computer Science Laboratory"}.

projects:

%% projects
{project, erlang, 1}.
{project, otp, 2}.
{project, beam, 3}.
{project, mnesia, 5}.
{project, wolf, 6}.
{project, documentation, 7}.
{project, www, 8}.

这三个表 employeesdeptprojects 由实际记录组成。以下数据库内容存储在表中,并建立在关系之上。这些表是 managerat_depin_proj

manager:

{manager, 104465, 'B/SF'}.
{manager, 104465, 'B/SFP'}.
{manager, 114872, 'B/SFR'}.

at_dep:

{at_dep, 104465, 'B/SF'}.
{at_dep, 107912, 'B/SF'}.
{at_dep, 114872, 'B/SFR'}.
{at_dep, 104531, 'B/SFR'}.
{at_dep, 104659, 'B/SFR'}.
{at_dep, 104732, 'B/SFR'}.
{at_dep, 117716, 'B/SFP'}.
{at_dep, 115018, 'B/SFP'}.

in_proj:

{in_proj, 104465, otp}.
{in_proj, 107912, otp}.
{in_proj, 114872, otp}.
{in_proj, 104531, otp}.
{in_proj, 104531, mnesia}.
{in_proj, 104545, wolf}.
{in_proj, 104659, otp}.
{in_proj, 104659, wolf}.
{in_proj, 104732, otp}.
{in_proj, 104732, mnesia}.
{in_proj, 104732, erlang}.
{in_proj, 117716, otp}.
{in_proj, 117716, documentation}.
{in_proj, 115018, otp}.
{in_proj, 115018, mnesia}.

房间号是员工记录的一个属性。这是一个结构化属性,由一个元组组成。元组的第一个元素标识走廊,第二个元素标识该走廊中的房间。另一种方法是将此表示为一个记录 -record(room, {corr, no}).,而不是匿名元组表示。

现在 Company 数据库已初始化并包含数据。

编写查询

通常使用函数 mnesia:read/3mnesia:read/1 从 DBMS 中检索数据。以下函数用于提高工资

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).

由于需要在增加工资后使用函数 mnesia:write/1 更新记录,因此在从表中读取记录时会获取写锁(read 的第三个参数)。

直接从表中读取值并非总是可行。可能需要搜索一个或多个表才能获取所需的数据,这通过编写数据库查询来完成。查询总是比使用 mnesia:read/1 进行的直接查找操作更昂贵。因此,在对性能要求严格的代码中应避免使用查询。

有两种方法可用于编写数据库查询

  • Mnesia 函数
  • QLC

使用 Mnesia 函数

以下函数提取存储在数据库中的女性员工的姓名

mnesia:select(employee, [{#employee{sex = female, name = '$1', _ = '_'},[], ['$1']}]).

select 必须始终在活动(例如事务)中运行。可以构造以下函数以从 shell 中调用

all_females() ->
    F = fun() ->
		Female = #employee{sex = female, name = '$1', _ = '_'},
		mnesia:select(employee, [{Female, [], ['$1']}])
        end,
    mnesia:transaction(F).

select 表达式匹配 employee 表中所有字段 sex 设置为 female 的条目。

可以从 shell 中调用此函数,如下所示

(klacke@gin)1> company:all_females().
{atomic, ["Carlsson Tuula", "Fedoriw Anna"]}

有关 select 及其语法的描述,请参阅 模式匹配

使用 QLC

本节仅包含简单的入门示例。有关 QLC 查询语言的完整描述,请参阅 STDLIB 中的 qlc 手册页。

使用 QLC 可能比直接使用 Mnesia 函数更昂贵,但提供了更友好的语法。

以下函数从数据库中提取女性员工的列表

Q = qlc:q([E#employee.name || E <- mnesia:table(employee),
                              E#employee.sex == female]),
qlc:e(Q),

从 QLC 列表推导式访问 Mnesia 表必须始终在事务中完成。考虑以下函数

females() ->
    F = fun() ->
		Q = qlc:q([E#employee.name || E <- mnesia:table(employee),
					      E#employee.sex == female]),
		qlc:e(Q)
	end,
    mnesia:transaction(F).

可以从 shell 中调用此函数,如下所示

(klacke@gin)1> company:females().
{atomic, ["Carlsson Tuula", "Fedoriw Anna"]}

在传统的关联数据库术语中,此操作称为选择,然后是投影。

前面的列表推导式表达式包含许多语法元素

  • 第一个 [ 方括号表示“构建列表”。
  • || 表示“满足条件”,箭头 <- 表示“取自”。

因此,前面的列表推导式演示了如何形成列表 E#employee.name,其中 E 取自 employee 表,并且每个记录的属性 sex 等于原子 female

整个列表推导式必须传递给函数 qlc:q/1

可以将包含低级 Mnesia 函数的列表推导式在同一事务中组合。要提高所有女性员工的工资,请执行以下操作

raise_females(Amount) ->
    F = fun() ->
                Q = qlc:q([E || E <- mnesia:table(employee),
                                E#employee.sex == female]),
		Fs = qlc:e(Q),
                over_write(Fs, Amount)
        end,
    mnesia:transaction(F).

over_write([E|Tail], Amount) ->
    Salary = E#employee.salary + Amount,
    New = E#employee{salary = Salary},
    mnesia:write(New),
    1 + over_write(Tail, Amount);
over_write([], _) ->
    0.

函数 raise_females/1 返回元组 {atomic, Number},其中 Number 是工资增加的女性员工人数。如果发生错误,则返回值 {aborted, Reason},并且 Mnesia 保证不会为任何员工提高工资。

示例

33> company:raise_females(33).
{atomic,2}