在我以前的一篇文章中,我写了关于Erlang Term Storage表(或简称为ETS)的表,该表允许将任意数据的元组存储在内存中。 我们还讨论了基于磁盘的ETS(DETS),它提供的功能稍有限制,但允许您将内容保存到文件中。
但是,有时您可能需要更强大的解决方案来存储数据。 满足Mnesia —最初在Erlang中引入的实时分布式数据库管理系统。 Mnesia具有关系/对象混合数据模型,并具有许多不错的功能,包括复制和快速数据搜索。
在本文中,您将学习:
- 如何创建Mnesia模式并启动整个系统。
- 有哪些表类型可用,以及如何创建它们。
- 如何执行CRUD操作以及“脏”和“事务”功能之间有什么区别。
- 如何修改表和添加二级索引。
- 如何使用健忘包来简化数据库和表的使用。
让我们开始吧,好吗?
Mnesia简介
因此,如上所述,Mnesia是一个对象和关系数据模型,可以很好地扩展。 与任何其他流行的解决方案(例如Postgres或MySQL)一样,它具有DMBS查询语言并支持原子事务。 Mnesia的表可以存储在磁盘和内存中,但是程序可能在不知道实际数据位置的情况下编写。 此外,您可以跨多个节点复制数据。 还要注意,Mnesia与所有其他代码在同一BEAM实例中运行。
由于Mnesia是Erlang模块,因此您应该使用原子访问它:
:mnesia
虽然可以创建这样的别名:
alias :mnesia, as: Mnesia
Mnesia中的数据被组织成表格 , 表格中以原子表示自己的名字(这与ETS非常相似)。 这些表可以具有以下类型之一:
-
:set
set-默认类型。 您不能有多个行具有完全相同的主键(稍后我们将介绍如何定义主键)。 这些行没有以任何特定方式排序。 -
:ordered_set
—与:set
,但是数据由主键排序。 稍后,我们将了解:ordered_set
表的某些读取操作将有所不同。 -
:bag
—多个行可能具有相同的键,但是这些行仍然不能完全相同。
表具有其他属性,可以在官方文档中找到(我们将在下一节中讨论其中的一些属性)。 但是,在开始创建表之前,我们需要一个模式,因此让我们继续下一节并添加一个。
创建架构和表
为了创建一个新的模式,我们将使用一个名称非常奇怪的方法: create_schema/1
。 基本上,它将在磁盘上为我们创建一个新的数据库。 它接受一个节点作为参数:
:mnesia.create_schema([node()])
节点是处理其通信,内存和其他内容的Erlang VM 。 节点可以相互连接,并且不限于一台PC,您也可以通过Internet连接到其他节点。
运行上述代码后,将创建一个名为Mnesia.nonode@nohost的新目录,该目录将包含您的数据库。 nonode @ nohost是此处的节点名称。 但是,在创建任何表之前,必须先启动Mnesia。 这就像调用start/0
函数一样简单:
:mnesia.start()
Mnesia应该在所有参与的节点上启动,每个节点通常都具有一个将文件写入其中的文件夹(在本例中,该文件夹名为Mnesia.nonode@nohost )。 组成Mnesia系统的所有节点都被写入架构,以后您可以添加或删除单个节点。 此外,在启动时,节点交换模式信息以确保一切正常。
如果Mnesia成功启动,则会返回:ok
原子。 您可以稍后通过调用stop/0
停止系统:
:mnesia.stop() # => :stopped
现在我们可以创建一个新表。 至少,我们应该提供其名称和记录的属性列表(将它们视为列):
:mnesia.create_table(:user, [attributes: [:id, :name, :surname]])
# => {:atomic, :ok}
如果系统未运行,则不会创建该表{:aborted, {:node_not_running, :nonode@nohost}}
而是返回{:aborted, {:node_not_running, :nonode@nohost}}
错误。 此外,如果表已经存在,您将收到{:aborted, {:already_exists, :user}}
错误。
因此,我们的新表名为:user
,它具有三个属性:id
, :name
和:surname
。 请注意,列表中的第一个属性始终用作主键,我们可以利用它来快速搜索记录。 在本文的后面,我们将看到如何编写复杂的查询并添加二级索引。
另外,请记住,表的默认类型是:set
,但是可以很容易地更改它:
:mnesia.create_table(:user, [
attributes: [:id, :name, :surname],
type: :bag
])
您甚至可以通过将:access_mode
设置为:read_only:
来使表只读:read_only:
:mnesia.create_table(:user, [
attributes: [:id, :name, :surname],
type: :bag,
access_mode: read_only
])
创建模式和表之后,该目录将具有一个schema.DAT文件以及一些.log文件。 现在,让我们继续下一部分并将一些数据插入到新表中!
写操作
要将某些数据存储在表中,您需要利用write/1
函数 。 例如,让我们添加一个名为John Doe的新用户:
:mnesia.write({:user, 1, "John", "Doe"})
请注意,我们已经指定了表的名称和所有要存储的用户属性。 尝试运行代码...,它失败并显示{:aborted, :no_transaction}
错误。 为什么会这样呢? 好吧,这是因为write/1
函数应该在transaction中执行。 如果由于某种原因,您不想坚持使用事务,则可以使用dirty_write/1
以“肮脏的方式”完成写操作:
:mnesia.dirty_write({:user, 1, "John", "Doe"}) # => :ok
通常不建议使用这种方法,因此让我们借助transaction
功能构建一个简单的transaction
:
:mnesia.transaction(fn ->
:mnesia.write({:user, 1, "John", "Doe"})
end) # => {:atomic, :ok}
transaction
接受具有一个或多个分组操作的匿名函数。 请注意,在这种情况下,结果是{:atomic, :ok}
,而不仅仅是:ok
(与dirty_write
函数一样)。 这样做的主要好处是,如果在事务处理过程中出现问题,则会回滚所有操作。
实际上,这是一个原子性原则 ,它表示应该执行所有操作,或者在发生错误的情况下不应该执行任何操作。 例如,假设您要付给员工薪水,突然出现问题。 该操作停止了,您肯定不想在有些员工拿到了薪水而有些员工没有拿到薪水的情况下结束工作。 那时原子交易非常方便。
transaction
功能可以根据需要进行许多写操作:
write_data = fn ->
:mnesia.write({:user, 2, "Kate", "Brown"})
:mnesia.write({:user, 3, "Will", "Smith"})
end
:mnesia.transaction(write_data) # => {:atomic, :ok}
有趣的是,数据也可以使用write
功能进行更新。 只需为其他属性提供相同的键和新值:
update_data = fn ->
:mnesia.write({:user, 2, "Kate", "Smith"})
:mnesia.write({:user, 3, "Will", "Brown"})
end
:mnesia.transaction(update_data)
但是请注意,这不适用于:bag
类型的表。 因为这样的表允许多个记录具有相同的键,所以您将简单地得到两个记录: [{:user, 2, "Kate", "Brown"}, {:user, 2, "Kate", "Smith"}]
。 尽管如此, :bag
表仍不允许存在完全相同的记录。
读取操作
好吧,既然我们的表中有一些数据,为什么不尝试读取它们呢? 与写操作一样,您可以以“脏”或“事务”方式执行读取。 当然,“肮脏的方式”更简单(但这就是原力军的阴暗面,卢克!):
:mnesia.dirty_read({:user, 2}) # => [{:user, 2, "Kate", "Smith"}]
因此, dirty_read
根据提供的键返回找到的记录的列表。 如果表是:set
或:ordered_set
,则列表将只有一个元素。 对于:bag
表,列表当然可以包含多个元素。 如果未找到任何记录,则该列表将为空。
现在,让我们尝试执行相同的操作,但使用事务处理方法:
read_data = fn ->
:mnesia.read({:user, 2})
end
:mnesia.transaction(read_data) => {:atomic, [{:user, 2, "Kate", "Brown"}]}
大!
读取数据还有其他有用的功能吗? 但是当然! 例如,您可以获取表的第一条或最后一条记录:
:mnesia.dirty_first(:user) # => 2
:mnesia.dirty_last(:user) # => 2
dirty_first
和dirty_last
都有它们的交易对象,即first
和last
,应该包装在一个事务中。 所有这些函数返回记录的关键,但要注意,在这两种情况下,我们得到2
结果,即使我们有两个记录与按键2
和3
。 为什么会这样呢?
似乎对于:set
和:bag
表, dirty_first
和dirty_last
(以及first
和last
)函数是同义词,因为数据未按任何特定顺序排序。 但是,如果您有一个:ordered_set
表,则记录将按其键排序,结果将是:
:mnesia.dirty_first(:user) # => 2
:mnesia.dirty_last(:user) # => 3
也可以使用dirty_next
和dirty_prev
(或next
和prev
)来获取下一个或上一个键:
:mnesia.dirty_next(:user, 2) => 3
:mnesia.dirty_next(:user, 3) => :"$end_of_table"
如果没有更多记录,则返回一个特殊原子:"$end_of_table"
。 另外,如果表是:set
或:bag
,则dirty_next
和dirty_prev
是同义词。
最后,您可以使用dirty_all_keys/1
或all_keys/1
从表中获取所有键:
:mnesia.dirty_all_keys(:user) # => [3, 2]
删除作业
为了从表中删除记录,请使用dirty_delete
或delete
:
:mnesia.dirty_delete({:user, 2}) # => :ok
这将删除具有给定键的所有记录。
同样,您可以删除整个表:
:mnesia.delete_table(:user)
此方法没有“肮脏”的对应对象。 显然,删除表后,您将无法对其写入任何内容,并且将返回{:aborted, {:no_exists, :user}}
错误。
最后,如果您确实处于删除状态,则可以使用delete_schema/1
删除整个架构:
:mnesia.delete_schema([node()])
如果{:error, {'Mnesia is not stopped everywhere', [:nonode@nohost]}}
此操作将返回{:error, {'Mnesia is not stopped everywhere', [:nonode@nohost]}}
错误,因此请不要忘记这样做:
:mnesia.stop()
:mnesia.delete_schema([node()])
更复杂的读取操作
既然我们已经了解了使用Mnesia的基础知识,那么让我们更深入地了解如何编写高级查询。 首先,有match_object
和dirty_match_object
函数可用于根据提供的属性之一搜索记录:
:mnesia.dirty_match_object({:user, :_, "Kate", "Brown"})
# => [{:user, 2, "Kate", "Brown"}]
不需要的属性都用:_
原子标记。 您可以仅设置姓氏,例如:
:mnesia.dirty_match_object({:user, :_, :_, "Brown"})
# => [{:user, 2, "Kate", "Brown"}]
您还可以使用select
和dirty_select
提供自定义搜索条件。 为了了解这一点,首先让我们用以下值填充表:
write_data = fn ->
:mnesia.write({:user, 2, "Kate", "Brown"})
:mnesia.write({:user, 3, "Will", "Smith"})
:mnesia.write({:user, 4, "Will", "Smoth"})
:mnesia.write({:user, 5, "Will", "Smath"})
end
:mnesia.transaction(write_data)
现在,我要做的是查找所有以Will
为名称且键小于5
,这意味着结果列表应仅包含“ Will Smith”和“ Will Smoth”。 这是相应的代码:
:mnesia.dirty_select(
:user,
[{
{:user, :"$1", :"$2", :"$3"},
[
{:<, :"$1", 5},
{:==, :"$2", "Will"}
],
[:"$$"]
}]
) # => [[3, "Will", "Smith"], [4, "Will", "Smoth"]]
这里的事情要复杂一些,所以让我们逐步讨论一下此片段。
- 首先,我们有
{:user, :"$1", :"$2", :"$3"}
部分。 在这里,我们提供了表名和位置参数列表。 它们应该以这种看起来很奇怪的形式编写,以便以后使用。$1
对应于:id
,$2
是name
,$3
是surname
。 - 接下来,有一系列应应用于给定参数的保护功能。
{:<, :"$1", 5}
表示仅选择标记为$1
(即:id
)的属性小于5
。{:==, :"$2", "Will"}
反过来意味着我们正在选择:name
设置为"Will"
。 - 最后,
[:"$$"]
表示我们要在结果中包括所有字段。 您可以说[:"$2"]
仅显示名称。 注意,顺便说一下,结果包含一个列表列表:[[3, "Will", "Smith"], [4, "Will", "Smoth"]]
。
您也可以将某些属性标记为不喜欢使用:_
原子的属性。 例如,让我们忽略姓:
:mnesia.dirty_select(
:user,
[{
{:user, :"$1", :"$2", :_},
[
{:<, :"$1", 5},
{:==, :"$2", "Will"}
],
[:"$$"]
}]
) # => [[3, "Will"], [4, "Will"]]
但是,在这种情况下,姓氏不会包含在结果中。
修改表格
执行转型
现在假设我们要通过添加新字段来修改表。 这可以通过使用transform_table
函数来完成,该函数接受表名,一个适用于所有记录的函数以及新属性列表:
:mnesia.transform_table(
:user,
fn ({:user, id, name, surname}) ->
{:user, id, name, surname, :rand.uniform(1000)}
end,
[:id, :name, :surname, :salary]
)
在此示例中,我们添加了一个名为:salary
的新属性(在最后一个参数中提供)。 至于转换函数 (第二个参数),我们将此新属性设置为随机值。 您还可以修改此转换函数内的任何其他属性。 更改数据的过程称为“迁移”,并且这个概念应该对来自Rails世界的开发人员很熟悉。
现在,您可以使用table_info
来获取有关表属性的table_info
:
:mnesia.table_info(:user, :attributes) # => [:id, :name, :surname, :salary]
:salary
属性在那里! 而且,当然,您的数据也到位:
:mnesia.dirty_read({:user, 2}) # => [{:user, 2, "Kate", "Brown", 778}]
您可以在ElixirSchool网站上找到同时使用create_table
和transform_table
函数的更为复杂的示例。
添加索引
Mnesia允许您使用add_table_index
函数对任何属性建立索引。 例如,让我们对:surname
属性建立索引:
:mnesia.add_table_index(:user, :surname) # => {:atomic, :ok}
如果索引已经存在,您将收到错误{:aborted, {:already_exists, :user, 4}}
。
正如该函数的文档所述,索引不是免费提供的。 具体来说,它们占用额外的空间(与表大小成比例),并使插入操作稍微慢一些。 另一方面,它们使您可以更快地搜索数据,因此这是一个公平的权衡。
您可以使用dirty_index_read
或index_read
函数按索引字段进行搜索:
:mnesia.dirty_index_read(:user, "Smith", :surname)
# => [{:user, 3, "Will", "Smith"}]
在这里,我们使用二级索引:surname
搜索用户。
使用失忆症
直接使用Mnesia模块可能有点乏味,但是幸运的是,有一个名为Amnesia (duh!)的第三方程序包,使您可以更轻松地执行琐碎的操作。
例如,您可以这样定义数据库和表:
use Amnesia
defdatabase Demo do
deftable User, [{ :id, autoincrement }, :name, :surname, :email], index: [:email] do
end
end
这将使用表User
定义一个名为Demo
的数据库。 用户将要命名一个名字,一个姓氏,一个电子邮件(一个索引字段)和一个id(将主键设置为自动递增)。
接下来,您可以使用内置的混合任务轻松创建模式:
mix amnesia.create -d Demo --disk
在这种情况下,数据库将基于磁盘,但是您可以设置其他一些可用选项 。 还有一个删除任务,显然将破坏数据库和所有数据:
mix amnesia.drop -d Demo
可以同时破坏数据库和架构:
mix amnesia.drop -d Demo --schema
有了数据库和架构,就可以对表执行各种操作。 例如,创建一个新记录:
Amnesia.transaction do
will_smith = %User{name: "Will", surname: "Smith", email: "will@smith.com"} |> User.write
end
或通过ID获取用户:
Amnesia.transaction do
will_smith = User.read(1)
end
此外,您可以在User
user_id
作为外键建立与User
表的关系时定义Message
表:
deftable Message, [:user_id, :content] do
end
这些表内部可能有一堆帮助函数,例如,创建一条消息或获取所有消息:
deftable User, [{ :id, autoincrement }, :name, :surname, :email], index: [:email] do
def add_message(self, content) do
%Message{user_id: self.id, content: content} |> Message.write
end
def messages(self) do
Message.read(self.id)
end
end
现在,您可以查找用户,为其创建消息或轻松列出其所有消息:
Amnesia.transaction do
will_smith = User.read(1)
will_smith |> User.add_message "hi!"
will_smith |> User.messages
end
很简单,不是吗? 其他一些用法示例可以在失忆症的官方网站上找到 。
结论
在本文中,我们讨论了可用于Erlang和Elixir的Mnesia数据库管理系统。 我们讨论了此DBMS的主要概念,并了解了如何创建模式,数据库和表以及如何执行所有主要操作:创建,读取,更新和销毁。 最重要的是,您学习了如何使用索引,如何转换表以及如何使用Amnesia包来简化数据库的使用。
我真的希望这篇文章对您有所帮助,并且您也渴望在行动中尝试Mnesia。 与往常一样,我感谢您与我在一起,直到下一次!
翻译自: https://code.tutsplus.com/articles/store-everything-with-elixir-and-mnesia--cms-29821