Mnesia 数据库

This translation is up to date.

Mnesia 是一个强大的分布式实时数据库管理系统。

目录

概要

Mnesia 是 Erlang 运行时中自带的一个数据库管理系统(DBMS),也可以在 Elixir 中很自然地使用。Mnesia 的数据库模型可以混合了关系型和对象型数据模型的特征,让它可以用来开发任何规模的分布式应用程序。

应用场景

何时该使用何种技术常常是一个令人困惑的事情。如果下面这些问题中任意一个的答案是 yes 的话,则是一个很好的迹象告诉我们在这个情况下用 Mnesia 比用 ETS 或者 DETS 要适合。

  • 我是否需要回滚事务?
  • 我是否需要用一个简单的语法来读写数据?
  • 我是否需要在多于一个以上的节点存储数据?
  • 我是否需要选择数据存储的位置(内存还是硬盘)?

Schema

因为 Mnesia 是 Erlang 的内容,还没有被包含到 Elixir,我们要用 :mnesia 这种方式去引用 Mnesia (参考和 Erlang 互操作)。


iex> :mnesia.create_schema([node()])

# or if you prefer the Elixir feel...

iex> alias :mnesia, as: Mnesia
iex> Mnesia.create_schema([node()])

在本课中,我们会使用后一种方式来使用 Mnesia 的 API。Mnesia.create_schema/1 会初始化一个空的 Schema 并且传递给一个节点列表。 在本例中,我们传入的是当前 IEx 会话所在的节点。

节点(Node)

一旦我们在 IEx 中执行了 Mnesia.create_schema([node()]) 命令后,我们就可以在当前目录下看到一个叫 [email protected] 或者类似名字的文件夹。你也许会好奇到底 [email protected] 代表着什么,因为我们还没有在之前的课程中见过它。我们接下来就来看看:

$ iex --help
Usage: iex [options] [.exs file] [data]

  -v                Prints version
  -e "command"      Evaluates the given command (*)
  -r "file"         Requires the given files/patterns (*)
  -S "script"       Finds and executes the given script
  -pr "file"        Requires the given files/patterns in parallel (*)
  -pa "path"        Prepends the given path to Erlang code path (*)
  -pz "path"        Appends the given path to Erlang code path (*)
  --app "app"       Start the given app and its dependencies (*)
  --erl "switches"  Switches to be passed down to Erlang (*)
  --name "name"     Makes and assigns a name to the distributed node
  --sname "name"    Makes and assigns a short name to the distributed node
  --cookie "cookie" Sets a cookie for this distributed node
  --hidden          Makes a hidden node
  --werl            Uses Erlang's Windows shell GUI (Windows only)
  --detached        Starts the Erlang VM detached from console
  --remsh "name"    Connects to a node using a remote shell
  --dot-iex "path"  Overrides default .iex.exs file and uses path instead;
                    path can be empty, then no file will be loaded

** Options marked with (*) can be given more than once
** Options given after the .exs file or -- are passed down to the executed code
** Options can be passed to the VM using ELIXIR_ERL_OPTIONS or --erl

当你给 IEx 传递 --help 选项的时候,IEx 会列出所有可用的选项。我们可以看到有 --name--sname 两个选项可以给节点起名。 一个节点(Node)就是一个运行中的 Erlang 虚拟机,它独自管理着自己的通讯,垃圾回收,进程调度以及内存等等。如果你没有给节点起名,那么这个节点的名字就叫 [email protected]

$ iex --name [email protected]

Erlang/OTP 21.1 [erts-10.1] [source] [64-bit] [smp:4:4] [async-threads:10] [hipe] [kernel-poll:false] [dtrace]

Interactive Elixir (1.7.3) - press Ctrl+C to exit (type h() ENTER for help)
iex([email protected])> Node.self
:"[email protected]"

我们可以看到,当我给节点起名后,我们当前的节点名字已经叫做 :"[email protected]"。如果我们再运行 Mnesia.create_schema([node()]) 的话,我们会看到另外一个叫做 [email protected] 的文件夹。这样设计的目的很简单。Erlang 中的节点只是用来连接其他节点用以分享(分发)信息和资源,它们并不一定要在同一台机器上,也可以通过局域网或者互联网等方式通讯。

启动 Mnesia

我们已经了解了如何设置 Mnesia 数据库,现在我们就可以通过 Mnesia.start/0 来启动它了。

iex> alias :mnesia, as: Mnesia
iex> Mnesia.create_schema([node()])
:ok
iex> Mnesia.start()
:ok

需要注意的是,如果是在一个有多个节点的分布式系统中运行 Mnesia,必须要在每一个参与的节点上面运行 Mnesia.start/1

创建表

我们可以用 Mnesia.create_table/2 在数据库中创建表。下面的例子中,我们创建了一个名为 Person 的表,并且通过一个关键字列表来定义结构。

iex> Mnesia.create_table(Person, [attributes: [:id, :name, :job]])
{:atomic, :ok}

我们同过原子 :id, :name:job 定义了表的字段。Mnesia.create_table/2 可能返回下面两种结果:

  • {:atomic, :ok} 代表执行成功
  • {:aborted, Reason} 代表执行失败

如果数据库中已经存在同名的表,返回结果中的 Reason{:already_exists, table}。所以当我们再执行一次上面的命令是,我们会得到下面的结果:

iex> Mnesia.create_table(Person, [attributes: [:id, :name, :job]])
{:aborted, {:already_exists, Person}}

脏操作

首先我们来学习对 Mnesia 表的脏炒作方式。一般情况下,我们都不会使用脏操作,因为脏操作并不一定保证成功,但是它可以帮助我们学习和适应 Mnesia 的使用方式。下面让我们往 Person 表中添加一些记录。

iex> Mnesia.dirty_write({Person, 1, "Seymour Skinner", "Principal"})
:ok

iex> Mnesia.dirty_write({Person, 2, "Homer Simpson", "Safety Inspector"})
:ok

iex> Mnesia.dirty_write({Person, 3, "Moe Szyslak", "Bartender"})
:ok

…然后我们可以通过 Mnesia.dirty_read/1 来读取数据:

iex> Mnesia.dirty_read({Person, 1})
[{Person, 1, "Seymour Skinner", "Principal"}]

iex> Mnesia.dirty_read({Person, 2})
[{Person, 2, "Homer Simpson", "Safety Inspector"}]

iex> Mnesia.dirty_read({Person, 3})
[{Person, 3, "Moe Szyslak", "Bartender"}]

iex> Mnesia.dirty_read({Person, 4})
[]

如果我们查询的记录不存在时,Mnesia 会返回一个空的列表。

事务(Transaction)

我们一般会把我们对数据库的读写包在一个数据库事务里面。对事务的支持对设计容错系统和分布式系统非常重要。Mnesia 的事务是通过对数据库的多个操作包含到一个函数体中来实现。首先我们创建一个匿名函数,如此例中的 data_to_write,然后把这个函数传给 Mnesia.transaction

iex> data_to_write = fn ->
...>   Mnesia.write({Person, 4, "Marge Simpson", "home maker"})
...>   Mnesia.write({Person, 5, "Hans Moleman", "unknown"})
...>   Mnesia.write({Person, 6, "Monty Burns", "Businessman"})
...>   Mnesia.write({Person, 7, "Waylon Smithers", "Executive assistant"})
...> end
#Function<20.54118792/0 in :erl_eval.expr/5>

iex> Mnesia.transaction(data_to_write)
{:atomic, :ok}

从 IEx 中打印的消息看来,我们可以安全地假设数据已经被成功地写进了 Person 表。我们来验证一下使用事务从数据库里面读出刚刚写入的数据。我们可以用 Mnesia.read/1 来从数据库里面读取数据,同样的,我们也需要使用一个匿名函数。

iex> data_to_read = fn ->
...>   Mnesia.read({Person, 6})
...> end
#Function<20.54118792/0 in :erl_eval.expr/5>

iex> Mnesia.transaction(data_to_read)
{:atomic, [{Person, 6, "Monty Burns", "Businessman"}]}

如果你想要更新数据,你还是调用 Mnesia.write/1,只要记录里面的 key 和现有记录的 key 相同即可。要更新 Hans 那条记录的话,可以这样做:

iex> Mnesia.transaction(
...>   fn ->
...>     Mnesia.write({Person, 5, "Hans Moleman", "Ex-Mayor"})
...>   end
...> )

使用索引

Mnesia 也支持在非主键字段上添加索引,然后通过这个索引来查询数据。我们来试下在 Person 表的 :job 字段上添加索引:

iex> Mnesia.add_table_index(Person, :job)
{:atomic, :ok}

结果跟 Mnesia.create_table/2 返回的相同:

  • {:atomic, :ok} 表示执行成功
  • {:aborted, Reason} 表示执行失败

类似的,如果索引已经存在,返回结果中的 Reason{:already_exists, table, attribute_index}。所以当我们再执行一次上面的命令是,我们会得到下面的结果:

iex> Mnesia.add_table_index(Person, :job)
{:aborted, {:already_exists, Person, 4}}

创建索引后,我们可以通过索引来获取数据。下面的例子中使用 Mnesia.index_read/2 来获取工作是 Principal 的记录:

iex> Mnesia.transaction(
...>   fn ->
...>     Mnesia.index_read(Person, "Principal", :job)
...>   end
...> )
{:atomic, [{Person, 1, "Seymour Skinner", "Principal"}]}

匹配和选择

Mnesia 可以通过匹配的方式支持复杂的查询,也支持使用函数的方式来查询数据。 Mnesia supports complex queries to retrieve data from a table in the form of matching and ad-hoc select functions.

Mnesia.match_object/1 函数可以通过模式匹配取回所有匹配的记录。如果有为任何一个字段添加索引的话,查询的效率会更高。不想某个字段参与匹配的话,可以用一个特殊的原子 :_ 来替代。

iex> Mnesia.transaction(
...>   fn ->
...>     Mnesia.match_object({Person, :_, "Marge Simpson", :_})
...>   end
...> )
{:atomic, [{Person, 4, "Marge Simpson", "home maker"}]}

Mnesia.select/2 函数允许我们通过一个查询函数来查询数据。下面的例子是选择所有 key 大于 3 的记录:

iex> Mnesia.transaction(
...>   fn ->
...>     Mnesia.select(Person, [{{Person, :"$1", :"$2", :"$3"}, [{:>, :"$1", 3}], [:"$$"]}])
...>   end
...> )
{:atomic, [[7, "Waylon Smithers", "Executive assistant"], [4, "Marge Simpson", "home maker"], [6, "Monty Burns", "Businessman"], [5, "Hans Moleman", "unknown"]]}

让我们来仔细看上面的例子。第一个参数是表名,Person,第二个参数是 {match, [guard], [result]}这样的形式:

  • match 跟你传给 Mnesia.match_object/1 函数的那个参数一样;但是请注意那个特别的原子:"$n"是用来指定后面部分的参数位置。
  • guard 列表里面包含了你想要应用的过滤函数和这个函数的参数的元组。在本例中, 是由内置函数 :> , 位置参数 :$1以及常数 3 组成。
  • result 列表是你希望查询返回的结果的字段的列表。:"$$" 用来表示返回所有字段,你也可以用 [:"$1", :"$2"] 来返回头两个字段。

更多的信息请参考 Erlang 的官方文档.

数据初始化和迁移

不管是什么软件解决方案,都会碰到需要更新你的系统并且迁移你数据库里的数据的时候。比方说,你在你的系统的第二版中需要往 Person 表中添加一个 :age 字段。我们不能在重新创建一个 Person 表了,但是我们可以改造这张表,我们还需要知道什么时候需要更改表。要实现这个,我们可以用 Mnesia.table_info/2 函数还获取现在的表结构,以及通过 Mnesia.transform_table/3 函数来改变表结构。

在下面的代码中,我们要实现这些逻辑:

  • 创建 v2 的表结构,包括这些属性: [:id, :name, :job, :age]
  • 根据建表函数的返回结果分别处理:
    • {:atomic, :ok}: 为 Person 表的 :job:age 字段添加索引
    • {:aborted, {:already_exists, Person}}: 检查现有的字段并且做相应的处理:
      • 如果是 v1 的字段列表 ([:id, :name, :job]),改造表结构,给所有人的年龄设为 21 并且在 :age 上添加索引
      • 如果已经是我们想要的 v2 的字段列表,则无需做任何处理
      • 如果是其他情况,则返回错误

Mnesia.transform_table/3 函数接受的参数列表为,表名和一个把旧的数据格式转换为新的数据格式的函数。

iex> case Mnesia.create_table(Person, [attributes: [:id, :name, :job, :age]]) do
...>   {:atomic, :ok} ->
...>     Mnesia.add_table_index(Person, :job)
...>     Mnesia.add_table_index(Person, :age)
...>   {:aborted, {:already_exists, Person}} ->
...>     case Mnesia.table_info(Person, :attributes) do
...>       [:id, :name, :job] ->
...>         Mnesia.transform_table(
...>           Person,
...>           fn ({Person, id, name, job}) ->
...>             {Person, id, name, job, 21}
...>           end,
...>           [:id, :name, :job, :age]
...>           )
...>         Mnesia.add_table_index(Person, :age)
...>       [:id, :name, :job, :age] ->
...>         :ok
...>       other ->
...>         {:error, other}
...>     end
...> end