查看源代码 构建 Mnesia 数据库

本节介绍了设计 Mnesia 数据库的基本步骤,以及使程序员能够使用不同解决方案的编程结构。 包括以下主题:

  • 定义模式
  • 数据模型
  • 启动 Mnesia
  • 创建表

定义模式

Mnesia 系统的配置在模式中描述。模式是一个特殊的表,其中包含表名和每个表的存储类型等信息(即表是存储在 RAM 中、磁盘上还是两者兼而有之,以及其位置)。

与数据表不同,模式表中的信息只能通过使用本节中描述的与模式相关的函数来访问和修改。

Mnesia 具有用于定义数据库模式的各种函数。可以移动或删除表,并且可以重新配置表布局。

这些函数的一个重要方面是系统可以在重新配置表时访问该表。例如,可以移动表并同时对同一表执行写入操作。此功能对于需要连续服务的应用程序至关重要。

本节介绍可用于模式管理的函数,所有这些函数都返回以下元组之一:

  • 如果成功,则返回 {atomic, ok}
  • 如果失败,则返回 {aborted, Reason}

模式函数

模式函数如下:

  • mnesia:create_schema(NodeList) 初始化一个新的空模式。这是启动 Mnesia 的强制性要求。Mnesia 是一个真正的分布式 DBMS,模式是一个系统表,它在 Mnesia 系统中的所有节点上复制。如果 NodeList 中的任何节点上已存在模式,则此函数将失败。该函数要求在参数 NodeList 中包含的所有 db_nodes 上停止 Mnesia。应用程序仅调用此函数一次,因为它通常是一次性活动,用于初始化新数据库。

  • mnesia:delete_schema(DiscNodeList) 删除 DiscNodeList 中节点上的任何旧模式。它还会删除所有旧表以及所有数据。此函数要求在所有 db_nodes 上停止 Mnesia

  • mnesia:delete_table(Tab) 永久删除表 Tab 的所有副本。

  • mnesia:clear_table(Tab) 永久删除表 Tab 中的所有条目。

  • mnesia:move_table_copy(Tab, From, To) 将表 Tab 的副本从节点 From 移动到节点 To。表存储类型 {type} 会保留,因此如果将 RAM 表从一个节点移动到另一个节点,它在新节点上仍然是 RAM 表。其他事务仍然可以在移动表时对其执行读取和写入操作。

  • mnesia:add_table_copy(Tab, Node, Type) 在节点 Node 上创建表 Tab 的副本。参数 Type 必须是原子 ram_copiesdisc_copiesdisc_only_copies 之一。如果将系统表 schema 的副本添加到节点,则希望 Mnesia 模式也驻留在那里。此操作会扩展构成此特定 Mnesia 系统的节点集。

  • mnesia:del_table_copy(Tab, Node) 删除节点 Node 上表 Tab 的副本。当删除表的最后一个副本时,该表将被删除。

  • mnesia:transform_table(Tab, Fun, NewAttributeList, NewRecordName) 更改表 Tab 中所有记录的格式。它将参数 Fun 应用于表中的所有记录。Fun 必须是一个函数,它接受旧类型的记录,并返回新类型的记录。表键不得更改。

    示例

    -record(old, {key, val}).
    -record(new, {key, val, extra}).
    
    Transformer =
       fun(X) when record(X, old) ->
          #new{key = X#old.key,
               val = X#old.val,
               extra = 42}
       end,
    {atomic, ok} = mnesia:transform_table(foo, Transformer,
                                          record_info(fields, new),
                                          new),

    参数 Fun 也可以是原子 ignore,这表示仅更新有关表的元数据。不建议使用 ignore(因为它会在元数据和实际数据之间产生不一致),但它作为用户执行自己的(离线)转换的可能性包含在内。

  • mnesia:change_table_copy_type(Tab, Node, ToType) 更改表的存储类型。例如,在指定为 Node 的节点上,将 RAM 表更改为 disc_table

数据模型

Mnesia 采用的数据模型是扩展的关系数据模型。数据组织为一组表,不同数据记录之间的关系可以建模为描述关系的更多表。每个表都包含 Erlang 记录的实例。记录表示为 Erlang 元组。

每个对象标识符 (OID) 由表名和键组成。例如,如果员工记录由元组 {employee, 104732, klacke, 7, male, 98108, {221, 015}} 表示,则此记录的 OID 是元组 {employee, 104732}

因此,每个表都由记录组成,其中第一个元素是记录名称,表的第二个元素是键,它标识该表中的特定记录。表名和键的组合是 arity two 元组 {Tab, Key},称为 OID。有关记录名称和表名之间关系的更多信息,请参阅 记录名称与表名称

使 Mnesia 数据模型成为扩展关系模型的原因是能够在属性字段中存储任意 Erlang 项。例如,一个属性值可以是通向其他表中其他项的整个 OID 树。这种类型的记录很难在传统的关系 DBMS 中建模。

启动 Mnesia

在启动 Mnesia 之前,必须完成以下操作:

  • 必须在所有参与节点上初始化一个空模式。
  • 必须启动 Erlang 系统。
  • 必须定义具有磁盘数据库模式的节点,并使用函数 mnesia:create_schema(NodeList) 实现。

当运行具有两个或多个参与节点的分布式系统时,必须在每个参与节点上执行函数 mnesia:start()。这通常是嵌入式环境中的启动脚本的一部分。在测试环境或交互式环境中,也可以从 Erlang shell 或其他程序中使用 mnesia:start()

初始化模式并启动 Mnesia

让我们使用 入门 中描述的示例数据库 Company,来说明如何在两个单独的节点(称为 a@ginb@skeppet)上运行数据库。在启动 Mnesia 之前,这些节点中的每一个都必须具有 Mnesia 目录和一个已初始化的模式。有两种方法可以指定要使用的 Mnesia 目录:

  • 通过在启动 Erlang shell 或在应用程序脚本中提供应用程序参数来指定 Mnesia 目录。以前,以下示例用于为 Company 数据库创建目录:

    % erl -mnesia dir '"/ldisc/scratch/Mnesia.Company"'
  • 如果没有输入命令行标志,则 Mnesia 目录将成为启动 Erlang shell 的节点上的当前工作目录。

要启动 Company 数据库并使其在两个指定的节点上运行,请输入以下命令:

  1. 在节点 a@gin 上:
 gin % erl -sname a  -mnesia dir '"/ldisc/scratch/Mnesia.company"'
  1. 在节点 b@skeppet 上:
skeppet % erl -sname b -mnesia dir '"/ldisc/scratch/Mnesia.company"'
  1. 在两个节点之一上:
(a@gin)1> mnesia:create_schema([a@gin, b@skeppet]).
  1. 在两个节点上都调用了函数 mnesia:start()
  2. 要初始化数据库,请在两个节点之一上执行以下代码:
dist_init() ->
    mnesia:create_table(employee,
                         [{ram_copies, [a@gin, b@skeppet]},
                          {attributes, record_info(fields,
						   employee)}]),
    mnesia:create_table(dept,
                         [{ram_copies, [a@gin, b@skeppet]},
                          {attributes, record_info(fields, dept)}]),
    mnesia:create_table(project,
                         [{ram_copies, [a@gin, b@skeppet]},
                          {attributes, record_info(fields, project)}]),
    mnesia:create_table(manager, [{type, bag},
                                  {ram_copies, [a@gin, b@skeppet]},
                                  {attributes, record_info(fields,
							   manager)}]),
    mnesia:create_table(at_dep,
                         [{ram_copies, [a@gin, b@skeppet]},
                          {attributes, record_info(fields, at_dep)}]),
    mnesia:create_table(in_proj,
                        [{type, bag},
                         {ram_copies, [a@gin, b@skeppet]},
                         {attributes, record_info(fields, in_proj)}]).

如图所示,这两个目录驻留在不同的节点上,因为 /ldisc/scratch(“本地”磁盘)存在于两个不同的节点上。

通过执行这些命令,配置了两个 Erlang 节点来运行 Company 数据库,并因此初始化了数据库。这仅在设置时需要一次。下次启动系统时,会在两个节点上调用 mnesia:start(),以从磁盘初始化系统。

Mnesia 节点系统中,每个节点都知道所有表的当前位置。在此示例中,数据会复制到两个节点上,并且可以在两个节点中的任何一个上执行操作表中数据的函数。操作 Mnesia 数据的代码的行为方式相同,而无论数据驻留在何处。

函数 mnesia:stop() 停止在执行该函数的节点上的 Mnesia。函数 mnesia:start/0mnesia:stop/0 对“本地” Mnesia 系统起作用。没有函数启动或停止一组节点。

启动过程

通过调用以下函数启动 Mnesia

mnesia:start().

此函数在本地启动 DBMS。

配置的选择会改变表的位置和加载顺序。备选方案如下:

  1. 仅在本地存储的表从本地 Mnesia 目录初始化。
  2. 本地以及其他地方驻留的复制表从磁盘或通过从另一个节点复制整个表来启动,具体取决于哪个副本是最新的。Mnesia 确定哪个表是最新的。
  3. 驻留在远程节点上的表在加载后可供其他节点使用。

表初始化是异步的。函数调用 mnesia:start() 返回原子 ok,然后开始初始化不同的表。根据数据库的大小,这可能需要一些时间,并且应用程序程序员必须等待应用程序需要的表才能使用。这是通过使用函数 mnesia:wait_for_tables(TabList, Timeout) 来实现的,该函数会暂停调用方,直到正确初始化 TabList 中指定的所有表。

如果一个节点上的复制表被初始化,但 Mnesia 推断出另一个(远程)副本比本地节点上的副本更新,并且初始化过程没有继续,则可能会出现问题。在这种情况下,调用 mnesia:wait_for_tables/2 会暂停调用者,直到远程节点从其本地磁盘初始化表,并且该节点已通过网络将表复制到本地节点。

然而,这个过程可能很耗时,快捷函数 mnesia:force_load_table(Tab) 以更快的速度从磁盘加载所有表。该函数强制从磁盘加载表,而无需考虑网络情况。

因此,可以假设如果一个应用程序想使用表 ab,该应用程序必须执行类似于以下操作,然后才能使用这些表

case mnesia:wait_for_tables([a, b], 20000) of
  {timeout, RemainingTabs} ->
    panic(RemainingTabs);
  ok ->
    synced
end.

警告

当强制从本地磁盘加载表时,在本地节点关闭且远程副本处于活动状态时,对复制表执行的所有操作都会丢失。这可能导致数据库不一致。

如果启动过程失败,函数 mnesia:start() 返回神秘的元组 {error,{shutdown, {mnesia_sup,start_link,[normal,[]]}}}。要获取有关启动失败的更多信息,请使用命令行参数 -boot start_sasl 作为 erl 脚本的参数。

创建表

函数 mnesia:create_table(Name, ArgList) 用于创建表。执行此函数时,它返回以下响应之一

  • 如果函数执行成功,则返回 {atomic, ok}
  • 如果函数失败,则返回 {aborted, Reason}

函数参数如下

  • Name 是表的名称。它通常与构成表的记录的名称相同。有关详细信息,请参阅 record_name
  • ArgList{Key,Value} 元组的列表。以下参数有效
    • {type, Type},其中 Type 必须是原子 setordered_setbag 之一。默认值为 set

      请注意,目前 ordered_set 不支持 disc_only_copies 表。

      类型为 setordered_set 的表每个键包含零个或一个记录,而类型为 bag 的表每个键可以包含任意数量的记录。每个记录的键始终是记录的第一个属性。

      以下示例说明了 set 类型和 bag 类型之间的区别

       f() ->
          F = fun() ->
                mnesia:write({foo, 1, 2}),
                mnesia:write({foo, 1, 3}),
                mnesia:read({foo, 1})
              end,
          mnesia:transaction(F).

      如果表 foo 的类型为 set,则此事务返回列表 [{foo,1,3}]。但是,如果表类型为 bag,则返回列表 [{foo,1,2}, {foo,1,3}]

      Mnesia 表格中永远不能包含同一表格中相同记录的重复项。重复记录具有相同内容和键的属性。

    • {disc_copies, NodeList},其中 NodeList 是此表要驻留在磁盘上的节点列表。

      对类型为 disc_copies 的表副本的写入操作会将数据写入磁盘副本和表的 RAM 副本。

      可以在一个节点上有一个类型为 disc_copies 的复制表,并在另一个节点上将同一表存储为不同的类型。默认值为 []。如果需要以下操作特性,则这种安排是可取的

      1. 读取操作必须快速且在 RAM 中执行。
      2. 所有写入操作都必须写入持久存储。

      disc_copies 表副本的写入操作分两步执行。首先,将写入操作附加到日志文件,然后将实际操作在 RAM 中执行。

    • {ram_copies, NodeList},其中 NodeList 是此表存储在 RAM 中的节点列表。默认值为 [node()]。如果使用默认值创建表,则该表仅位于本地节点上。

      类型为 ram_copies 的表副本可以使用函数 mnesia:dump_tables(TabList) 转储到磁盘。

    • {disc_only_copies, NodeList}。这些表副本仅存储在磁盘上,因此访问速度较慢。但是,仅磁盘副本比其他两种存储类型的表副本消耗的内存更少。

    • {index, AttributeNameList},其中 AttributeNameList 是指定 Mnesia 要构建和维护的属性名称的原子列表。列表中每个元素都存在一个索引表。Mnesia 记录的第一个字段是键,因此不需要额外的索引。

      记录的第一个字段是元组的第二个元素,它是记录的表示形式。

    • {snmp, SnmpStruct}SnmpStructSNMP 用户指南中进行了描述。基本上,如果此属性存在于 mnesia:create_table/2ArgList 中,则该表可以立即通过 SNMP 访问。

      设计使用 SNMP 来操作和控制系统的应用程序很容易。Mnesia 提供了组成 SNMP 控制应用程序的逻辑表和组成 Mnesia 表的物理数据之间的直接映射。默认值为 []

    • {local_content, true}。当应用程序需要一个内容在每个节点上都是本地唯一的表时,可以使用 local_content 表。所有 Mnesia 节点都知道表的名称,但其内容对于每个节点都是唯一的。必须在本地执行对此类型表的访问。

    • {attributes, AtomList} 是一个记录属性名称列表,这些记录将填充该表。默认列表为 [key, val]。该表除了键之外,至少必须有一个额外的属性。当访问记录中的单个属性时,不建议将属性名称硬编码为原子。请使用构造 record_info(fields, record_name) 代替。

      表达式 record_info(fields, record_name) 由 Erlang 预处理器处理,并返回记录字段名称列表。对于记录定义 -record(foo, {x,y,z}).,表达式 record_info(fields,foo) 展开为列表 [x,y,z]。因此,您可以提供属性名称或使用 record_info/2 表示法。

      建议使用 record_info/2 表示法,因为它更容易维护程序,并且程序在未来记录更改方面变得更加健壮。

    • {record_name, Atom} 指定表中存储的所有记录的通用名称。存储在表中的所有记录都必须以此名称作为它们的第一个元素。record_name 默认为表的名称。有关更多信息,请参阅记录名称与表名称

例如,考虑以下记录定义

-record(funky, {x, y}).

以下调用将创建一个在两个节点上复制的表,在属性 y 上具有额外的索引,并且类型为 bag

mnesia:create_table(funky, [{disc_copies, [N1, N2]}, {index, [y]},
                            {type, bag}, {attributes, record_info(fields, funky)}]).

而对以下默认代码值的调用将返回一个在本地节点上具有 RAM 副本的表,没有额外的索引,并且属性默认为列表 [key,val]

mnesia:create_table(stuff, [])