1. OverView
うん、チートシートもあるけど、並行して色々使わないと忘れる。
もうmap忘れてた!忘れて、データの管理システムとしてETSを頑張ろう!と思った次第。
本日は、ETSでご機嫌を伺おうかと思います。
Erlang Term Storage (ETS)はOTP内部に組み込まれている強力なインメモリストレージエンジンで、
非常に大きなデータセットを扱える様です。
ほかにも、DETS(Disk-based Erlang Term Storage)と言うディスクを使用してデータをディスクに永続化するバージョンもありますが、とりあえず、ETSをやっていきたいと思います。
今回は、さくっとElixir School準拠でやって行きますね。
2. まずは解説
2.1 テーブルの作成
:etc.newでまずはテーブルを作ります。
new(Name, Options) -> tid() | atom()
Name = atom()
Options = [Option]
Optinsは解説するにはまだ調べてないので、デカいことを言えませんな。
iex(4)> table = :ets.new(:user_lookup, [:set, :protected])
#Reference<0.614714515.3794403338.130351>
iex(5)> table = :ets.new(:user_lookup, [:set, :protected, :named_table])
:user_lookup
protected -
The owner process can read and write to the table. Other processes can only read the table. This is the default setting for the access rights.
set
The table is a set table: one key, one object, no order among objects. This is the default table type.
オプションに:named_tableが追加されている事に注意してください。
一つ目の書き方だと、:ets.newの戻り値にバインドされた変数、tableを介してアクセスするのですが、
二つ目の書き方だと、名前である:user_lookupでアクセス出来る様です。
なので、このサンプルでは後者で行きます。
さて、今、ビビっているので原文で僕の気持ちを共有してください。
ETSはスキーマを持ちません。唯一の制限は、データは最初の項がキーであるタプルとして格納されなくてはいけないというものです。
フリーダムだなぁ。後で、検索を読んでから、どこまで自由か、後ほど試してみましょうか。
では、insertのサンプル
iex(6)> :ets.insert(:user_lookup, {"doomspork", "Sean", ["Elixir", "Ruby", "Java"]})
true
次、更新…いや、INSERTか?
と言うのも、insert/2をテーブル作成時にset, ordered_setを指定したテーブルに対して実行すると、keyが同じ場合は上書きされちゃうそうです。
ちょっと早いですが、lookup/2を使って、中身を見てみましょう。
iex(6)> :ets.insert(:user_lookup, {"doomspork", "Sean", ["Elixir", "Ruby", "Java"]})
true
iex(7)> :ets.lookup(:user_lookup, "doomspork")
[{"doomspork", "Sean", ["Elixir", "Ruby", "Java"]}]
iex(8)> :ets.insert(:user_lookup, {"doomspork", "Sean", ["Elixir", "Ruby", "JavaScript"]})
true
iex(9)> :ets.lookup(:user_lookup, "doomspork")
[{"doomspork", "Sean", ["Elixir", "Ruby", "JavaScript"]}]
updateが要らんのは良いけど、バグりそう…まぁ、なので、ちゃんと対策があります。
キーが存在している場合に false を返してくれる insert_new/2 があるのです。
iex(10)> :ets.insert_new(:user_lookup, {"doomspork", "Sean", ["Elixir", "Ruby", "Java"]})
false
iex(11)> :ets.insert_new(:user_lookup, {"3100", "", ["Elixir", "Ruby", "JavaScript"]})
true
iex(12)> :ets.lookup(:user_lookup, "doomspork")
[{"doomspork", "Sean", ["Elixir", "Ruby", "JavaScript"]}]
iex(13)> :ets.lookup(:user_lookup, "3100")
[{"3100", "", ["Elixir", "Ruby", "JavaScript"]}]
:ets.insert_new/2では、重複時にfalseが返り、失敗するので、安心ですね。
2.2 データのマッチ
さて、お待ちかね検索となります。
もう、上で使ってしまいましたが、順に、「俺が納得するまで」弄り倒しましょう。
…ごめん、つきあって、
2.2.1 キー探索(:ets.lookup)
上で例が出ておりますね。:ets.lookup/2です。
:ets.lookup(テーブル名, KEY)
…第一引数がテーブル名で済むのは、テーブル作成時にオプションに:named_tableを設定しているからですね。
iex(2)> :ets.insert(:user_lookup, {"doomspork", "Sean", ["Elixir", "Ruby", "Java"]})
true
iex(3)> :ets.insert(:user_lookup, {"doomspork2", "mokemoke", ["Elixir", "Ruby", "Java"], "beginner"})
true
iex(4)> :ets.lookup(:user_lookup, "doomspork")
[{"doomspork", "Sean", ["Elixir", "Ruby", "Java"]}]
iex(5)> :ets.lookup(:user_lookup, "doomspork2")
[{"doomspork2", "mokemoke", ["Elixir", "Ruby", "Java"], "beginner"}]
iex(6)>
ついでなんで、しれっと混ぜておりますが、前述の「ETSはスキーマを持ちません」って話は間違いないですね。
tableをsetに設定した場合、keyに対して、タプルを一つ格納してくれるDBって捉えるのがよいのかもしれません。
2.2.2 単純なマッチ(:ets.match/2)
さぁ、皆さんご一緒に、「どこが単純じゃ!」
やってみたら単純でした。
マッチ内で変数(variable )を指定するには、 :"$1" 、 :"$2" 、 :"$3" などのアトムを用います。
変数の数字は結果の位置を示していて、マッチの位置ではありません。
興味のない値については、 :_ 変数を用います。
:"$1"や:_が変数ですね。
そう考えると、単純タプルをマッチしてくれている様です。
iex(4)> :ets.lookup(:user_lookup, "doomspork")
[{"doomspork", "Sean", ["Elixir", "Ruby", "Java"]}]
iex(6)> :ets.match(:user_lookup, {:"$1", "Sean", :_})
[["doomspork"]]
"$1"だけが返っていますね。
iex(16)> :ets.lookup(:user_lookup, "doomspork2")
[{"doomspork2", "mokemoke", ["Elixir", "Ruby", "Java"], "beginner"}]
iex(17)> :ets.match(:user_lookup, {:"$1", "mokemoke", :"$3" })
[] <-タプルがマッチしてないので、検索結果が返ってこない。
iex(18)> :ets.match(:user_lookup, {:"$1", "mokemoke", :"$3", :"$4" })
[["doomspork2", ["Elixir", "Ruby", "Java"], "beginner"]]
iex(21)> :ets.match(:user_lookup, {:"$3", "mokemoke", :_, :_ })
[["doomspork2"]]
どうやら、:"$[1-9}"と言うタプルの数字は、変数を表すので数字はなんも良いようです。
これは、Erlangを学ばんとダメかなー。
次に、match_object/2となります。
こちらは、matchはマッチした変数(結果)だけを返しましたが、こちらは全体を返してくれます。
iex(4)> :ets.match_object(:user_lookup, {:"$1", :_, :"$3"})
[
{"doomspork", "Sean", ["Elixir", "Ruby", "Java"]},
{"3100", "", ["Elixir", "Ruby", "JavaScript"]}
]
iex(5)> :ets.match(:user_lookup, {:"$1", :_, :"$3"})
[
["doomspork", ["Elixir", "Ruby", "Java"]],
["3100", ["Elixir", "Ruby", "JavaScript"]]
]
違いがわかるでしょうか?"Sean"が、match_objectの中に入ってますね。
ets.match_objectはレコード全体を返してくれています。
2.2.3 発展的な探索(select/2)
select/2を紹介する前に、まずは、テストデータを入れ替えますね。
iex(10)> :ets.insert_new(:user_lookup, {"doomspork", "Sean", ["Elixir", "Ruby", "Java"]})
false
iex(11)> :ets.insert_new(:user_lookup, {"3100", "", ["Elixir", "Ruby", "JavaScript"]})
true
まずは、:ets.match_objectで検索してみましょう。
2レコードとも、オブジェクトが返っていることにご注意。
iex(6)> :ets.match_object(:user_lookup, {:"$1", :_, :"$3"})
[
{"doomspork", "Sean", ["Elixir", "Ruby", "Java"]},
{"3100", "", ["Elixir", "Ruby", "JavaScript"]}
]
では、select/2です。
select(Tab, MatchSpec, Limit) ->
{[Match], Continuation} | '$end_of_table'
第二引数が、LISTになっているので、ご注意ください。
iex(7)> :ets.select(:user_lookup, [{{:"$1", :_, :"$3"}, [], [:"$_"]}])
[
{"doomspork", "Sean", ["Elixir", "Ruby", "Java"]},
{"3100", "", ["Elixir", "Ruby", "JavaScript"]}
]
引数のMatchSpecは、LIST形式で、以下の三つを与えます。
[{match_pattern(), [term()], [term()]}]
詳しくは以下を参照。
https://www.erlang.org/docs/23/man/ets#type-match_spec
LIST内部。一つ目の引数はmatch_pattern()…これは上でさんざんやった{:"$1", :_, :"$3"}ですね。
次は、ガード条件、今回は空リスト
今回は三つ目の引数で、戻り値を指定できます。
今まで使った"$1"に加え、以下のものが指定出来ます。
:"$$" 結果をリストで返す
:"$_" 結果は元のデータオブジェクトで返す。
二つ目のガード条件を弄ってみましょう。
iex(14)> :ets.select(:user_lookup, [{{:"$1", :_, :"$3"}, [{:"==", :"$1", "doomspork"}], [:"$$"]}])
[["doomspork", ["Elixir", "Ruby", "Java"]]]
GUARD節で、条件が変わったのを確認できるでしょうか?
と、複雑なので次の項でfun2ms/1により簡潔な記述を試みます。
2.2.4 発展的な探索を簡潔にしてみる(select/2とfun2ms/1)
さて、先ほど話した検索条件、match_specを、関数で書く方法があります。
:ets.fun2msで、匿名変数で条件が置き換えられている事にご注意ください。
iex(19)> fun = :ets.fun2ms(fn {username, _, langs} when length(langs) > 2 -> username end)
[{{:"$1", :_, :"$2"}, [{:>, {:length, :"$2"}, 2}], [:"$1"]}]
iex(20)> :ets.select(:user_lookup, fun)
["doomspork", "3100"]
抜き出してみますね?
fn {username, _, langs} when length(langs) > 2 -> username end
条件も戻り値も ->の先で指定しておりますな。
2.3 レコードの除去(:ets.delete/2)
:ets.delete/2で、除去した条件を削除します。
iex(23)> :ets.delete(:user_lookup, "doomspork")
true
iex(24)>
2.4 テーブルの除去(:ets.delete/1)
:ets.delete/1で、テーブルごと削除します。
iex(24)> :ets.delete(:user_lookup)
true
3. 本日のチートシート
うーん、もうちょいmarkdownを調べないと、読みにくいんだよなぁ。
とりあえず、書いときますね。
やりたいこと | 例 |
---|---|
テーブルの作成 | iex(5)> table = :ets.new(:user_lookup, [:set, :protected, :named_table]) :user_lookup |
テーブルの挿入 | iex(6)> :ets.insert(:user_lookup, {"doomspork", "Sean", ["Elixir", "Ruby", "Java"]}) true |
テーブルの更新 | insertをすると、同じキーを持っているレコードが置き換えられる |
テーブルを更新せずに挿入 | iex(10)> :ets.insert_new(:user_lookup, {"doomspork", "Sean", ["Elixir", "Ruby", "Java"]}) |
データの検索 | |
キー探索 | iex(4)> :ets.lookup(:user_lookup, "doomspork") |
単純なマッチ | :ets.match/2を使用する iex(6)> :ets.match(:user_lookup, {:"$1", "Sean", :_}) |
:ets.match_objectを使用する。オブジェクト(レコード)全体が返る iex(4)> :ets.match_object(:user_lookup, {:"$1", :_, :"$3"}) |
|
発展的な探索 | iex(7)> :ets.select(:user_lookup, [{{:"$1", :, :"$3"}, [], [:"$"]}]) |
fun2ms/1を使うと、match_specを関数で記述出来る iex(19)> fun = :ets.fun2ms(fn {username, _, langs} when length(langs) > 2 -> username end) |
|
レコードの除去 | iex(23)> :ets.delete(:user_lookup, "doomspork") |
テーブルの除去 | :ets.delete/1で、テーブルごと削除します。 iex(24)> :ets.delete(:user_lookup) |