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

最近[Phoenix](http://www.phoenixframework.org/)というElixir製のWAFを勉強している。生産性とスケーラビリティをここまで両立できているのかと感心している。データベース操作には、[Ecto](https://github.com/elixir-lang/ecto)がRailsのActiveRecordに相当するものだが、オブジェクトではないのでORMではない。もっとシンプルな構成をしていて、データベース操作やクエリの組み立てを関数合成のようにできるようになっている。Elixirではマクロを利用することで抽象構文木を操作することができる。例えばElixirの`unless`という構文はマクロで定義されている。つまり、自分でもシンタックスを定義できるということだ。そのおかげでEctoでは、SQLのシンタックスのようなエレガントなAPIを提供することができている。 Ectoはデータベース操作用途に作られたプログラミング言語のような美しさを兼ね備えているのだ。

## Ectoの基本構成
- Ecto.Repo このモジュールを通して、CRUD操作をする
- Ecto.Model モデル定義とストレージ変更におけるライフサイクルの定義。
- Ecto.Query レポジトリからの情報を獲得するためのクエリ。クエリは`Queryable`プロトコルで合成可能。

今回は主にEcto.Modelの解説。

## モデルの生成
PhoenixではRailsのようにコマンドラインからモデルを生成する。

```sh:
$ 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のソースコードを追ってみると、

```elixir: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つのパラメータを指定することができて、

```sh:
$ mix phoenix.gen.model User users emails:array:string
```

```sh:
$ mix phoenix.gen.model User users class_id:references:classes
```

みたいになる。

**2015/8/6編集 v0.16.0で同じ機能が２つあるのは混乱を招くので`belongs_to`は廃止されました。**

## マイグレーション
Ectoにも、Railsのようなmigration操作が定義できる。 
スキーマの変更定義をする場合、以下のようにしてスキーマの変更定義ファイルを生成する。

```sh:
$ mix ecto.gen.migration add_column
```

また、スキーマの変更を適用するには、

```sh:
$ mix ecto.migration
```
でデータベースのスキーマを更新する。

```sh:
$ mix ecto.rollback
```
で一つ前にロールバックすることができる。

## 関連

### 1対1

```elixir:
has_one :permalink, permalink
```
デフォルトを設定可能。

```elixir:
has_one :post, Post, defaults: [title: "default"]
```

### 1対多
関係はschemaの中で以下のように定義します。`on_delete:`は省略することができて、デフォルトでは`:nothing`。

```elixir
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`で階層的に関連付を行える。

```elixir
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.

と[言っている](https://github.com/elixir-lang/ecto/issues/183)のでそのうちサポートされるだろう。
代わりに現状では以下のようにする。

```elixir:
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ではモデルの変更があったとき、変更前・変更後のコールバックを指定することができる。
公式の[ドキュメント](http://www.phoenixframework.org/docs/ecto-models)には以下のような例が載っている。

```elixir:
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` - データベースから取得後