技術メモ

プログラミングとか電子工作とか

Phoenix入門 (第12章 Ecto その2)

f:id:ysmn_deus:20190219130922p:plain

どうも、靖宗です。
Ectoの続きです。前回はほとんど仕様みたいな感じになってたので実際に扱う所までできたらなぁと思います。

Ecto

Changesets and Validations

概要

Changesetsは前回みたスキーマが定義されているファイルにある関数で、データをデータベースに登録する前に型変換やバリデーションが行われる関数のようです。
バリデーションで不正な値をはじいたり、扱ってるデータのフィールドが更新されているか否かも取得できるみたいです。

def changeset(%User{} = user, attrs) do
  user
  |> cast(attrs, [:name, :email, :bio, :number_of_pets])
  |> validate_required([:name, :email, :bio, :number_of_pets])
end

changeset/2関数の中にはパイプラインが存在していて、userデータがcast/3validate_required/3という関数を通過しています。
cast/3は第一引数に構造体、第二引数にパラメータ(値の登録や更新に使われる)、第三引数にアップデートするカラムの指定を渡す仕様のようです。役割としてはスキーマのフィールドを取ってきてるだけのようです。
validate_required/3cast/3の返値を第一引数で受けて、第二引数に渡されたカラムのデータをバリデーションするようです。

実際に扱う

なにはともあれ動かして確認するのが一番です。iexで確認できるようなのでやってみます。
ややこしいので最初にalias HelloPhoenix.Userを使って、いちいちHelloPhoenix.User.changesetとか書かないでいいようにしておきます。

PS \hello_phoenix> iex.bat -S mix
Interactive Elixir (1.8.0) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> alias HelloPhoenix.User
HelloPhoenix.User

空のUser構造体を利用してchangesetを使ってみます。

iex(2)> changeset = User.changeset(%User{}, %{})
#Ecto.Changeset<
  action: nil,
  changes: %{},
  errors: [
    name: {"can't be blank", [validation: :required]},
    email: {"can't be blank", [validation: :required]},
    bio: {"can't be blank", [validation: :required]},
    number_of_pets: {"can't be blank", [validation: :required]}
  ],
  data: #HelloPhoenix.User<>,
  valid?: false
>

なにか帰ってきましたがエラーが出てるっぽいです。まぁ空でバリデーションしてるんであたりまえなんですが。
changeset.valid?ではじかれてるかどうか判断できるようです。

iex(3)> changeset.valid?
false

changeset.errorsで何がマズいのか見れます。各フィールドのエラーが参照できてますね。

iex(4)> changeset.errors
[
  name: {"can't be blank", [validation: :required]},
  email: {"can't be blank", [validation: :required]},
  bio: {"can't be blank", [validation: :required]},
  number_of_pets: {"can't be blank", [validation: :required]}
]

全てが必須項目になってます。

バリデーションの編集

number_of_petsをオプション(必須ではない)にしてみましょう。
changesetのパイプラインのvalidate_requiredを編集します。

...
  def changeset(user, attrs) do
    user
    |> cast(attrs, [:name, :email, :bio, :number_of_pets])
    |> validate_required([:name, :email, :bio]) # これ
  end
...

iexで再コンパイルして実行します。

iex(5)> recompile()
Compiling 1 file (.ex)
:ok
iex(6)> changeset = User.changeset(%User{}, %{})
#Ecto.Changeset<
  action: nil,
  changes: %{},
  errors: [
    name: {"can't be blank", [validation: :required]},
    email: {"can't be blank", [validation: :required]},
    bio: {"can't be blank", [validation: :required]}
  ],
  data: #HelloPhoenix.User<>,
  valid?: false
>
iex(7)> changeset.errors
[
  name: {"can't be blank", [validation: :required]},
  email: {"can't be blank", [validation: :required]},
  bio: {"can't be blank", [validation: :required]}
]

number_of_petsのエラーは消えました。必須か否かはvalidate_requiredに渡すリストで制御されていることが確認できました。

不要なデータ(castの動作チェック)

changesetに渡すパラメータの中にスキーマに登録されていないデータが存在した場合にはどうなるでしょうか?(バリデーションではじかれるか無視される?)
実際にやってみます。

iex(8)> params = %{name: "Joe Example", email: "joe@example.com", bio: "An example to all", number_of_pets: 5, random_key: "random value"}
%{
  bio: "An example to all",
  email: "joe@example.com",
  name: "Joe Example",
  number_of_pets: 5,
  random_key: "random value"
}

random_key: "random value"という余分なキーペアを入れたマップを作成しました。
これをchangesetに渡します。

iex(9)> changeset = User.changeset(%User{}, params)
#Ecto.Changeset<
  action: nil,
  changes: %{
    bio: "An example to all",
    email: "joe@example.com",
    name: "Joe Example",
    number_of_pets: 5
  },
  errors: [],
  data: #HelloPhoenix.User<>,
  valid?: true
>

はじかれはしないようです。
バリデーションは正常でしょうか?

iex(10)> changeset.valid?
true

問題ナシ。では値は?

iex(11)> changeset.changes
%{
  bio: "An example to all",
  email: "joe@example.com",
  name: "Joe Example",
  number_of_pets: 5
}

random_key: "random value"は綺麗さっぱり消滅しました。おそらくcast/3のところでスキーマに無いパラメータは落とされるのでしょう。

他のバリデーション

RDBで様々な制約などが考えられますし、そもそもの制約(英語だけとか)もあるので、必須か否かのバリデーションだけでは心許ないです。
もちろん他のバリデーションもあるようなので見ていきます。まずは、データ長を指定するvalidate_lengthから。

  def changeset(user, attrs) do
    user
    |> cast(attrs, [:name, :email, :bio, :number_of_pets])
    |> validate_required([:name, :email, :bio])
    |> validate_length(:bio, min: 2)
  end

これで最小2文字というバリデーションがかかるようです。パイプラインで完結に書けるのはElixirの良いところですね。
実際に1文字とかを登録してバリデーションではじかれるかやってみます。

iex(12)> recompile()
Compiling 1 file (.ex)
:ok
iex(13)> changeset = User.changeset(%User{}, %{bio: "A"})
#Ecto.Changeset<
  action: nil,
  changes: %{bio: "A"},
  errors: [
    bio: {"should be at least %{count} character(s)",
     [count: 2, validation: :length, kind: :min]},
    name: {"can't be blank", [validation: :required]},
    email: {"can't be blank", [validation: :required]}
  ],
  data: #HelloPhoenix.User<>,
  valid?: false
>
iex(14)> changeset.errors[:bio]
{"should be at least %{count} character(s)",
 [count: 2, validation: :length, kind: :min]}

長さ足りないぞ!って怒られてます。
最大長さはminのところをmaxにすればよさそうです。

  def changeset(user, attrs) do
    user
    |> cast(attrs, [:name, :email, :bio, :number_of_pets])
    |> validate_required([:name, :email, :bio])
    |> validate_length(:bio, min: 2)
    |> validate_length(:bio, max: 140)
  end

お次はフォーマットを決めるvalidate_format/3。例としてemailのフォーマットを@が入ってないと怒るようにします。

  def changeset(user, attrs) do
    user
    |> cast(attrs, [:name, :email, :bio, :number_of_pets])
    |> validate_required([:name, :email, :bio])
    |> validate_length(:bio, min: 2)
    |> validate_length(:bio, max: 140)
    |> validate_format(:email, ~r/@/)
  end

最後の引数に渡す正規表現でフォーマットを決定しているようです。
実際にはじかれるか確認します。

iex(22)> changeset = User.changeset(%User{}, %{email: "example.com"})
#Ecto.Changeset<
  action: nil,
  changes: %{email: "example.com"},
  errors: [
    email: {"has invalid format", [validation: :format]},
    name: {"can't be blank", [validation: :required]},
    bio: {"can't be blank", [validation: :required]}
  ],
  data: #HelloPhoenix.User<>,
  valid?: false
>
iex(23)> changeset.errors[:email]
{"has invalid format", [validation: :format]}

長さの時もそうでしたが、一応changesに値は格納されるようです。(ただし、changeset.valid?はfalse)

Data Persistence

スキーマとかを見て来ましたが、これらを取得したり格納したりはまだでした。たぶんそんな話。
lib/hello_phoenix/repo.exを前回みましたが、ココの編集次第ではPostgreSQLなどだけでなくRESTful APIもEctoで扱えるみたいです。それは便利そう。

insert

とりあえずiexで触ってみます。最初にUserエイリアスも付けてますが、さっきやってるので本来は不要です。

iex(24)> alias HelloPhoenix.{Repo, User}
[HelloPhoenix.Repo, HelloPhoenix.User]
iex(25)> Repo.insert(%User{email: "user1@example.com"})
[debug] QUERY OK db=16.0ms
INSERT INTO "users" ("email","inserted_at","updated_at") VALUES ($1,$2,$3) RETURNING "id" ["user1@example.com", ~N[2019-03-22 02:27:19], ~N[2019-03-22 02:27:19]]
{:ok,
 %HelloPhoenix.User{
   __meta__: #Ecto.Schema.Metadata<:loaded, "users">,
   bio: nil,
   email: "user1@example.com",
   id: 1,
   inserted_at: ~N[2019-03-22 02:27:19],
   name: nil,
   number_of_pets: nil,
   updated_at: ~N[2019-03-22 02:27:19]
 }}
iex(26)> Repo.insert(%User{email: "user2@example.com"})
[debug] QUERY OK db=16.0ms
INSERT INTO "users" ("email","inserted_at","updated_at") VALUES ($1,$2,$3) RETURNING "id" ["user2@example.com", ~N[2019-03-22 02:28:00], ~N[2019-03-22 02:28:00]]
{:ok,
 %HelloPhoenix.User{
   __meta__: #Ecto.Schema.Metadata<:loaded, "users">,
   bio: nil,
   email: "user2@example.com",
   id: 2,
   inserted_at: ~N[2019-03-22 02:28:00],
   name: nil,
   number_of_pets: nil,
   updated_at: ~N[2019-03-22 02:28:00]
 }}

Repo.insert/1でデータを格納できるようです。様々な情報(何秒かかったとか)はデバッグ環境だから出ているものだそうで、基本的には返値の{:ok, %User{}}で成功か否かを判断するようです。
一応PostgreSQLに格納されてるか見ときます。

PS > docker exec -it ph_psql /bin/bash
root@c1b71c3bc403:/# psql -U elixir
psql (10.5 (Debian 10.5-1.pgdg90+1))
Type "help" for help.

elixir=# \connect hello_phoenix_dev
You are now connected to database "hello_phoenix_dev" as user "elixir".
hello_phoenix_dev=# select * from users;
 id | name |       email       | bio | number_of_pets |     inserted_at     |     updated_at
----+------+-------------------+-----+----------------+---------------------+---------------------
  1 |      | user1@example.com |     |                | 2019-03-22 02:27:19 | 2019-03-22 02:27:19
  2 |      | user2@example.com |     |                | 2019-03-22 02:28:00 | 2019-03-22 02:28:00
(2 rows)

2行追加されています。

all

今度はデータを取り出してみます。スキーマを指定すれば該当するテーブルからデータを取り出せるようです。
とりあえず全部取り出します。

iex(27)> Repo.all(User)
[debug] QUERY OK source="users" db=0.0ms
SELECT u0."id", u0."bio", u0."email", u0."name", u0."number_of_pets", u0."inserted_at", u0."updated_at" FROM "users" AS u0 []
[
  %HelloPhoenix.User{
    __meta__: #Ecto.Schema.Metadata<:loaded, "users">,
    bio: nil,
    email: "user1@example.com",
    id: 1,
    inserted_at: ~N[2019-03-22 02:27:19],
    name: nil,
    number_of_pets: nil,
    updated_at: ~N[2019-03-22 02:27:19]
  },
  %HelloPhoenix.User{
    __meta__: #Ecto.Schema.Metadata<:loaded, "users">,
    bio: nil,
    email: "user2@example.com",
    id: 2,
    inserted_at: ~N[2019-03-22 02:28:00],
    name: nil,
    number_of_pets: nil,
    updated_at: ~N[2019-03-22 02:28:00]
  }
]

取ってきたデータは既にElixirの構造体になっており、非常に便利です。

Ecto.Query

Ecto.Queryを利用すると様々な機能が使える上にSQLインジェクション攻撃防止や最適化もしてくれるようです。

iex(28)> import Ecto.Query
Ecto.Query
iex(29)> Repo.all(from u in User, select: u.email)
[debug] QUERY OK source="users" db=0.0ms
SELECT u0."email" FROM "users" AS u0 []
["user1@example.com", "user2@example.com"]

SQL文の様な書き方ができるようになりました。
from u in UserでUser構造体のテーブルの各要素をuとするとき、という条件を付け、第二引数のselect: u.emailでu(各要素)のemailを抽出する処理になるようです。
少々SQL文とは違いますが雰囲気一緒なので慣れればどうということは無さそうです。

別の例を実行します。

iex(30)> Repo.one(from u in User, where: ilike(u.email, "%1%"), select: count(u.id))
[debug] QUERY OK source="users" db=0.0ms queue=15.0ms
SELECT count(u0."id") FROM "users" AS u0 WHERE (u0."email" ILIKE '%1%') []
1

Repo.oneは結果が1個のクエリを発行する際に使われる関数のようで、where句やselect句を取っています。
ilikeやcountはPostgreSQLなどに準拠する物です。

Ecto.Queryを利用すればSQLの様な形で直接マップのリストなどを作成することができます。

iex(33)> Repo.all(from u in User, select: %{u.id => u.email})
[debug] QUERY OK source="users" db=0.0ms
SELECT u0."id", u0."email" FROM "users" AS u0 []
[%{1 => "user1@example.com"}, %{2 => "user2@example.com"}]

updateなど

Repoには当然のことRepo.update/1Repo.delete/1などが実装されています。その他にもRepo.insert_allRepo.update_allRepo.delete_allも実装されています。

その他詳細

とはいえココで全部の機能を書ききれる訳ではないのでより知りたい方はEctoのドキュメントを参照とのこと。

hexdocs.pm

近いうちにやります。

リレーションとか

どうやらこの章ではデータベースのリレーションなどには言及しないようです。
Contextの章(次章)で言及があるそうですが、クッソ長いや~~~ん・・・( ´゚д゚`)
英語の練習と思ってがんばります

Using MySQL

MySQLを利用するときの指南書。
たぶんMySQLじゃないとだめってシーン少ないと思うので素直にPostgreSQLを利用するのが良いんじゃないかとは思いますが、さっと見ておきます。

まずmix phx.newするときに明示的にmysqlを利用するオプションを付けるようです。たぶん付けなくてもあとで変更可能だとは思うんですがめんどくさいと思います。

mix phx.new hello_phoenix --database mysql

あとはmix deps.getして完了!簡単ですね。

では、既存のプロジェクトをMySQLに変更する場合。
まずmix.exsを編集します。

defmodule HelloPhoenix.MixProject do
  use Mix.Project

  . . .
  # Specifies your project dependencies.
  #
  # Type `mix help deps` for examples and options.
  defp deps do
    [
      {:phoenix, "~> 1.4.0"},
      {:phoenix_pubsub, "~> 1.1"},
      {:phoenix_ecto, "~> 4.0"},
      {:ecto_sql, "~> 3.0"},
      {:mariaex, ">= 0.0.0"}, # これを追加
      {:phoenix_html, "~> 2.11"},
      {:phoenix_live_reload, "~> 1.2", only: :dev},
      {:gettext, "~> 0.11"},
      {:plug_cowboy, "~> 2.0"}
    ]
  end
end

次にconfig/dev.exsを編集。

config :hello_phoenix, HelloPhoenix.Repo,
username: "root",
password: "",
database: "hello_phoenix_dev"

必要に応じてconfig/test.exsconfig/prod.secret.exsも編集。
お次にlib/hello_phoenix/repo.ex

    adapter: Ecto.Adapters.Postgres

Ecto.Adapters.MySQLに変更。

以上で完了のはず!mix do deps.get, compileして、mix ecto.createすれば利用できるはずです。

なんにせよ途中でデータベースを変更するのはあまり推奨されることでは無いとは思います。
可能であればRESTful APIで分離して管理しておけばAPI側でデータベースを変更するのは楽だと思います。