Programming Phoenix勉強その6

Programming Phoenix勉強その6

その6です。 実際にDBを操作するところからです。

新規ユーザ生成処理

Rumbl.UserController に以下の関数を実装します。

def new(conn, _params) do
  changeset = User.changeset(%User{})
  render conn, "new.html", changeset: changeset
end

changeset 周りとかが謎めいていますが一旦置いときます。単に自分が今理解してないだけですが・・・ DBの操作とそれ以外の検証とかエラーとかセキュリティとかを分離するのに役立つっぽいです。 user.ex に上記で利用している User.changeset 関数を実装します。

def changeset(model, params \\ :empty) do
  model
  |> cast(params, ~w(name username), [])
  |> validate_length(:username, min: 1, max: 20)
end

Ecto を使う関数を定義しました。 castEcto.changeset を生成してバリデーションチェックを掛けているようです。

前準備

:new アクションを実装する前に前準備をします。 今まで書いてあったルーティング設定を消して以下を追加します。まぁ説明不要だと思います。

resouces "/users", UserController, only: [:index, :show, :new, :create]

テンプレート実装

:new に対応するテンプレートを適当に作ります。

<h1>New User</h1>

<%= form_for @changeset, user_path(@conn, :create), fn f -> %>
  <div class="form-group">
    <%= text_input f, :name, placeholder: "Name", class: "form-control" %>
  </div>
  <div class="form-group">
    <%= text_input f, :username, placeholder: "Username", class: "form-control" %>
  </div>
  <div class="form-group">
    <%= password_input f, :password, placeholder: "Password", class: "form-control" %>
  </div>
  <%= submit "Create User", class: "btn, btn-primary" %>
<% end %>

ここまでやって起動したところ、何やらWarningが出たので解消します。

warning: `Ecto.Changeset.cast/4` is deprecated, please use `cast/3` + `validate_required/3` instead
    (rumbl) web/models/user.ex:15: Rumbl.User.changeset/2
    (rumbl) web/controllers/user_controller.ex:16: Rumbl.UserController.new/2
    (rumbl) web/controllers/user_controller.ex:1: Rumbl.UserController.action/2
    (rumbl) web/controllers/user_controller.ex:1: Rumbl.UserController.phoenix_controller_pipeline/2

warning: passing :empty to Ecto.Changeset.cast/3 is deprecated, please pass an empty map or :invalid instead
    (rumbl) web/models/user.ex:15: Rumbl.User.changeset/2
    (rumbl) web/controllers/user_controller.ex:16: Rumbl.UserController.new/2
    (rumbl) web/controllers/user_controller.ex:1: Rumbl.UserController.action/2
    (rumbl) web/controllers/user_controller.ex:1: Rumbl.UserController.phoenix_controller_pipeline/2

Warningの解消

Ecto とかのバージョの違いのせいか2つWarningが出てました。1つは user.exchangeset/2 関数のデフォルト引数で :empty としていた部分です。 その部分を以下のように変えます。

def changeset(model, params \\ %{}) do

単純に空の Map にしただけですね。 もう1つ cast/4 関数を呼び出している部分でもWarningが出ているので修正します。

model
|> cast(params, [:name, :username])
|> validate_required([:name, :username])
|> validate_length(:username, min: 1, max: 20)

ここ とか ここらへん 参考にしましたが英語力の無さ故にあってるかわからないです。誰か教えて!! パラメータの名前的にはあってそうですが・・・ また、これを見ると castchangeset を返してきて、それに対してバリデーションを掛けているのがわかります。

Createアクションの実装

new アクションを実装したので実際にDBにインサートする create アクションを実装します。

def create(conn, %{"user" => user_params}) do
  changeset = User.changeset(%User{}, user_params)
  case Repo.insert(changeset) do
    {:ok, user} ->
      conn
      |> put_flash(:info, "#{user.name} created!")
      |> redirect(to: user_path(conn, :index))
    {:error, changeset} ->
      render(conn, "new.html", changeset: changeset)
  end
end

あんまり説明することはないですが、 conn からのパイプラインで作成後の template 用の処理を読んでる点くらいでしょうか。パイプラインが最大の特徴かもしれませんが・・・ また、 new.html.eex もエラーを表示するように変えます。

<h1>New User</h1>
<%= if @changeset.action do %>
  <div class="alert alert-danger">
    <p>Oops, something went wrong! Please check the errors below.</p>
  </div>
<% end %>

<%= form_for @changeset, user_path(@conn, :create), fn f -> %>
  <div class="form-group">
    <%= text_input f, :name, placeholder: "Name", class: "form-control" %>
    <%= error_tag f, :name %>
  </div>
  <div class="form-group">
    <%= text_input f, :username, placeholder: "Username", class: "form-control" %>
    <%= error_tag f, :username %>
  </div>
  <div class="form-group">
    <%= password_input f, :password, placeholder: "Password", class: "form-control" %>
    <%= error_tag f, :password %>
  </div>
  <%= submit "Create User", class: "btn, btn-primary" %>
<% end %>

error_tag/2 関数は viewerror_helpers.ex に定義されている関数です。

Changesetについて

このchapterの最後に changeset について触れられています。

iex(1)> changeset = Rumbl.User.changeset(%Rumbl.User{username: "eric"})
#Ecto.Changeset<action: nil, changes: %{},
 errors: [name: {"can't be blank", [validation: :required]}],
 data: #Rumbl.User<>, valid?: false>
iex(2)> changeset
#Ecto.Changeset<action: nil, changes: %{},
 errors: [name: {"can't be blank", [validation: :required]}],
 data: #Rumbl.User<>, valid?: false>
iex(3)> import Ecto.Changeset
Ecto.Changeset
iex(4)> changeset.changes
%{}
iex(5)> changeset = put_change(changeset, :username, "ericmj")
#Ecto.Changeset<action: nil, changes: %{username: "ericmj"},
 errors: [name: {"can't be blank", [validation: :required]}],
 data: #Rumbl.User<>, valid?: false>
iex(6)> changeset.changes
%{username: "ericmj"}
iex(7)> get_change(changeset, :username)
"ericmj"

これを見ると changeset はバリデーション以外にも変更をの追跡と保持を行っていることがわかります。

まとめ

簡単なDB操作を行いました。今回はテンプレート周りはおまけだったように思います。 何かしらフレームワーク触ったことあればそんなに違和感はなく使えると思います。 ずっと changeset が謎だったんですが、少し理解できたと思います。 基本的なところは結構網羅されてきたんじゃないかと思いますのでサクサク行きたいです。