Mnesia

Mnesiaは、耐久性のあるリアルタイム分散データベース管理システムです。

概要

Mnesiaは、Elixirで自然に使えるErlang Runtime Systemに同梱されているデータベース管理システム(DBMS)です。 Mnesiaのリレーショナルとオブジェクトのハイブリッドデータモデルは、どんな規模の分散アプリケーションの開発にも適しているものです。

使用するタイミング

特定の技術をいつ使うかは、しばしば迷うところです。 以下の質問のいずれかに「はい」と答えられるなら、ETSやDETSよりもMnesiaを使用する良い指標となります。

スキーマ

MnesiaはElixirではなくErlangコアの一部なので、コロン構文でアクセスしなければなりません(レッスン: 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 は新しい空のスキーマを初期化し、ノードリストを渡します。 この場合、IExセッションに関連付けられたノードを渡します。

ノード

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 オプションを渡すと、可能なすべてのオプションが表示されます。 name--sname オプションがあり、ノードに情報を割り当てることができることがわかります。 ノードとは起動しているErlang仮想マシンのことで、それ自身の通信やガベージコレクション、処理のスケジューリング、メモリなどを扱います。 ノードはデフォルトで [email protected] という名前になっています。

$ iex --name [email protected]

Erlang/OTP {{ site.erlang.OTP }} [erts-{{ site.erlang.erts }}] [source] [64-bit] [smp:4:4] [async-threads:10] [hipe] [kernel-poll:false] [dtrace]

Interactive Elixir ({{ site.elixir.version }}) - 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のノードは、他のノードに接続し、情報やリソースを共有(配布)するために使われます。 これは同じマシンに限定する必要はなく、LANやインターネットなどを通じて通信できます。

Mnesiaを開始する

さて、背景の基本が終わり、データベースをセットアップしたら、今度は Mnesia.start/0 コマンドでMnesia DBMSを起動する段階に入りました。

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

関数 Mnesia.start/0 は非同期です。 これは既存のテーブルの初期化を開始し、:okアトムを返します。 Mnesiaを起動した直後に既存のテーブルに対して何らかのアクションを実行する必要がある場合、 Mnesia.wait_for_tables/2 という関数を呼び出す必要があります。 これは、テーブルが初期化されるまで呼び出し元を一時停止します。 データの初期化とマイグレーションのセクションの例を参照してください。

2つ以上のノードが参加する分散システムを実行する場合、 Mnesia.start/1 関数を参加している全てのノードで実行する必要があることに留意するとよいでしょう。

テーブルを作成する

データベース内にテーブルを作成するには、関数 Mnesia.create_table/2 を使用します。 以下では、Person という名前のテーブルを作成し、テーブルのスキーマを定義するキーワードリストを渡します。

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

アトムを使用して :id:name:job カラムを定義します。 最初のアトム (この場合は :id) がプライマリキーとなります。 プライマリキーに加えて、少なくとも1つの属性が必要です。

Mnesia.create_table/2 を実行すると、以下のいずれかのレスポンスが返されます。

とくに、テーブルがすでに存在している場合、理由は {: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は空のリストを返します。

トランザクション

伝統的に、私たちはトランザクションを使用して、データベースへの読み書きをカプセル化しています。 トランザクションは、耐障害性の高い分散システムを設計する上で重要な役割を果たします。 Mnesiaのトランザクションは、一連のデータベース操作を1つの機能ブロックとして実行することができるメカニズムです。 まず、無名関数、この場合は 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}

このトランザクションメッセージに基づけば、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 を呼び出すだけです。 したがって、ハンスのレコードを更新するには、次のようにします。

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 が返すものと似ています。

とくに、インデックスがすでに存在している場合、理由は {:already_exists, table, attribute_index} という形式になり、このインデックスを再度追加しようとすると、次のような結果になります。

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

インデックスが正常に作成されたら、それに対して読み取りを行い、すべてのプリンシパルのリストを取得できます。

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

マッチとセレクト

Mnesiaはテーブルからデータを取得するための複雑なクエリを、マッチングやアドホックなセレクト関数という形でサポートしています。

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 関数を使うと、Elixir言語(あるいはErlang)の任意の演算子や関数を使ったカスタムクエリを指定できます。 例として、キーが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 で、2番目の属性は {match, [guard], [result]} という形式の3つの値です。

詳しくはErlang Mnesia ドキュメントのselect/2を参照してください。

データの初期化とマイグレーション

どのソフトウェアソリューションでも、ソフトウェアのアップグレードやデータベースに保存されているデータのマイグレーションが必要になる時期がやってきます。 たとえば、アプリのv2で Person テーブルに :age カラムを追加したいとします。 一度作成された Person テーブルを作成することはできませんが、変換することは可能です。 そのためには、いつ変換するのかを知っておく必要があります。これは、テーブルを作成する際に行うことができます。 そのためには、 Mnesia.table_info/2 関数で現在のテーブルの構造を取得し、 Mnesia.transform_table/3 関数で新しい構造へ変換します。

以下のコードでは、次のロジックを実装することでこれを実現しています。

Mnesia.start/0 でMnesiaを起動した直後に既存のテーブルに対して何らかのアクションを実行すると、それらのテーブルが初期化されておらず、アクセスできない可能性があります。 その場合、Mnesia.wait_for_tables/2 関数を使用する必要があります。 これは、テーブルが初期化されるか、タイムアウトに達するまで、現在のプロセスを一時停止させます。

Mnesia.transform_table/3 関数は、テーブルの名前、レコードを古いフォーマットから新しいフォーマットに変換する関数、新しい属性のリストを属性として受け取ります。

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.wait_for_tables([Person], 5000)
        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
Caught a mistake or want to contribute to the lesson? Edit this lesson on GitHub!