(追記)本記事は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のドライバです。
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 :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を参照します。その接続情報をもとにデータベースにアクセスします。
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の編集は必要ありません。)
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を受け取り実行できるようになります。
def start(_type, _args) do
import Supervisor.Spec
children = [
Friends.Repo,
]
最後にconfig/config.exsを編集して、以下の一行を追加します。これでアプリケーションにrepoについて教えます。
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 を編集して以下のようにします。
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 を定義します。
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の定義を追加します。
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 (差分)には含まれません。
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