LoginSignup
37

More than 1 year has passed since last update.

Elixir Ecto のまとめ

Last updated at Posted at 2018-01-12

(追記)本記事は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

(2018/04/30)追記 - 本記事の応用記事を書きました。
Elmとleafletでつくるgeolocationの発信機・受信機 - Qiita
Elmとleafletでつくるgeolocationの再現機 - Qiita

1.Ectoのインストール

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

mix new friends --sup

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

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

 インストールします。

mix deps.get

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

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

2.Ecto.Repo

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

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

mix ecto.gen.repo -r Friends.Repo

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

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を参照します。その接続情報をもとにデータベースにアクセスします。

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の編集は必要ありません。)

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を受け取り実行できるようになります。

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

  children = [
    Friends.Repo,
  ]

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

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

3.DBとテーブルの作成

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

mix ecto.create

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

 mix ecto.gen.migration create_friends

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

[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 を編集して以下のようにします。

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ファイルを走らせて、テーブルを作成します。

mix ecto.migrate

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

4.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 を定義します。

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 を使ってみます。

[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 を作ります。

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 にアクセスします。

iex(2)> person.age # => 28
28

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

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 の欄が返されるのが確認できます。

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
 }}

5.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の意味をつかむためには、「6.Ecto.Query」の最後の例であるRepo.update(changeset)の使い方を見るとわかりやすいです。

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

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が変更部分を表しています。

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に含まれるもの)に関するもののみが行われます。

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 を行い、エラーが返ってくるかを判断しなければなりません。

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 (差分)には含まれません。

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 が外れてしまいます。

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
>

6.Ecto.Query

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

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

mix ecto.drop
mix ecto.create
mix ecto.migrate

iex -S mix

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

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 する問い合わせで、それぞれ最初の結果と最後の結果を返します。

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する問い合わせです。

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 による問い合わせです。

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"
}

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

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) でデータベースに反映させます。

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
Elixir Ecto のまとめ - Qiita
[Elixir Ecto Association - Qiita]
(https://qiita.com/sand/items/5581497972473e308f05)
Phoenix1.3の基本的な仕組み - Qiita
Phoenixのログイン管理のためのSessionの使い方 - Qiita
Phoenix1.3のUserアカウントとSession - Qiita
Phoenix1.3+Guardian1.0でJWT - Qiita

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
37