Etsの使用方法に関するサンプルというのは、全てMnesiaのサンプルに関連するものです。一般的にはEtsに関するサンプルは全て、Detsテーブルに対しても適用することができます。
EtsテーブルとMnesiaテーブルに関するセレクト、マッチの操作は非常に高価な操作になる可能性があります。これらの操作を行うときはテーブルを全てスキャンする必要があります。そのため、セレクト、マッチの操作を行う必要性を最小限するにようなデータ構造を目指さなければなりません。しかし、もし本当にセレクト、マッチの操作が必要な場合には、これは tab2list を使用する場合と比べれば、まだ効率的です。これから続くセクションでは、このことや、セレクト、マッチを避ける方法に関するサンプルを提供していきます。 ets:select/2 と、 mnesia:select/3 という関数は ets:match/2, ets:match_object/2, mnesia:match_object/3 と比べればまだ好ましいと言えるでしょう。
Note
テーブル全体がスキャンされない場合には、いくつか例外的なケースがあります。例えば、 ordered_set テーブルを検索する時に、キーの一部が制限されている場合、もしくは、セカンダリーインデックスにより、効率的にMnesiaテーブル上のフィールドを検索している場合などです。もちろん、これは選択やマッチのときに、 bag 形式のテーブルを効率的に使用してインデックスを利用して、特定のサブセットのアクセスだけに限定できた場合に限ります。
選択、マッチ操作の中で使用される、一部以外のフィールドの値が '_' というレコードを作成する場合(訳注: この例では年齢だけがリテラルで、他のフィールドはすべて '_' )は、以下のようなコードがもっとも簡単で最速の実装になります:
#person{age = 42, _ = '_'}.
削除操作はテーブルの中に要素がなかったとしても成功とみなされます。そのため、すべての人は削除する前にEts/Mnesiaテーブルに要素が存在するかどうかチェックしたがりますが、それは不要です。Etsテーブルに関するサンプルを以下に示します。
推奨:
...
ets:delete(Tab, Key),
...
非推奨:
...
case ets:lookup(Tab, Key) of
[] ->
ok;
[_|_] ->
ets:delete(Tab, Key)
end,
...
すでに持っているデータを取得することは避けましょう。抽象データ型の Person を操作するモジュールがあったとします。このモジュールはインタフェースとなる関数, print_person/1 をエクスポートしています。また、この関数は内部関数として print_name/1, print_age/1, print_occupation/1 の3つの関数を利用しています。
Note
print_name/1 などの関数をインタフェース関数を定義すると、問題を明るみにだすことができます。ユーザ向けのインタフェースとして、データの内部表現を公開して知らせたくない場合に効果を発揮します。
推奨:
%%% インタフェース関数
print_person(PersonId) ->
%% 名前付きテーブルのpersionから人を検索
case ets:lookup(person, PersonId) of
[Person] ->
print_name(Person),
print_age(Person),
print_occupation(Person);
[] ->
io:format("No person with ID = ~p~n", [PersonID])
end.
%%% 内部関数
print_name(Person) ->
io:format("No person ~p~n", [Person#person.name]).
print_age(Person) ->
io:format("No person ~p~n", [Person#person.age]).
print_occupation(Person) ->
io:format("No person ~p~n", [Person#person.occupation]).
非推奨:
%%% インタフェース関数
print_person(PersonId) ->
%% 名前付きテーブルのpersonから人を検索
case ets:lookup(person, PersonId) of
[Person] ->
print_name(PersonID),
print_age(PersonID),
print_occupation(PersonID);
[] ->
io:format("No person with ID = ~p~n", [PersonID])
end.
%%% 内部関数
print_name(PersonID) ->
[Person] = ets:lookup(person, PersonId),
io:format("No person ~p~n", [Person#person.name]).
print_age(PersonID) ->
[Person] = ets:lookup(person, PersonId),
io:format("No person ~p~n", [Person#person.age]).
print_occupation(PersonID) ->
[Person] = ets:lookup(person, PersonId),
io:format("No person ~p~n", [Person#person.occupation]).
永続化しないデータストレージとしては、Mnesiaの local_content テーブルよりもEtsテーブルの方が良いです。例えMnesiaの dirty_write 操作が、Ets書き込みと比較して、決まった大きさのオーバーヘッドしかかからないとしても同様です。Mnesiaはもしテーブルが複製されたり、インデックス付けをしたりする場合にはチェックが必要になり、毎回の dirty_write の操作ごとに少なくとも一回のEtsの探索が行われることになります。そのため、Etsの書き込みは、つねにMnesiaの書き込みよりも高速です。
idnoをキーとして、以下のデータを含むEtsテーブルがあったとします:
[#person{idno = 1, name = "Adam", age = 31, occupation = "mailman"},
#person{idno = 2, name = "Bryan", age = 31, occupation = "cashier"},
#person{idno = 3, name = "Bryan", age = 35, occupation = "banker"},
#person{idno = 4, name = "Carl", age = 25, occupation = "mailman"}]
もしEtsテーブルに保存されているすべてのデータを取り出さなければならない場合には、 ets:tab2list/1 を使用することができます。しかし、通常の使い方では、データの全部ではなく、そのサブセットだけが必要なことが多いため、 ets:tab2list/1 ではコストがかかりすぎてしまいます。例えば、それぞれのレコードの中の一つのフィールド、ここでは全員の年齢だけが必要になったとします。その場合は以下のように書きます:
.. DO
推奨:
...
ets:select(Tab,[{ #person{idno='_',
name='_',
age='$1',
occupation = '_'},
[],
['$1']}]),
...
非推奨:
...
TabList = ets:tab2list(Tab),
lists:map(fun(X) -> X#person.age end, TabList),
...
もし、Bryanという名前のすべての人の年齢が必要になったとしたら、以下のように書きます:
.. DO
推奨:
...
ets:select(Tab,[{ #person{idno='_',
name="Bryan",
age='$1',
occupation = '_'},
[],
['$1']}]),
...
非推奨:
...
TabList = ets:tab2list(Tab),
lists:foldl(fun(X, Acc) -> case X#person.name of
"Bryan" ->
[X#person.age|Acc];
_ ->
Acc
end
end, [], TabList),
...
もっとも非推奨:
...
TabList = ets:tab2list(Tab),
BryanList = lists:filter(fun(X) -> X#person.name == "Bryan" end,
TabList),
lists:map(fun(X) -> X#person.age end, BryanList),
...
もし、Bryanという名前の人に関して、Etsテーブルに保存されているすべての属性が必要になった場合には以下のようにします。
推奨:
...
ets:select(Tab, [{#person{idno='_',
name="Bryan",
age='_',
occupation = '_'}, [], ['$_']}]),
...
非推奨:
...
TabList = ets:tab2list(Tab),
lists:filter(fun(X) -> X#person.name == "Bryan" end, TabList),
...
もし、テーブル上のデータに対して、キーの順番でアクセスするという使い方をするのであれば、通常の set テーブル型の代わりに、 ordered_set テーブル型を使用することができます。 ordered_set はキーフィールドに関連づけられたErlangの項の順番でトラバースされます。そのため、 select, match_object, foldl といった関数の返値はkeyの値の順序で返されるようになります。 ordered_set に対する first, next といった操作をしても、キーの順序で値が返されます。
Note
ordered_set はオブジェクトがキーの順番に処理されるということしか保証していません。 ets:select/2 のような関数の返値は、もしキーが結果に含まれていないとしても(訳注:意味?)、キーの順序に現れることになります。
Etsテーブルは単一キーテーブルです。内部実装はハッシュテーブルもしくは、キーの順序で格納されるツリーの2種類あり、どちらかを使用することができます。言い換えると、可能な場合はいつでも、キーを利用して値を検索する、ということです。登録済みのキーを利用して検索する場合、 set を利用したEtsテーブルの場合は定数時間で、 ordered_set を利用したEtsテーブルの場合はO(logN)で検索することができます。キーによる検索は、テーブル全体を検索するよりも望ましいと言えます。上記の例で言うと、 idno フィールドはテーブルのキーになっているため、名前だけを指定した検索の場合にはテーブル全体を完全にスキャンしてマッチする結果を見つけ出します。
簡単な解決策としては名前(name)のフィールドを idno の代わりにキーにするというものがありますが、もし名前がユニークでない場合に問題になります。より汎用性の高い解決策としては、もう一つテーブルを作成し、 name をキーに、 idno をデータに格納することです。こうすると、名前欄に対して、インデックスを作成するようなことが可能になります。2番目のテーブルは当然、マスターのテーブルと不一致があってはいけません。Mnesiaを使うことでこのようなことができますが、自家製のインデックステーブルを使うと、Mnesiaを使用するオーバーヘッドに比べて、かなり効率的に処理できるはずです。
前のサンプルのテーブルに対するインデックステーブルは bag である必要があります。 bag を使うと、キーの値は1つ以上格納することができるようになります。以下のような構成になります:
[#index_entry{name="Adam", idno=1},
#index_entry{name="Bryan", idno=2},
#index_entry{name="Bryan", idno=3},
#index_entry{name="Carl", idno=4}]
このインデックステーブルを利用して、”Bryan”という名前の全ての人の年齢のフィールドを探索する場合には、以下のようにします:
...
MatchingIDs = ets:lookup(IndexTable,"Bryan"),
lists:map(fun(#index_entry{idno = ID}) ->
[#person{age = Age}] = ets:lookup(PersonTable, ID),
Age
end,
MatchingIDs),
...
注意すべきポイントとしては、上記のコードが、 ets:match/2 を使用しないで、代わりに ets:lookup/2 を呼んでいる点です。 lists:map/2 の呼び出しは、 name が”Bryan”にマッチした idno に対してのみ行われます。そのため、マスターテーブルの検索の回数は最低限の回数に抑えられます。
インデックステーブルを維持するには、テーブルにレコードを挿入するたびにある程度のオーバーヘッドが生じます。そのため、テーブルに挿入操作をする回数よりも、テーブルから検索する操作の回数の方が多くなければ、コストに見合わないということになります。しかし、要素の検索にキーが使用できる場合には効果はかなり大きいということは覚えておくといいでしょう。
もしも、テーブルのキーになっている以外のフィールドを検索する機会が多い場合には、 mnesia:select/match_object を使用すると、テーブル全体を探索するため、パフォーマンス上の劣化が大きくなります。このような場合には、セカンダリーインデックスを作成し、 mnesia:index_read を使用して、高速なアクセスをすべきです。しかし、これを行うと、メモリの必要量は増加します。以下にサンプルを示します:
-record(person, {idno, name, age, occupation}).
...
{atomic, ok} =
mnesia:create_table(person, [{index,[#person.age]},
{attributes,
record_info(fields, person)}]),
{atomic, ok} = mnesia:add_table_index(person, age),
...
PersonsAge42 = mnesia:dirty_index_read(person, 42, #person.age),
...
トランザクションは、多くの異なるプロセスが、平行で内容を更新するような、分散Mnesiaデータベースの一致性を保証するために使用します。しかし、リアルタイムのリクエストが必要になった場合には、トランザクションを使用する代わりに、「汚い」操作を行う必要があるでしょう。汚い操作を行うことで、一致性の保証は失われます。これを解決するには、通常、一つのプロセスだけがテーブルをアップデートするようにすることで解決します。他のプロセスはこのプロセスに対して、アップデート要求を送信するという実装になります:
...
% トランザクションを使用する
Fun = fun() ->
[mnesia:read({Table, Key}),
mnesia:read({Table2, Key2})]
end,
{atomic, [Result1, Result2]} = mnesia:transaction(Fun),
...
% 汚い操作を行う
...
Result1 = mnesia:dirty_read({Table, Key}),
Result2 = mnesia:dirty_read({Table2, Key2}),
...
Copyright c 1991-2009 Ericsson AB