最近PhoenixというElixir製のWAFを勉強している。生産性とスケーラビリティをここまで両立できているのかと感心している。データベース操作には、EctoがRailsのActiveRecordに相当するものだが、オブジェクトではないのでORMではない。もっとシンプルな構成をしていて、データベース操作やクエリの組み立てを関数合成のようにできるようになっている。Elixirではマクロを利用することで抽象構文木を操作することができる。例えばElixirのunless
という構文はマクロで定義されている。つまり、自分でもシンタックスを定義できるということだ。そのおかげでEctoでは、SQLのシンタックスのようなエレガントなAPIを提供することができている。 Ectoはデータベース操作用途に作られたプログラミング言語のような美しさを兼ね備えているのだ。
Ectoの基本構成
- Ecto.Repo このモジュールを通して、CRUD操作をする
- Ecto.Model モデル定義とストレージ変更におけるライフサイクルの定義。
- Ecto.Query レポジトリからの情報を獲得するためのクエリ。クエリは
Queryable
プロトコルで合成可能。
今回は主にEcto.Modelの解説。
モデルの生成
PhoenixではRailsのようにコマンドラインからモデルを生成する。
$ mix phoenix.gen.model User users username:string first_name:string last_name:string
のようにしてレコードとカラムを定義することが出来る。
ちなみにPhoenixではphoenix.gen.json
やphoenix.gen.html
を使うことで、JSON APIやWebでのCRUD操作を一気に生成することも可能。
データ型
ここで、Phoenixのモデル生成で定義可能なデータ型を確認する。PhoenixのTaskのソースコードを追ってみると、
@doc """
Generates some sample params based on the parsed attributes.
"""
def params(attrs) do
Enum.into attrs, %{}, fn
{k, {:array, _}} -> {k, []}
{k, :belongs_to} -> {k, nil}
{k, :integer} -> {k, 42}
{k, :float} -> {k, "120.5"}
{k, :decimal} -> {k, "120.5"}
{k, :boolean} -> {k, true}
{k, :text} -> {k, "some content"}
{k, :date} -> {k, %{year: 2010, month: 4, day: 17}}
{k, :time} -> {k, %{hour: 14, min: 0}}
{k, :datetime} -> {k, %{year: 2010, month: 4, day: 17, hour: 14, min: 0}}
{k, :uuid} -> {k, "7488a646-e31f-11e4-aace-600308960662"}
{k, _} -> {k, "some content"}
end
end
と定義されている箇所があった。テストコードでの@valid_attrs
のパラメータを生成するために使っている。
このように、Elixirではパターンマッチを使うことでシンプルな記述になる。
primitiveタイプを含めると以下のコマンドでデータ型を生成することができる。
type | 概要 |
---|---|
:array | 配列 |
:map | 辞書 |
:boolean | 真偽値 |
:integer | 整数 |
:float | 浮動小数点 |
:decimal | 精度の高い浮動小数点 |
:string | 文字列 |
:text | 文字列 |
:date | 日付 |
:datetime | 日時 |
:uuid | UUID |
:references | 参照 |
ここまで書いて先ほどの関数に:references
がないことに気づいてプルリクしたら無事マージされた。
:references
と:array
は3つのパラメータを指定することができて、
$ mix phoenix.gen.model User users emails:array:string
$ mix phoenix.gen.model User users class_id:references:classes
みたいになる。
2015/8/6編集 v0.16.0で同じ機能が2つあるのは混乱を招くのでbelongs_to
は廃止されました。
マイグレーション
Ectoにも、Railsのようなmigration操作が定義できる。
スキーマの変更定義をする場合、以下のようにしてスキーマの変更定義ファイルを生成する。
$ mix ecto.gen.migration add_column
また、スキーマの変更を適用するには、
$ mix ecto.migration
でデータベースのスキーマを更新する。
$ mix ecto.rollback
で一つ前にロールバックすることができる。
関連
1対1
has_one :permalink, permalink
デフォルトを設定可能。
has_one :post, Post, defaults: [title: "default"]
1対多
関係はschemaの中で以下のように定義します。on_delete:
は省略することができて、デフォルトでは:nothing
。
has_many :comments, MyApp.Comment, on_delete: :fetch_and_delete
on_delete:
のオプション
-
:nothing
- なにもしない。 -
:fetch_and_delete
- ひとつひとつ削除する。before_delete
、after_delete
コールバックが呼ばれる。 -
:delete_all
- すべて削除する。コールバックは呼ばれない。 -
:nilify_all
-nil
に設定される。コールバックは呼ばれない。
:through
で階層的に関連付を行える。
has_many :posts_comments, through: [:posts, :comments]
多対多
Phoenixでは現状、中間テーブルを自動生成できない。José Valimは
We also want to support more direct many to many which automatically manages the intermediate table.
We want to add many_to_many "tags", Tag or something similar.
と言っているのでそのうちサポートされるだろう。
代わりに現状では以下のようにする。
defmodule MyApp.Comment do
schema "comments" do
field :comment, :string
has_many :comment_posts, MyApp.CommentPost
has_many :posts, through: [:comment_posts, :post]
end
end
defmodule MyApp.CommentPost do
schema "comment_posts" do
belongs_to :comment, MyApp.Comment
belongs_to :post, MyApp.Post
end
end
defmodule MyApp.Post do
schema "posts" do
field :comment, :text
has_many :comment_posts, MyApp.CommentPost
has_many :comments, through: [:comment_posts, :comment]
end
end
Ecto.Modelのライフサイクル
Ectoではモデルの変更があったとき、変更前・変更後のコールバックを指定することができる。
公式のドキュメントには以下のような例が載っている。
defmodule HelloPhoenix.Video do
. . .
before_update :reset_approved_at
def reset_approved_at(changeset) do
changeset
|> Ecto.Changeset.put_change(:approved_at, nil)
end
end
設定可能なコールバック
-
before_delete
- 削除前 -
after_delete
- 削除後 -
before_update
- 更新前 -
after_update
- 更新後 -
before_insert
- 挿入前 -
after_insert
- 挿入後 -
after_load
- データベースから取得後