Elixir Ecto のまとめ

**(追記)本記事は2022/09/25 に更新しました**

　Phoenixをやろうとして、いろんなドキュメントを読んでいるのですが、Ecto関連が初心者の私には複雑かなと思えたので、まじめにGetting Startedから始めてみました。EctoはElixirのdatabase wrapperでquery generatorです。異なるDB（PostgreSQLやMySQLなど）のインターフェースを標準化してくれます。デフォルトはPostgreSQLです。
https://hexdocs.pm/ecto/getting-started.html

　Ectoは大きく4つの構成要素（**Ecto.Repo、Ecto.Schema、Ecto.Changeset、Ecto.Query**）から成ります。 この枠組みを押さえておかないと、いろいろなドキュメントを読む時に、Ectoの説明がまとまりがないと感じてしまうことがあります。ここでは以下に順を追って説明していきます。
https://hexdocs.pm/ecto/Ecto.html


　また今回の実験の環境は下記の記事にある手順で構築しました。
[CentOS7にErlangとElixir、Phoenixをインストールしてみる - Qiita](https://qiita.com/sand/items/e98c9e98351b816c7e69)


(2018/04/30)追記 - 本記事の応用記事を書きました。
[Elmとleafletでつくるgeolocationの発信機・受信機 - Qiita](https://qiita.com/sand/items/18e3a4af0aca1c6376b8)
[Elmとleafletでつくるgeolocationの再現機 - Qiita](https://qiita.com/sand/items/13de00918515b6dba844)



# １．Ectoのインストール

　まずmixでfriendsというプロジェクトを作成します。オプションの --sup はこのアプリケーションがsupervision treeを持っていることを示しています。

```sh:
mix new friends --sup
```


　次に必要なパッケージをインストールします。以下のファイルのdepsを変更します。postgrexはPostgreSQLのドライバです。

```elixir:mix.exs
defp deps do
      {:ecto_sql, "~> 3.2"},
      {:postgrex, "~> 0.15"}
end
```

　インストールします。

```sh:
mix deps.get
```

　これでEctoは以下の経路でPostgreSQLにアクセスできるようになります。

```
Ecto --> Ecto.Adapters.Postgres --> postgrex --> PostgreSQL.
```

# ２．Ecto.Repo
　Repo(Repositories)はdata storeのラッパーで、データストアに対するAPIを提供します。データベースにアクセスするときはもちろん、開発の初期段階でモックデータにアクセスするときもこのAPIを利用します。EctoではRepository　を通して create, update, destroy や query を発行することができます。 ですからRepository は、データベースとコミュニケーションするための adapter や credentialsを知る必要があります。それらの設定方法を以下に示します。

　まず以下のコマンド一発でconfig/config.exsに必要な項目を追加し、且つlib/friends/repo.exファイルを作成します。

```sh:
mix ecto.gen.repo -r Friends.Repo
```

　まずconfig/config.exsをご確認ください。以下のような項目が追加されているはずです。私は更に自分の環境に合わせてusername/passwordを手で書き換えました。これはPostgreSQLに接続するために必要な情報を含んでいます。

```elixir:config/config.exs
config :friends, Friends.Repo,
  database: "friends_repo",
  username: "postgres",
  password: "postgres",
  hostname: "localhost"
```

　同時にlib/friends/repo.exというファイルも作成されているので確認してください。Friends.Repoはデータベースへのqueryに使われるます。ここではEcto.Repoを使うことが宣言されています。otp_appはどのアプリがデータベースへアクセスしているかを教えています。:friendsとなっているのでこのプロジェクトのconfig.exsを参照します。その接続情報をもとにデータベースにアクセスします。

```elixir:lib/friends/repo.ex(Ectoバージョン)
defmodule Friends.Repo do
  use Ecto.Repo,
    otp_app: :friends,
    adapter: Ecto.Adapters.Postgres
end
```

　ちなみに開発の初期段階でデータベース(Ecto)を使わずに、モックデータを使う場合はここの定義を以下のように変えます。つまりここでFriends.Repo.all や Friends.Repo.get、Friends.Repo.get_byなどのAPIを実装します。こうしておけば本番のデータベース利用時に他の個所の変更はなくスムーズに移行できます。（厳密にいえばEctoを使わない場合は、以下のlib/friends/application.exの編集は必要ありません。）

```elixir:lib/friends/repo.ex(モックバージョン)
defmodule Friends.Repo do
  def all(Friends.Person ) do
   [%Friends.Person{id: "1", first_name: "José", last_name: "Valim", age: 18},
    %Friends.Person{id: "2", first_name: "Bruce", last_name: "Rapids", age: 20},
    %Friends.Person{id: "3", first_name: "Chris", last_name: "Mccord", age: 19}]
  end
  def all(_module), do: []

  def get(module, id) do
   Enum.find all(module), fn map -> map.id == id end
  end
  def get_by(module, params) do
   Enum.find all(module), fn map -> 
   Enum.all?(params, fn {key, val} -> Map.get(map, key) == val end)
  end
end
```


　次にlib/friends/application.exを編集します。Friends.Repo をapplication の supervision treeの中のsupervisor としてセットします。これによって、Ecto process を起動して、アプリのqueryを受け取り実行できるようになります。

```elixir:lib/friends/application.ex
def start(_type, _args) do
  import Supervisor.Spec

  children = [
    Friends.Repo,
  ]
```

　最後にconfig/config.exsを編集して、以下の一行を追加します。これでアプリケーションにrepoについて教えます。

```elixir:config/config.exs
config :friends, ecto_repos: [Friends.Repo]
```


# ３．DBとテーブルの作成

　以上で準備は終わっているのでDBを作成します。以下のコマンドを打ちます。

```sh:
mix ecto.create
```

　次にテーブルを作成するための準備をします。まず以下のmixコマンドを打ちます。

```sh:
 mix ecto.gen.migration create_friends
```

　そうすると以下のようにファイルを作成した旨のメッセージが表示されます。

```sh:
[root@www13134uf friends]# mix ecto.gen.migration create_friends
* creating priv/repo/migrations
* creating priv/repo/migrations/20220925095912_create_friends.exs
```

　ファイル 20220925095912_create_friends.exs を編集して以下のようにします。

```elixir:priv/repo/migrations/20220925095912_create_friends.exs
defmodule Friends.Repo.Migrations.CreateFriends do
  use Ecto.Migration

  def change do
    create table(:friends) do
      add :first_name, :string
      add :last_name, :string
      add :age, :integer
    end
  end
end
```

　最後に上のmigrationファイルを走らせて、テーブルを作成します。

```sh:
mix ecto.migrate
```

　もしこの時 migration に失敗したら、**mix ecto.rollback** でこのchangeをundoできます。



# ４．Ecto.Schema
　**Schema はDB テーブルを Elixir struct に map するために使われます。** 以下のSchemaで **friendsテーブル** を **Friends.Person** struct に map します。

　Friends.Person schemaは以下のような構文で作られ、Elixir structに落とし込まれます。**schema はデフォルトで id という integr field が自動追加されます。** field macro はname と type で field を定義します。

```elixir:lib/friends/person.ex
defmodule Friends.Person do
  use Ecto.Schema

  schema "friends" do
    field :first_name, :string
    field :last_name, :string
    field :age, :integer
  end
end
```


　schemaファイルを作ったら **iex -S mix** で Elixir shell を立ち上げ、schema を使ってみます。

```sh:
[root@www13134uf friends]# iex -S mix
Erlang/OTP 20 [erts-9.1] [source] [64-bit] ...

Interactive Elixir (1.7.0-dev) - press Ctrl+C to exit (type h() ENTER for help)
```

　Friends.Person struct で person を作ります。

```sh:
iex(1)> person = %Friends.Person{age: 28}
%Friends.Person{
  __meta__: #Ecto.Schema.Metadata<:built, "people">,
  age: 28,
  first_name: nil,
  id: nil,
  last_name: nil
}
```

　上で作った person の age にアクセスします。

```sh:
iex(2)> person.age # => 28
28
```

　Friends.Person struct で空の person を作り直します。

```sh:
iex(3)> person = %Friends.Person{}
%Friends.Person{
  __meta__: #Ecto.Schema.Metadata<:built, "people">,
  age: nil,
  first_name: nil,
  id: nil,
  last_name: nil
}
```

　上で作った person を DB に insert します。結果として **id** の欄が返されるのが確認できます。

```sh:
iex(4)> Friends.Repo.insert(person)

15:20:43.992 [debug] QUERY OK db=3.0ms
INSERT INTO "people" VALUES (DEFAULT) RETURNING "id" []
{:ok,
 %Friends.Person{
   __meta__: #Ecto.Schema.Metadata<:loaded, "people">,
   age: nil,
   first_name: nil,
   id: 3,
   last_name: nil
 }}
```



# ５．Ecto.Changeset 

　**Changesets** は、データベースに変更を加える時に用いられる **変更部分( params )を表すデータ構造** で、変更部分( params )に含まれるfield値を filter したり castしたりする方法を提供します。同時にその変更部分( params )がDBに適用される前に、track や validateを行います。

https://hexdocs.pm/ecto/Ecto.html
https://hexdocs.pm/ecto/Ecto.Changeset.html

※changesetの意味をつかむためには、「６．Ecto.Query」の最後の例であるRepo.update(changeset)の使い方を見るとわかりやすいです。

　それでは実験してみましょう。まずlib/friends/person.exにchangesetの定義を追加します。

```elixir:lib/friends/person.ex
def changeset(person, params \\ %{}) do
  person
  |> Ecto.Changeset.cast(params, [:first_name, :last_name, :age])
  |> Ecto.Changeset.validate_required([:first_name, :last_name])
end
```

 ここでcastがchangesetを作ってくれています。dataに対してparamsが変更部分を表しています。

::: note info
**cast(data, params, permitted, opts \\ [])**
params を data の変更部分として、permitted keys に沿って、適用する。changeset をリターンする。
:::

　さて person を作り、それをもとに changeset をつくり、最後にそれを insert します。insert は前回のように schema struct の person を直接引数として **insert(person)** のようにも使えますし、今回のように **insert(changeset)** のようにも使えます。changeset の方が cast や validation を行ってくれるので良いですね。ちなみに validation は変更されるフィールド（paramsに含まれるもの）に関するもののみが行われます。

```sh:
iex(1)> person = %Friends.Person{}
%Friends.Person{
  __meta__: #Ecto.Schema.Metadata<:built, "people">,
  age: nil,
  first_name: nil,
  id: nil,
  last_name: nil
}

iex(2)> changeset = Friends.Person.changeset(person, %{first_name: "Ryan", last_
name: "Bigg"})
#Ecto.Changeset<
  action: nil,
  changes: %{first_name: "Ryan", last_name: "Bigg"},
  errors: [],
  data: #Friends.Person<>,
  valid?: true
>

iex(3)> Friends.Repo.insert(changeset)

16:55:17.455 [debug] QUERY OK db=2.8ms
INSERT INTO "people" ("first_name","last_name") VALUES ($1,$2) RETURNING "id" ["Ryan", "Bigg"]
{:ok,
 %Friends.Person{
   __meta__: #Ecto.Schema.Metadata<:loaded, "people">,
   age: nil,
   first_name: "Ryan",
   id: 4,
   last_name: "Bigg"
 }}
```

　次は上と同じ手順ですが、**validation エラー** になる例です。changeset を作る段階でchangeset.valid? が false でエラーになっています。当然次の insert もエラーになります。しかしchangeset.valid? が true であっても insert がエラーになることはあります。通常は、最終的にデータベースに insert を行い、エラーが返ってくるかを判断しなければなりません。


```sh:
iex(10)> person = %Friends.Person{}
%Friends.Person{
  __meta__: #Ecto.Schema.Metadata<:built, "people">,
  age: nil,
  first_name: nil,
  id: nil,
  last_name: nil
}

iex(11)> changeset = Friends.Person.changeset(person, %{})
#Ecto.Changeset<
  action: nil,
  changes: %{},
  errors: [
    first_name: {"can't be blank", [validation: :required]},
    last_name: {"can't be blank", [validation: :required]}
  ],
  data: #Friends.Person<>,
  valid?: false
>

iex(12)> Friends.Repo.insert(changeset)
{:error,
 #Ecto.Changeset<
   action: :insert,
   changes: %{},
   errors: [
     first_name: {"can't be blank", [validation: :required]},
     last_name: {"can't be blank", [validation: :required]}
   ],
   data: #Friends.Person<>,
   valid?: false
 >}
```

　**cast(data, params, permitted, opts \\ [])** の **permitted** の意味をはっきりさせるためにchangeset の定義を以下のように変えます。**changeset においては permitted にある項目だけに注目して、changes（差分）が作られます。params に含まれていても permitted になければ changes （差分）には含まれません。**

```elixir:lib/friends/person.ex
def changeset(person, params \\ %{}) do
  person
  # |> Ecto.Changeset.cast(params, [:first_name, :last_name, :age])
  |> Ecto.Changeset.cast(params, [:first_name, :age])
  |> Ecto.Changeset.validate_required([:first_name, :last_name])
end
```

　permitted から last_name を外すと、以下のように changes （差分）からも last_name が外れてしまいます。

```sh:
iex(1)> person = %Friends.Person{}
%Friends.Person{
  __meta__: #Ecto.Schema.Metadata<:built, "people">,
  age: nil,
  first_name: nil,
  id: nil,
  last_name: nil
}
iex(2)> changeset = Friends.Person.changeset(person, %{first_name: "Ryan", last_name: "Bigg"})
#Ecto.Changeset<
  action: nil,
  changes: %{first_name: "Ryan"},
  errors: [last_name: {"can't be blank", [validation: :required]}],
  data: #Friends.Person<>,
  valid?: false
>
```






# ６．Ecto.Query

　**データベースへの問い合わせは、まず Query を構築してから、次に Ecto.Repo に Query を渡してデータベースへの問い合わせを実行する、というステップを踏みます。**

　まずデーターベースを作り直し、shellを立ち上げます。

```sh:
mix ecto.drop
mix ecto.create
mix ecto.migrate

iex -S mix
```

　データベースにサンプルデータを insert します。

```sh:
iex(1)> people = [
...(1)>   %Friends.Person{first_name: "Ryan", last_name: "Bigg", age: 28},
...(1)>   %Friends.Person{first_name: "John", last_name: "Smith", age: 27},
...(1)>   %Friends.Person{first_name: "Jane", last_name: "Smith", age: 26},
...(1)> ]

Enum.each(people, fn (person) -> Friends.Repo.insert(person) end)
```

　以下は 1 個の結果を fetch する問い合わせで、それぞれ最初の結果と最後の結果を返します。

```sh:
iex(18)> Friends.Person |> Ecto.Query.first |> Friends.Repo.one

14:12:53.333 [debug] QUERY OK source="people" db=2.8ms
SELECT p0."id", p0."first_name", p0."last_name", p0."age" FROM "people" AS p0 ORDER BY p0."id" LIMIT 1 []
%Friends.Person{
  __meta__: #Ecto.Schema.Metadata<:loaded, "people">,
  age: 28,
  first_name: "Ryan",
  id: 1,
  last_name: "Bigg"
}


iex(19)> Friends.Person |> Ecto.Query.last |> Friends.Repo.one

14:13:09.237 [debug] QUERY OK source="people" db=2.8ms
SELECT p0."id", p0."first_name", p0."last_name", p0."age" FROM "people" AS p0 ORDER BY p0."id" DESC LIMIT 1 []
%Friends.Person{
  __meta__: #Ecto.Schema.Metadata<:loaded, "people">,
  age: 26,
  first_name: "Jane",
  id: 3,
  last_name: "Smith"
}
```

　以下は全ての結果をfetchする問い合わせです。

```sh:
iex(20)> Friends.Person |> Friends.Repo.all

14:15:42.476 [debug] QUERY OK source="people" db=1.8ms
SELECT p0."id", p0."first_name", p0."last_name", p0."age" FROM "people" AS p0 []
[
  %Friends.Person{
    __meta__: #Ecto.Schema.Metadata<:loaded, "people">,
    age: 28,
    first_name: "Ryan",
    id: 1,
    last_name: "Bigg"
  },
  %Friends.Person{
    __meta__: #Ecto.Schema.Metadata<:loaded, "people">,
    age: 27,
    first_name: "John",
    id: 2,
    last_name: "Smith"
  },
  %Friends.Person{
    __meta__: #Ecto.Schema.Metadata<:loaded, "people">,
    age: 26,
    first_name: "Jane",
    id: 3,
    last_name: "Smith"
  }
]
```

　以下は id による問い合わせです。

```sh:
iex(24)> Friends.Person |> Friends.Repo.get(2)

14:17:39.662 [debug] QUERY OK source="people" db=1.6ms
SELECT p0."id", p0."first_name", p0."last_name", p0."age" FROM "people" AS p0 WHERE (p0."id" = $1) [2]
%Friends.Person{
  __meta__: #Ecto.Schema.Metadata<:loaded, "people">,
  age: 27,
  first_name: "John",
  id: 2,
  last_name: "Smith"
}
```

　属性の値を指定した問い合わせです。

```sh:
iex(25)>  Friends.Person |> Friends.Repo.get_by(first_name: "Ryan")

14:21:02.842 [debug] QUERY OK source="people" db=2.8ms
SELECT p0."id", p0."first_name", p0."last_name", p0."age" FROM "people" AS p0 WHERE (p0."first_name" = $1) ["Ryan"]
%Friends.Person{
  __meta__: #Ecto.Schema.Metadata<:loaded, "people">,
  age: 28,
  first_name: "Ryan",
  id: 1,
  last_name: "Bigg"
}
```

　最後にデータベースの update の例です。現在の person を fetch して、更新内容を changeset にまとめます。最後に **Repo.update(changeset)** でデータベースに反映させます。

```sh:
iex(31)> person = Friends.Person |> Ecto.Query.first |> Friends.Repo.one

14:23:16.025 [debug] QUERY OK source="people" db=0.5ms
SELECT p0."id", p0."first_name", p0."last_name", p0."age" FROM "people" AS p0 ORDER BY p0."id" LIMIT 1 []
%Friends.Person{
  __meta__: #Ecto.Schema.Metadata<:loaded, "people">,
  age: 28,
  first_name: "Ryan",
  id: 1,
  last_name: "Bigg"
}

iex(32)> changeset = Friends.Person.changeset(person, %{age: 29})
#Ecto.Changeset<
  action: nil,
  changes: %{age: 29},
  errors: [],
  data: #Friends.Person<>,
  valid?: true
>

iex(33)> Friends.Repo.update(changeset)

14:23:38.647 [debug] QUERY OK db=2.1ms
UPDATE "people" SET "age" = $1 WHERE "id" = $2 [29, 1]
{:ok,
 %Friends.Person{
   __meta__: #Ecto.Schema.Metadata<:loaded, "people">,
   age: 29,
   first_name: "Ryan",
   id: 1,
   last_name: "Bigg"
 }}
```


以上です。



**■ Elixir/Phoenixの基礎についてまとめた過去記事**
[Elixir Ecto チュートリアル - Qiita](https://qiita.com/sand/items/0861ad7a16f7ac612f41)
[Elixir Ecto のまとめ - Qiita](https://qiita.com/sand/items/71d0b35d74a4781f3564)
[Elixir Ecto Association - Qiita]
(https://qiita.com/sand/items/5581497972473e308f05)
[Phoenix1.3の基本的な仕組み - Qiita](https://qiita.com/sand/items/f42dc35c47589eab687e)
[Phoenixのログイン管理のためのSessionの使い方 - Qiita](https://qiita.com/sand/items/a88f1c39f3a6f7a01f99)
[Phoenix1.3のUserアカウントとSession - Qiita](https://qiita.com/sand/items/8e541f8a950ae6c63931)
[Phoenix1.3+Guardian1.0でJWT - Qiita](https://qiita.com/sand/items/393827194e687efafd4f)