Elixir
Phoenix
ecto

Elixir Phoenixのデータベース操作モジュールEcto入門

More than 3 years have passed since last update.

最近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.jsonphoenix.gen.htmlを使うことで、JSON APIやWebでのCRUD操作を一気に生成することも可能。


データ型

ここで、Phoenixのモデル生成で定義可能なデータ型を確認する。PhoenixのTaskのソースコードを追ってみると、


phoenix.model.ex


@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_deleteafter_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 - データベースから取得後