どうも、靖宗です。
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/3
とvalidate_required/3
という関数を通過しています。
cast/3
は第一引数に構造体、第二引数にパラメータ(値の登録や更新に使われる)、第三引数にアップデートするカラムの指定を渡す仕様のようです。役割としてはスキーマのフィールドを取ってきてるだけのようです。
validate_required/3
はcast/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/1
やRepo.delete/1
などが実装されています。その他にもRepo.insert_all
、Repo.update_all
、Repo.delete_all
も実装されています。
その他詳細
とはいえココで全部の機能を書ききれる訳ではないのでより知りたい方はEctoのドキュメントを参照とのこと。
近いうちにやります。
リレーションとか
どうやらこの章ではデータベースのリレーションなどには言及しないようです。
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.exs
やconfig/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側でデータベースを変更するのは楽だと思います。