Elixirのコンソールで随分苦しんだので
Phoenixを触ってはみたものの、モデル操作辺りでコンソール操作がうまくいかず、すっかり打ちひしがれているそこのアナタ!
PhoenixもRailsのようにサクサク触ってみたいではなくないですか?
ただ・・・Phoenixのやつ、Railsコンソールみたいにサクサク実験できる段階になるまで少し前提条件が存在するようです。
実際、コンソールで挙動を試そうとしてググっても、何通りも書き方が出てきたり、そのとおりにやってみても言うこと聞かない事が多くて、コンソールの操作に本当にイライラします。
ここでは、そんな状況から脱するための前提条件をステップを踏んで紹介していきたいと思います。
ここでやること
- アプリ立ち上げ
- モデル作成
- 新規データーの作成
- DBへの登録(insert)
前提条件
MacOS 12.3.1 (Apple M1)
Elixir 1.13.4 (compiled with Erlang/OTP 25)
Phoenix installer v1.6.10
psql (PostgreSQL) 14.2
特にバージョンの差が出る部分でもないとは思いますので、古すぎなければ良いかと思います。
本文中、bashとzshの表記が混在していますが、Macがzshをデフォルトとしている為、Phoenix側からのガイドメッセージはbash($)表記ですが、コマンドはzsh(%)で表記されています。
ベースとなるアプリの作成
早速アプリを作成します。
% mix phx.new phx_sample
でサクッと土台を作ります。
.
.
(略)
* running mix deps.compile
We are almost there! The following steps are missing:
$ cd phx_sample
Then configure your database in config/dev.exs and run:
$ mix ecto.create
Start your Phoenix app with:
$ mix phx.server
You can also run your app inside IEx (Interactive Elixir) as:
$ iex -S mix phx.server
メッセージの通り、
% cd phx_sample
でプロジェクトフォルダに移動して、
% mix ecto.create
をやっておきます。
モデルの作成
モデルの作成ですが、 phx.gen.schema
というコマンドを使って作成します。
(※ 本当は phx.gen.context
で作った方がテスト用のファイル等も出来て都合良かったりしますが、今回は最低限で・・)
(* これは打たない)
% mix phx.gen.schema Post posts title:string content:text
とすると、以下のレスポンスとなります。
* creating lib/phx_sample/post.ex
* creating priv/repo/migrations/xxxxxxxxxxxx_create_posts.exs
Remember to update your repository by running migrations:
$ mix ecto.migrate
上記2ファイルが作成され、 post.ex
内に changeset
という関数が作成されますので、モデル操作に必要なものは揃います。
上記コマンドでは以下のような post.ex
が作成されます。
(* これはサンプル)
defmodule PhxSample.Post do
use Ecto.Schema
import Ecto.Changeset
schema "posts" do
field :content, :string
field :title, :string
timestamps()
end
@doc false
def changeset(post, attrs) do
post
|> cast(attrs, [:title, :content])
|> validate_required([:title, :content])
end
end
これだけでもモデル操作出来ますし、Railsのモデルと考えるのであれば、感覚として同じ状態ですが、Phoenixの場合、他のgenコマンドとの兼ね合いでネームスペースを作成しておくと良さそうです。
% mix phx.gen.schema Posts.Post posts title:string content:text
このように、 Posts.Post
として、 (同名である必要はありませんが)何か機能の括り的な命名で、ネームスペースを作成しておくと良いと思います。
すると、 以下のレスポンスとなり、少しモデルの作成場所が変わります。
* creating lib/phx_sample/posts/post.ex
* creating priv/repo/migrations/xxxxxxxxxxxx_create_posts.exs
Remember to update your repository by running migrations:
$ mix ecto.migrate
そして、作成されるファイル内容は、
defmodule PhxSample.Posts.Post do
use Ecto.Schema
import Ecto.Changeset
schema "posts" do
field :content, :string
field :title, :string
timestamps()
end
@doc false
def changeset(post, attrs) do
post
|> cast(attrs, [:title, :content])
|> validate_required([:title, :content])
end
end
このように 1行目の
defmodule PhxSample.Posts.Post do
部分だけ違って作成されます。
この影響で、モデルを呼び出す時の書き方が変わってきますので、操作で迷った際はモデルを見に行くようにすれば良いでしょう。
では、操作の前にマイグレーションを通しておきましょう。
% mix ecto.migrate
として、
Compiling 1 file (.ex)
Generated phx_sample app
12:57:35.205 [info] == Running xxxxxxxxxxx PhxSample.Repo.Migrations.CreatePosts.change/0 forward
12:57:35.207 [info] create table posts
12:57:35.217 [info] == Migrated xxxxxxxxxxxx in 0.0s
マイグレーションが通れば準備OKです。
コンソールを試す
まずはコンソールの起動
コンソールの起動は、
% iex -S mix
で行います。
Erlang/OTP 25 [erts-13.0.2] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [jit]
Interactive Elixir (1.13.4) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)>
こんな感じで、iexが表示されればOKです。 -S mix
とすることで、プロジェクトのモデルを操作する事が出来ています。
では、早速実験です。
Ecto
に Changeset.cast/3
という関数があります。
Ecto.Changeset.cast(%PhxSample.Posts.Post{update時に元のデーターが入る}, %{入力したいデーター}, [アトリビュート])
となりますので、元のデーターがない新規作成の場合
Ecto.Changeset.cast(%PhxSample.Posts.Post{}, %{ title: "", content: "" }, [:title, :content])
このようにすれば、作成済みのモデルを操作するための構造体が作成できます。
iex(1)> Ecto.Changeset.cast(%PhxSample.Posts.Post{}, %{ title: "", content: "" }, [:title, :content])
#Ecto.Changeset<action: nil, changes: %{}, errors: [],
data: #PhxSample.Posts.Post<>, valid?: true>
changeset
ただ、ここまで来ると毎回打つのが大変です。
もちろんアトリビュート等の部分は、すでにモデルに実装されておりますので、省略された記法が使えます。
defmodule PhxSample.Posts.Post do
use Ecto.Schema
import Ecto.Changeset
schema "posts" do
field :content, :string
field :title, :string
timestamps()
end
@doc false
def changeset(post, attrs) do
post
|> cast(attrs, [:title, :content])
|> validate_required([:title, :content])
end
end
ここに記述されている、 changeset/2
を使います。
この changeset/2
関数は、
import Ecto.Changeset
の記述によって、 Changeset.cast/3
が PhxSample.Posts.Post
内で使用可能となっています。
def changeset(post, attrs) do
post
|> cast(attrs, [:title, :content])
|> validate_required([:title, :content])
end
関数内部では
post
|> cast(attrs, [:title, :content])
このように使われていますね。
パイプ演算子を使って記述しているので、第一引数が見当たらなくなっていますね。
パイプ演算子の場合、本来の第一引数は、 post
部分となっています。
書き順が変わるので、慣れるまでなかなか覚えづらい箇所です。
この関数を使って書き直すと、
PhxSample.Posts.Post.changeset(%PhxSample.Posts.Post{}, %{})
または、データーを空文字で入れるなら
PhxSample.Posts.Post.changeset(%PhxSample.Posts.Post{}, %{title: "", content: ""})
こうなります。試してみましょう。
iex(2)> PhxSample.Posts.Post.changeset(%PhxSample.Posts.Post{}, %{title: "", content: ""})
#Ecto.Changeset<
action: nil,
changes: %{},
errors: [
title: {"can't be blank", [validation: :required]},
content: {"can't be blank", [validation: :required]}
],
data: #PhxSample.Posts.Post<>,
valid?: false
>
結果は少し変わってきますが、これは changeset/2
の内部で
|> validate_required([:title, :content])
バリデーションを通すようになっている為です。
alias
ここまで来ると、 PhxSample.Posts.Post
と書くのが煩わしいですね。
ただし、Railsとは違ってモデル名だけで呼び出しが出来ませんので、
Post.changeset(%Post{}, %{title: "", content: ""})
こういう書き方をすると、
iex(3)> Post.changeset(%Post{}, %{title: "", content: ""})
** (CompileError) iex:18: Post.__struct__/1 is undefined, cannot expand struct Post. Make sure the struct name is correct. If the struct name exists and is correct but it still cannot be found, you likely have cyclic module usage in your code
エラーとなります。
Railsでは
Post.new
のようにインスタンスを作る事が出来ていました。
近い操作性をPoenixで実現するのであれば、 alias
を書いておくのが良いようです。
alias PhxSample.Posts.Post
と書くことで、最後に記述されている Post
を PhxSample.Posts.Post
として使う事が出来ます。
iex(4)> alias PhxSample.Posts.Post
PhxSample.Posts.Post
こうすれば、
iex(5)> Post.changeset(%Post{}, %{title: "", content: ""})
#Ecto.Changeset<
action: nil,
changes: %{},
errors: [
title: {"can't be blank", [validation: :required]},
content: {"can't be blank", [validation: :required]}
],
data: #PhxSample.Posts.Post<>,
valid?: false
>
随分シンプルになってきました。
alias
さえ書いておけば、Railsの
Post.new
のような感覚で、
Post.changeset(%Post{}, %{})
と、書くことができました。
では、レコードさせてみましょう。
insert
changeset = Post.changeset(%Post{}, %{title: "テスト", content: "こんにちは"})
先に変数にchangesetを作成します。
iex(6)> changeset = Post.changeset(%Post{}, %{title: "テスト", content: "こんにちは"})
#Ecto.Changeset<
action: nil,
changes: %{content: "こんにちは", title: "テスト"},
errors: [],
data: #PhxSample.Posts.Post<>,
valid?: true
>
これを、登録するには、
PhxSample.Repo.insert(changeset)
このように書きます。
iex(7)> PhxSample.Repo.insert(changeset)
[debug] QUERY OK db=2.6ms queue=14.8ms idle=1698.4ms
INSERT INTO "posts" ("content","title","inserted_at","updated_at") VALUES ($1,$2,$3,$4) RETURNING "id" ["こんにちは", "テスト", ~N[2022-07-12 05:19:13], ~N[2022-07-12 05:19:13]]
↳ :erl_eval.do_apply/7, at: erl_eval.erl:744
{:ok,
%PhxSample.Posts.Post{
__meta__: #Ecto.Schema.Metadata<:loaded, "posts">,
content: "こんにちは",
id: 4,
inserted_at: ~N[2022-07-12 05:19:13],
title: "テスト",
updated_at: ~N[2022-07-12 05:19:13]
}}
登録できました。
ここまでの処理も、alias使ってもう少し書き直すと、
iex(8)> alias PhxSample.Repo
PhxSample.Repo
iex(9)> Repo.insert(changeset) [debug] QUERY OK db=2.9ms idle=639.5ms
INSERT INTO "posts" ("content","title","inserted_at","updated_at") VALUES ($1,$2,$3,$4) RETURNING "id" ["こんにちは", "テスト", ~N[2022-07-12 05:22:37], ~N[2022-07-12 05:22:37]]
↳ :erl_eval.do_apply/7, at: erl_eval.erl:744
{:ok,
%PhxSample.Posts.Post{
__meta__: #Ecto.Schema.Metadata<:loaded, "posts">,
content: "こんにちは",
id: 5,
inserted_at: ~N[2022-07-12 05:22:37],
title: "テスト",
updated_at: ~N[2022-07-12 05:22:37]
}}
このようになりますし、パイプ演算子を使うと、一旦変数を置かなくても
Post.changeset(%Post{}, %{title: "テスト", content: "こんにちは"}) |> Repo.insert
このように書けますので、
iex(10)> Post.changeset(%Post{}, %{title: "テスト", content: "こんにちは"}) |> Repo.insert
[debug] QUERY OK db=4.7ms queue=0.1ms idle=1254.3ms
INSERT INTO "posts" ("content","title","inserted_at","updated_at") VALUES ($1,$2,$3,$4) RETURNING "id" ["こんにちは", "テスト", ~N[2022-07-12 05:24:29], ~N[2022-07-12 05:24:29]]
↳ :erl_eval.do_apply/7, at: erl_eval.erl:744
{:ok,
%PhxSample.Posts.Post{
__meta__: #Ecto.Schema.Metadata<:loaded, "posts">,
content: "こんにちは",
id: 6,
inserted_at: ~N[2022-07-12 05:24:29],
title: "テスト",
updated_at: ~N[2022-07-12 05:24:29]
}}
Railsコンソールのような感覚で、コマンド履歴を利用して少しずつ結果を見なから実験できるようになりますね。
だいぶ扱いやすくなったのではないでしょうか?
まとめ
- コンソールを使うときは alias を使って記述を短縮
- パイプ演算子を使うと、処理の順番通りに記述出来るので、実験しやすくなる
次回は、呼び出し・編集・削除等も紹介します。