Phoenixガイドラインを読んでみた(9) - Ecto -

## はじめに

[前回](http://qiita.com/dynamitecoolguy/items/8529bbf96fd6b278ef1a)は、Channelを見てきました。
今回は[Ecto](http://www.phoenixframework.org/docs/ecto-models)、そうDBです。


## 外へ

多くのアプリケーションではデータストレージへのアクセスが必要とされています。ElixirではEctoを使ってデータアクセスを行うことが可能です。Ectoには、現在以下のデータベースへのアダプタが存在します。

+ PostgreSQL
+ MySQL
+ MSSQL
+ SQLite3
+ MongoDB

Phoenixでは、標準で、EctoとPostgreSQLのアダプタがセットされています。

以後、EctoとPostgrex(ElixirのPostgreSQLドライバ)がインストールされて、設定が完了しているとします。
Ectoでは、`phoenix.gen.html`を使ってリソースを生成することができます。ここでは、`User`リソース(フィールドとして`name`, `email`, `bio`, `number_of_pets`)を作ることにします。

```
$ mix phoenix.gen.html User users name:string email:string bio:string number_of_pets:integer
* creating web/controllers/user_controller.ex
* creating web/templates/user/edit.html.eex
* creating web/templates/user/form.html.eex
* creating web/templates/user/index.html.eex
* creating web/templates/user/new.html.eex
* creating web/templates/user/show.html.eex
* creating web/views/user_view.ex
* creating test/controllers/user_controller_test.exs
* creating priv/repo/migrations/20160428030023_create_user.exs
* creating web/models/user.ex
* creating test/models/user_test.exs

Add the resource to your browser scope in web/router.ex:

    resources "/users", UserController

Remember to update your repository by running migrations:

    $ mix ecto.migrate
```

migration, controller, controller test, model, model test, view, 複数のtemplate用のファイルが生成されました。

出力されたコメントに従って、`web/router.ex`に`resources "/users", UserController`を追加します。

同じく、コメントに従ってマイグレーションします(`mix ecto.migrate`)。データベースが見つからないよ的なエラーが出た場合は、`mix ecto.create`でデータベースを作成してくれます。
Mixは、`MIX_ENV=環境名`を指定しない限り開発環境と見なします。Ectoでは、Mixからこの環境を引き継いで、作成されるデータベース名に適当なsuffixを付与します。

データベースサーバに直接ログインてして、マイグレーションされたテーブルを見てみます。

```
$ sudo su - postgres
$ psql 
psql (9.5.2)
Type "help" for help.

postgres=# \connect hello_phoenix_dev
You are now connected to database "hello_phoenix_dev" as user "postgres".
hello_phoenix_dev=# \d
                List of relations
 Schema |       Name        |   Type   |  Owner   
--------+-------------------+----------+----------
 public | schema_migrations | table    | postgres
 public | users             | table    | postgres
 public | users_id_seq      | sequence | postgres
(3 rows)

hello_phoenix_dev=# \d users;
                                       Table "public.users"
     Column     |            Type             |                     Modifiers                      
----------------+-----------------------------+----------------------------------------------------
 id             | integer                     | not null default nextval('users_id_seq'::regclass)
 name           | character varying(255)      | 
 email          | character varying(255)      | 
 bio            | character varying(255)      | 
 number_of_pets | integer                     | 
 inserted_at    | timestamp without time zone | not null
 updated_at     | timestamp without time zone | not null
Indexes:
    "users_pkey" PRIMARY KEY, btree (id)

```

`phoenix.gen.html`で生成せされたマイグレーション用のソースは、`priv/repo/migrations`に展開されます。

```
defmodule HelloPhoenix.Repo.Migrations.CreateUser do
  use Ecto.Migration

  def change do
    create table(:users) do
      add :name, :string
      add :email, :string
      add :bio, :string
      add :number_of_pets, :integer

      timestamps
    end

  end
end
```

`inserted_at`, `updated_at`は、マイグレーションファイルの`timestamps/0`関数で追加されています。また、`id`はマイグレーションはなくても、暗黙的にprimary keyとして追加されます。

### リポジトリ

`HelloPhoenix.Repo`モジュール(lib/hello_phoenix/repo.ex)は、Phoenixアプリケーションにおけるデータベース接続を行う基本モジュールです。

```
defmodule HelloPhoenix.Repo do
  use Ecto.Repo, otp_app: :hello_phoenix
end
```

このモジュールの主な役割は二つです。一つ目は、`Ecto.Repo`からquery用の関数を引用することと、`opt_app`名をモジュールのアプリケーション名にセットすることです。
`phoenix.new`でアプリケーションを生成したとき、同様に設定も作成されます。設定は、`config/dev.exs`にあります。

```
. . .
# Configure your database
config :hello_phoenix, HelloPhoenix.Repo,
  adapter: Ecto.Adapters.Postgres,
  username: "postgres",
  password: "postgres",
  database: "hello_phoenix_dev",
  hostname: "localhost",
```

設定ファイルへの記載は、`opt_app`の名前と、Repoモジュールの指定から始まります。そして、アダプタにPostgresをセットし、ログインに必要な情報が記載されます。もちろん、これらは実際に接続するデータベースの設定に直接変更しても構いません。
同様の設定ファイルが、他の環境用(`config/test.exs`や`config/prod.secret.exs`)にもあります。

### モデル

Ectoのモデルは、いくつかの関数やスキーマのフィールドなどから構成されます。モデルには、他のモデルとの関連や、validation、changesetの取り扱いも定義されています。

先ほど定義されたuserのモデル(`web/models/user.ex`)は、以下のようになります。

```
defmodule HelloPhoenix.User do
  use HelloPhoenix.Web, :model

  schema "users" do
    field :name, :string
    field :email, :string
    field :bio, :string
    field :number_of_pets, :integer

    timestamps
  end

  @required_fields ~w(name email bio number_of_pets)
  @optional_fields ~w()

  @doc """
  Creates a changeset based on the `model` and `params`.

  If no params are provided, an invalid changeset is returned
  with no validation performed.
  """
  def changeset(model, params \\ :empty) do
    model
    |> cast(params, @required_fields, @optional_fields)
  end
end
```

### ChangesetとValidation

Changesetは、アプリケーションで使用できるようにデータ加工を行うパイプラインです。この加工には、型キャスト、validataion、外部パラメータによる何らかのフィルタなどが含まれます。

デフォルトで定義されるchangesetは以下の通りです。

```
  def changeset(model, params \\ :empty) do
    model
    |> cast(params, @required_fields, @optional_fields)
  end
```

`cast/4`関数は、データを`@required_fields`と`@optional_fields`に振り分けます。デフォルトでは、全フィールドがrequiredです。
現状の動作を少し確認してみます。

```
$ iex -S mix phoenix.server
```
>HelloPhoenix.UserをUserに

```
iex(1)> alias HelloPhoenix.User
```

>全て空データ。errorあり。valid?がfalseになる

```
iex(2)> changeset = User.changeset(%User{}, %{})
%Ecto.Changeset{action: nil, changes: %{}, constraints: [],
 errors: [name: "can't be blank", email: "can't be blank", bio: "can't be blank", number_of_pets: "can't be blank"],
 filters: %{},
 model: %HelloPhoenix.User{__meta__: #Ecto.Schema.Metadata<:built>, bio: nil, email: nil, id: nil, inserted_at: nil, name: nil, number_of_pets: nil, updated_at: nil},
 optional: [], opts: [],
 params: %{}, prepare: [], repo: nil, 
 required: [:name, :email, :bio, :number_of_pets],
 types: %{bio: :string, email: :string, id: :id, inserted_at: Ecto.DateTime, name: :string, number_of_pets: :integer, updated_at: Ecto.DateTime},
 valid?: false, validations: []}
```

>`required_fields`(number_of_pets)が空データ。errorあり。valid?がfalseになる

```
iex(3)> changeset2 = User.changeset(%User{}, %{name: "名前", email: "hogehoge@hogehoge", bio: "RXバイオライダー"})
%Ecto.Changeset{action: nil,
 changes: %{bio: "RXバイオライダー", email: "hogehoge@hogehoge", name: "名前"}, constraints: [],
 errors: [number_of_pets: "can't be blank"],
 filters: %{},
 model: %HelloPhoenix.User{__meta__: #Ecto.Schema.Metadata<:built>, bio: nil, email: nil, id: nil, inserted_at: nil, name: nil, number_of_pets: nil, updated_at: nil},
 optional: [], opts: [],
 params: %{"bio" => "RXバイオライダー", "email" => "hogehoge@hogehoge", "name" => "名前"}, prepare: [], repo: nil,
 required: [:name, :email, :bio, :number_of_pets],
 types: %{bio: :string, email: :string, id: :id, inserted_at: Ecto.DateTime, name: :string, number_of_pets: :integer, updated_at: Ecto.DateTime},
 valid?: false, validations: []}
```

>全データあり。errorなし。valid?がtrueになる

```
iex(4)> changeset3 = User.changeset(%User{}, %{name: "名前", email: "hogehoge@hogehoge", bio: "RXバイオライダー", number_of_pets: 3})
%Ecto.Changeset{action: nil,
 changes: %{bio: "RXバイオライダー", email: "hogehoge@hogehoge", name: "名前", number_of_pets: 3}, constraints: [],
 errors: [], filters: %{},
 model: %HelloPhoenix.User{__meta__: #Ecto.Schema.Metadata<:built>, bio: nil, email: nil, id: nil, inserted_at: nil, name: nil, number_of_pets: nil, updated_at: nil},
 optional: [], opts: [],
 params: %{"bio" => "RXバイオライダー", "email" => "hogehoge@hogehoge", "name" => "名前", "number_of_pets" => 3}, prepare: [], repo: nil,
 required: [:name, :email, :bio, :number_of_pets],
 types: %{bio: :string, email: :string, id: :id, inserted_at: Ecto.DateTime, name: :string, number_of_pets: :integer, updated_at: Ecto.DateTime},
 valid?: true, validations: []}
```

>全データと定義外のパラメータあり。errorなし。valid?がtrueになる

```
iex(5)> changeset4 = User.changeset(%User{}, %{name: "名前", email: "hogehoge@hogehoge", bio: "RXバイオライダー", number_of_pets: 3, extra: "エクストラ"})
%Ecto.Changeset{action: nil,
 changes: %{bio: "RXバイオライダー", email: "hogehoge@hogehoge", name: "名前", number_of_pets: 3}, constraints: [],
 errors: [], filters: %{},
 model: %HelloPhoenix.User{__meta__: #Ecto.Schema.Metadata<:built>, bio: nil, email: nil, id: nil, inserted_at: nil, name: nil, number_of_pets: nil, updated_at: nil},
 optional: [], opts: [],
 params: %{"bio" => "RXバイオライダー", "email" => "hogehoge@hogehoge", "extra" => "エクストラ", "name" => "名前", "number_of_pets" => 3}, prepare: [], repo: nil,
 required: [:name, :email, :bio, :number_of_pets],
 types: %{bio: :string, email: :string, id: :id, inserted_at: Ecto.DateTime, name: :string, number_of_pets: :integer, updated_at: Ecto.DateTime},
 valid?: true, validations: []}
```

>changeset4からの一部変更。errorなし。valid?がtrueになる

```
iex(6)> changeset5 = User.changeset(changeset4, %{name: "あたらしい名前", number_of_pets: 1, extra: "hoge", extra2: "fuga"}) 
%Ecto.Changeset{action: nil,
 changes: %{bio: "RXバイオライダー", email: "hogehoge@hogehoge", name: "あたらしい名前", number_of_pets: 1}, constraints: [],
 errors: [], filters: %{},
 model: %HelloPhoenix.User{__meta__: #Ecto.Schema.Metadata<:built>, bio: nil, email: nil, id: nil, inserted_at: nil, name: nil, number_of_pets: nil, updated_at: nil},
 optional: [], opts: [],
 params: %{"bio" => "RXバイオライダー", "email" => "hogehoge@hogehoge", "extra" => "hoge", "extra2" => "fuga", "name" => "あたらしい名前", "number_of_pets" => 1}, prepare: [], repo: nil,
 required: [:name, :email, :bio, :number_of_pets],
 types: %{bio: :string, email: :string, id: :id, inserted_at: Ecto.DateTime, name: :string, number_of_pets: :integer, updated_at: Ecto.DateTime},
 valid?: true, validations: []}
```

>それぞれのchangeset (未定義のパラメータextra, extra2はchangesetから除去されている)

```
iex(7)> changeset.changes
%{}
iex(8)> changeset2.changes
%{bio: "RXバイオライダー", email: "hogehoge@hogehoge", name: "名前"}
iex(9)> changeset3.changes
%{bio: "RXバイオライダー", email: "hogehoge@hogehoge", name: "名前", number_of_pets: 3}
iex(10)> changeset4.changes
%{bio: "RXバイオライダー", email: "hogehoge@hogehoge", name: "名前", number_of_pets: 3}
iex(11)> changeset5.changes
%{bio: "RXバイオライダー", email: "hogehoge@hogehoge", name: "あたらしい名前", number_of_pets: 1}
```

デフォルトのパイプラインに、別のvalidationを追加することもできます。
例えば、`bio`フィールドが最少2文字、最大140文字必要であり、メールアドレスに"@"が含まれていることをvalidateするためには、以下のようにパイプラインを記述できます。

```
  def changeset(model, params \\ :empty) do
    model
    |> cast(params, @required_fields, @optional_fields)
    |> validate_length(:bio, min: 2)
    |> validate_length(:bio, max: 140)
    |> validate_format(:email, ~r/@/)
  end
```

### Controller

ControllerでどのようにEctoを使用するかを見るために、先ほど`mix phoenix.gen.html`で作成された`HelloPhoenix.UserController`を見てみます。

```
defmodule HelloPhoenix.UserController do
. . .
  alias HelloPhoenix.User

  plug :scrub_params, "user" when action in [:create, :update]
. . .
end
```

前にも出ていますが、`alias HelloPhoenix.User`は、`%HelloPhoenix.User{}`と書く代わりに`%User{}`と書けるようにするための定義です。

`plug :scrub_params, "user" when action in [:create, :update]`にある`scrub_params/2`は、指定されたたパラメータが存在することを保証し、空文字列をnilに変換するPlugです。
なお、`when action in [:create, :update]`はPipelineで使用できるGuardです。説明は[こちら](https://hexdocs.pm/phoenix/Phoenix.Controller.Pipeline.html#summary)にありますが、`conn`、`action`(atom)、`controller`(alias *(UserController? 要確認)* )を使用することができます。

**`index`アクション**

```
  def index(conn, _params) do
    users = Repo.all(User)
    render(conn, "index.html", users: users)
  end
```

これは、DBから全ユーザを取得して、Template`index.html.eex`に渡す処理を行います。
全ユーザ取得は、`Repo.all/1`にモデル名(alias)を与えることで行われます。

なお、ここではChangesetは使いません。Changesetは、DBへのデータ挿入を保証するためのものであり、DBから取得したデータは正しいはずだからです。

**`new`アクション**

```
def new(conn, _params) do
  changeset = User.changeset(%User{})
  render(conn, "new.html", changeset: changeset)
end
```

このアクションではChangesetを使用します。基本的に、`new`アクションでは、パラメータを使わずに空のChangesetを使ってTemplate "new.html.eex"を表示します。
これは、この後の`create`アクションで、必須項目が入力されていないなどのエラーが発生したときも、"new.html.eex"を表示するからです(このときは、Changesetにユーザに入力した値がセットされています)。

**`create`アクション**

"new.html"でformがsubmitされると、`create`アクションが呼び出されます。

```
def create(conn, %{"user" => user_params}) do
  changeset = User.changeset(%User{}, user_params)

  case Repo.insert(changeset) do
    {:ok, _user} ->
      conn
      |> put_flash(:info, "User created successfully.")
      |> redirect(to: user_path(conn, :index))
    {:error, changeset} ->
      render(conn, "new.html", changeset: changeset)
  end
end
```

パターンマッチングによって"user"キーの値を使って、changesetを作成します。DBへのinsertが成功すれば、Flashメッセージをセットして`index`アクションにリダイレクトされます。失敗すれば、もう一度"new.html"が表示されます。

**`show`アクション**

```
def show(conn, %{"id" => id}) do
  user = Repo.get!(User, id)
  render(conn, "show.html", user: user)
end
```

`Repo.get!`でidに対応したユーザを取得し、"show.html.eex"Templateを表示します。`index`アクションと同様に、ここではChangesetは使用しません。

**`edit`アクション**

```
  def edit(conn, %{"id" => id}) do
    user = Repo.get!(User, id)
    changeset = User.changeset(user)
    render(conn, "edit.html", user: user, changeset: changeset)
  end
```

`edit`は、`show`と`new`を足したような処理です。`Repo.get!`でデータを取得し、そのデータを編集するためTemplate"edit.html.eex"を表示します。

"edit.html"では、パラメータの表示に`changeset`、ルーティング用のパスを構築するために`user`を使用しています。
"edit.html"は、次の`update`アクションで入力エラーが発生したときにも使用されます。このとき、`changeset`には、ユーザが入力したパラメータが入っているので、ユーザが入力した値がそのまま表示されることになります(`user`を使用してしまうと、DBに入っている値が参照されてしまう)。

**`update`アクション**

```
  def update(conn, %{"id" => id, "user" => user_params}) do
    user = Repo.get!(User, id)
    changeset = User.changeset(user, user_params)

    case Repo.update(changeset) do
      {:ok, user} ->
        conn
        |> put_flash(:info, "User updated successfully.")
        |> redirect(to: user_path(conn, :show, user))
      {:error, changeset} ->
        render(conn, "edit.html", user: user, changeset: changeset)
    end
  end
```

`update`アクションは`create`アクションとほぼ同じですが、`Repo.insert/1`の代わりに`Repo.update/1`を使用しています。
ちなみに、`Repo.update/1`では、値に変更が無い場合にはDBにデータを送らないといった処理を行っています。

**`delete`アクション**

```
  def delete(conn, %{"id" => id}) do
    user = Repo.get!(User, id)

    # Here we use delete! (with a bang) because we expect
    # it to always work (and if it does not, it will raise).
    Repo.delete!(user)

    conn
    |> put_flash(:info, "User deleted successfully.")
    |> redirect(to: user_path(conn, :index))
  end
```

最初に`Repo.get!`はDB上のレコードidを取得するために行っています。
その後`Repo.delete!`によりレコードを削除します。なお、レコードidが無くてもエラーにならないように"!"付きのバージョンを使用しています。
削除後、Flashメッセージをセットし、`index`アクションにリダイレクトします。

### Data Relationship And Dependencies

ここで、単純な動画投稿サイトを作る例を考えてみます。上記のユーザ管理に追加して、投稿動画の管理も行います。
動画(`Video`)モデルは、以下のように作成するものとします。

```
$ mix phoenix.gen.model Video videos name:string approved_at:datetime description:text likes:integer views:integer user_id:references:users
* creating priv/repo/migrations/20160513062728_create_video.exs
* creating web/models/video.ex
* creating test/models/video_test.exs

$ mix ecto.migrate
```

なお、Migration用のスクリプト(`priv/depo/migrations/....._create_video.exs`)は、以下のように生成されています。

```
defmodule HelloPhoenix.Repo.Migrations.CreateVideo do
  use Ecto.Migration

  def change do
    create table(:videos) do
      add :name, :string
      add :approved_at, :datetime
      add :description, :text
      add :likes, :integer
      add :views, :integer
      add :user_id, references(:users, on_delete: :nothing)

      timestamps
    end
    create index(:videos, [:user_id])

  end
end
```

Relationshipを定義するために、たとえば、以下のようなAPIが用意されています。

**`Schema.has_many/3`**

親対子の関連が、1対多であることを表します。今回の例だと、ユーザ→→動画の関連です。

**`Schema.belongs_to/3`**

子対親の関連が、1対1であることを表します。今回の例だと、動画→ユーザの関連です。

**`Schema.has_one/3`**

親対子の関連が、1対1であることを表します。

この他に、`embeds_many`、`embeds_one`があります (それぞれのモデルが独立して存在するのではなく、完全に親子関係になっていることを表す)。
なお、多対多の関連の場合には、関連モデルを作る(A ← →→ C ←← → B)ようです。

Userモデル(`wbeb/models/user.ex`)に　`has_many`関連を追加します。

```
  schema "users" do
    field :name, :string
    field :email, :string
    field :bio, :string
    field :number_of_pets, :integer

    has_many :videos, HelloPhoenix.Video
    timestamps
  end
```

Videoモデルは、生成時に`user_id:references:users`を指定していますので、自動的に`belongs_to`がセットされています。
また、`user_id`をfieldに追加する必要はありませんが、@required_fieldsにuser_idは追加しておきます。

```
defmodule HelloPhoenix.Video do
  use HelloPhoenix.Web, :model

  schema "videos" do
    field :name, :string
    field :approved_at, Ecto.DateTime
    field :description, :string
    field :likes, :integer
    field :views, :integer
    belongs_to :user, HelloPhoenix.User

    timestamps
  end

  @required_fields ~w(name approved_at description likes views user_id)
```

`web/controllers/user_controller.ex`も、この関連を反映するために修正を行います。

```
defmodule HelloPhoenix.UserController do
  ...
  def index(conn, _params) do
    users = User |> Repo.all |> Repo.preload [:videos]
    render(conn, "index.html", users: users)
  end

  def show(conn, %{"id" => id}) do
    user = User |> Repo.get!(id) |> Repo.preload [:videos]
    render(conn, "show.html", user: user)
  end
  ...
end
```

`Repo.preload/2`によって、いい感じでVideoモデルをロードしてくれます。