3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

【Railsエンジニア向き】Phoenixでコンソールからモデル操作をやってみよう【ハンズオン】

Last updated at Posted at 2022-07-12

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 が作成されます。

lib/phx_sample/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

そして、作成されるファイル内容は、

lib/phx_sample/posts/post.ex
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 とすることで、プロジェクトのモデルを操作する事が出来ています。

では、早速実験です。

EctoChangeset.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

ただ、ここまで来ると毎回打つのが大変です。

もちろんアトリビュート等の部分は、すでにモデルに実装されておりますので、省略された記法が使えます。

lib/phx_sample/posts/post.ex
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/3PhxSample.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

と書くことで、最後に記述されている PostPhxSample.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 を使って記述を短縮
  • パイプ演算子を使うと、処理の順番通りに記述出来るので、実験しやすくなる

次回は、呼び出し・編集・削除等も紹介します。

3
0
1

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
3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?