一般都需要用数据库进行发发工作。Mnesia是一种用Erlang编写的数据库,速度很快。
创建数据库
PS D:\Erlang6m\first\src> erl
Eshell V10.7 (abort with ^G)
1> mnesia:create_schema([node()]).
ok
2> init:stop().
ok
3>
D:\IdeaProjects\erlangdemo\src\demo5> dir
目录: D:\Erlang6m\first\src
Mode LastWriteTime Length Name
---- ------------- ------ ----
d----- 2023/8/1 16:42 ...
d----- 2023/9/14 20:55 Mnesia.nonode@nohost
mnesia:create_schema([node()])函数指定在[node()]节点列表(即当前节点下)初始化一个mnesia数据库。
创建Mnesia数据库操作,只需执行一次。执行成功可看到新建了Mnesia.nonode@nohost目录
代码清单
-module(test_mnesia).
-author("").
-include_lib("stdlib/include/qlc.hrl").
-export([do_this_once/0,start/0,reset_tables/0,example_tables/0,demo/1,add_shop_item/3,remove_shop_item/1]).
-record(shop, {item, quantity, cost}).
-record(cost, {name, price}).
-record(design, {id, plan}).
do_this_once() ->
mnesia:create_schema([node()]),
mnesia:start(),
mnesia:create_table(shop, [{attributes, record_info(fields, shop)}]),
mnesia:create_table(cost, [{attributes, record_info(fields, cost)}]),
mnesia:create_table(design, [{attributes, record_info(fields, design)}]),
mnesia:stop().
start() ->
mnesia:start(),
mnesia:wait_for_tables([shop,cost,design], 20000).
reset_tables() ->
mnesia:clear_table(shop),
mnesia:clear_table(cost),
F = fun() ->
lists:foreach(fun mnesia:write/1, example_tables())
end,
mnesia:transaction(F).
example_tables() ->
[%% shop 表
{shop, apple, 20, 2.3},
{shop, orange, 100, 3.8},
{shop, pear, 200, 3.6},
{shop, banana, 420, 4.5},
{shop, potato, 2456, 1.2},
%% cost 表
{cost, apple, 1.5},
{cost, orange, 2.4},
{cost, pear, 2.2},
{cost, banana, 1.5},
{cost, potato, 0.6}
].
demo(select_shop) ->
do(qlc:q([X || X <- mnesia:table(shop)]));
demo(select_some) ->
do(qlc:q([{X#shop.item, X#shop.quantity} || X <- mnesia:table(shop)]));
demo(reorder) ->
do(qlc:q([X#shop.item || X <- mnesia:table(shop), X#shop.quantity < 250]));
demo(join) ->
do(qlc:q([X#shop.item || X <- mnesia:table(shop),
X#shop.quantity < 250,
Y <- mnesia:table(cost),
X#shop.item =:= Y#cost.name,
Y#cost.price < 2
])).
add_shop_item(Name, Quantity, Cost) -> % 添加数据
Row = #shop{item=Name, quantity=Quantity, cost=Cost},
F = fun() ->
mnesia:write(Row)
end,
mnesia:transaction(F).
remove_shop_item(Item) -> % 删除数据
Oid = {shop, Item},
F = fun() ->
mnesia:delete(Oid)
end,
mnesia:transaction(F).
do(Q) ->
F = fun() -> qlc:e(Q) end,
{atomic, Val} = mnesia:transaction(F),
Val.
启动和载入表(准备工作)
PS D:\Erlang6m\first\src> erl
stopped
3> test_mnesia:start().
ok
4> test_mnesia:reset_tables().
{atomic,ok}
查询所有数据
demo(select_shop) ->
do(qlc:q([X || X <- mnesia:table(shop)]))
qlc:q/1的参数必须是列表解析语法语句本身,不能是其他任何结果相同的表达式。
数据行以不确定的顺序出现,没有做任何排序处理。
运行结果:
5> test_mnesia:demo(select_shop).
[{shop,potato,2456,1.2},
{shop,apple,20,2.3},
{shop,orange,100,3.8},
{shop,pear,200,3.6},
{shop,banana,420,4.5}]
查询指定列数据
demo(select_some) ->
do(qlc:q([{X#shop.item, X#shop.quantity} || X <- mnesia:table(shop)]))
运行结果:
6> test_mnesia:demo(select_some).
[{potato,2456},
{apple,20},
{orange,100},
{pear,200},
{banana,420}]
条件查询
demo(reorder) ->
do(qlc:q([X#shop.item || X <- mnesia:table(shop), X#shop.quantity < 250]))
把查询条件写到列表解析的语句中。
运行结果:
7> test_mnesia:demo(reorder).
[apple,orange,pear]
多表关联查询
demo(join) ->
do(qlc:q([X#shop.item || X <- mnesia:table(shop),
X#shop.quantity < 250,
Y <- mnesia:table(cost),
X#shop.item =:= Y#cost.name,
Y#cost.price < 2
]))
在shop表的item字段和cost表的name字段之间进行关联,语句如 X#shop.item=:=Y#cost.name
运行结果:
8> test_mnesia:demo(join).
[apple]
添加数据
add_shop_item(Name, Quantity, Cost) ->
Row = #shop{item=Name, quantity=Quantity, cost=Cost},
F = fun() ->
mnesia:write(Row)
end,
mnesia:transaction(F).
如果新建的记录和表中的记录主键相同,那么老数据会被覆盖,若不然就作为新的一行数据插入。
运行结果:
9> test_mnesia:demo(select_shop).
[{shop,potato,2456,1.2},
{shop,apple,20,2.3},
{shop,orange,100,3.8},
{shop,pear,200,3.6},
{shop,banana,420,4.5}]
10> test_mnesia:add_shop_item(peach,100,3.0).
{atomic,ok}
11> test_mnesia:add_shop_item(orange,236,2.8).
{atomic,ok}
12> test_mnesia:demo(select_shop).
[{shop,potato,2456,1.2},
{shop,apple,20,2.3},
{shop,peach,100,3.0},
{shop,orange,236,2.8},
{shop,pear,200,3.6},
{shop,banana,420,4.5}]
删除数据
remove_shop_item(Item) ->
Oid = {shop, Item},
F = fun() ->
mnesia:delete(Oid)
end,
mnesia:transaction(F).
删除数据需要知道这一行数据的目标ID(OID)。OID由表的名称和主键键值构成
运行结果:
13> test_mnesia:remove_shop_item(lemon).
{atomic,ok}
14> test_mnesia:demo(select_shop).
[{shop,potato,2456,1.2},
{shop,apple,20,2.3},
{shop,peach,100,3.0},
{shop,orange,236,2.8},
{shop,pear,200,3.6},
{shop,banana,420,4.5}]
Mnesia事务
farmer(…) ->
F = fun() ->
mnesia:write(Row),
…
mnesia:delete(Oid),
qlc:e(Q) % 用qlc:q/1编译过的查询语句
end,
mnesia:transaction(F).
在匿名函数内部是mnesia:write/1,mnesia:delete/1,qlc:e(Q)这些操作的组合。
调用mnesia:transaction(F)来执行这个函数中的表达式序列,即执行这个匿名函数体。
这种操作能避免死锁问题。可以保证每次Mnesia交易访问一个表时,他会试图进行锁定,具体锁定单个记录还是整张表,他会根据查询语句进行判断。如果判断出这个查询可能会导致死锁,就会立即取消事务,并恢复这个过程中发生的任何更改。
如果锁定时,因为有其他的进程,正在访问数据,因而事物无法成功初始化,系统会稍后一会儿来重试这个事物,所以事物函数中的代码可能会被执行很多次。
所以,交易方法内的代码应该避免任何副作用,比如若函数内有IO输出,因为这个函数可能会被多次重试,会产生大量输出。
取消一个事务
以拿两个苹果换一个橘子为例
代码演示:
farmer(Nwant) ->
%% Nwant = 农民计划购买的橙子数量
F = fun() ->
%% 找出苹果数量
[Apple] = mnesia:read({shop,apple}),
Napples = Apple#shop.quantity,
Apple1 = Apple#shop{quantity = Napples + 2*Nwant},
%% 更新数据库
mnesia:write(Apple1),
%% 找到橘子数量
[Orange] = mnesia:read({shop,orange}),
NOranges = Orange#shop.quantity,
if
NOranges >= Nwant ->
N1 = NOranges - Nwant,
Orange1 = Orange#shop{quantity=N1},
%% 更新数据库
mnesia:write(Orange1);
true ->
%% 橘子数量不足
mnesia:abort(oranges)
end
end,
mnesia:transaction(F).
我们应该在检查橘子数量,数量足够再去做苹果数量的更新,但现在一上来就做更新是为了演示事务机制,更新操作在事务失败时会回滚。成功才会把橘子和苹果的数据写回到数据库。
执行演示:
1> c(test_mnesia).
{ok,test_mnesia}
2> test_mnesia:start().
ok
3> test_mnesia:reset_tables().
{atomic,ok}
4> test_mnesia:demo(select_shop).
[{shop,potato,2456,1.2},
{shop,apple,20,2.3},
{shop,orange,100,3.8},
{shop,pear,200,3.6},
{shop,banana,420,4.5}]
5> test_mnesia:farmer(50).
{atomic,ok}
6> test_mnesia:demo(select_shop).
[{shop,potato,2456,1.2},
{shop,apple,120,2.3},
{shop,orange,50,3.8},
{shop,pear,200,3.6},
{shop,banana,420,4.5}]
7> test_mnesia:farmer(100).
{aborted,oranges}
8> test_mnesia:demo(select_shop).
[{shop,potato,2456,1.2},
{shop,apple,120,2.3},
{shop,orange,50,3.8},
{shop,pear,200,3.6},
{shop,banana,420,4.5}]
表的类型和位置
我们可以选择将表存储在内存或磁盘上,或者同时存在内存和磁盘上。也可以将数据表保存在一台机器上,或者在多台机器上备份。
内存表
非常快,但数据是瞬态的,当系统崩溃或停掉数据库时,这些数据都会丢失。
使用内存表时一定要检查当前系统的物理内存能否完整,装下这张表的全部数据,如果物理内存不够大,操作系统会使用虚拟内存,意味着大量的内存-磁盘数据会有换页操作,会导致性能的急剧恶化。
内存表的数据丢失问题很重要,可以给这个表在本机建立一个磁盘备份,或者在另外的机器上做个备份。可以是内存的,可以是磁盘的,也可以是两者混合型的。
磁盘表
系统崩溃不会导致其中的数据丢失,前提是没有发生磁盘物理损坏。
对于磁盘表来说,每次成功提交一个事务时,底层都会先将数据写到一个磁盘日志中,这个日志会持续保持增长,每隔一段时间会把日志中的数据同步到数据表中,并清除掉日志中相应条目。如果系统崩溃,下次重启时会检查这个磁盘日志,先将尚未写入的数据同步到数据库中,才会开放数据库服务。所以,只要一个事务成功,那么,其中的数据就会被写到磁盘日志中。尽管系统崩溃,也不会影响这个交易的数据。下次重启时,会自动将这些数据同步到数据表中。
磁盘表只有在一种情况下会发生数据丢失的情况,那就是在系统提交过程中崩溃,此时交易没有成功,提交数据没有写入磁盘日志,所以重启时无法进行同步。