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

  • 96
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

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