どうも、靖宗です。
引き続きContextsです。この分量だと3回に別れるかな・・・
In-context Relationships
Relationships
ということはようやくテーブルの関連付けなどでしょうか。
前回までに作成したAccount
のuser
ですが、メールアドレスなど登録情報が何もありません。(パスワードはありますが・・・)
そこで、ログイン用のメールアドレスを追加するのですが、これをuser
テーブルに追加してしまうと外部認証(Googleアカウントとかでログインするやつ)に対応しようとすると大変なことになります。
なのでアカウント情報にメールアドレスのデータを紐付けて管理することにします。
Contextに追加する
そこでまたContextを作成していくのですが、前回作成したAccount
にContextを追加していくようなことができるそうです。
phx.gen.html
のようなコマンド、mix phx.gen.context
で追加していきます。
PS \hello> mix phx.gen.context Accounts Credential credentials email:string:unique user_id:references:users You are generating into an existing context. The Hello.Accounts context currently has 6 functions and 1 files in its directory. * It's OK to have multiple resources in the same context as long as they are closely related * If they are not closely related, another context probably works better If you are not sure, prefer creating a new context over adding to the existing one. Would you like to proceed? [Yn] Y * creating lib/hello/accounts/credential.ex * creating priv/repo/migrations/20190401004125_create_credentials.exs * injecting lib/hello/accounts.ex * injecting test/hello/accounts/accounts_test.exs Remember to update your repository by running migrations: $ mix ecto.migrate
今回はウェブ上でのCRUD操作は要らないのでmix phx.gen.context
で追加したようです。
(userのviewなどに機能を追加していく)
* injecting
でlib/hello/accounts.ex
ファイルに追記されているのが分かります。
参照先が削除されたときの振る舞い(ON DELETE)
Credentialの情報(今回はメールアドレスだけ)はユーザーIDと1対1で関連付いているので、usersのデータが消去されたときには消えて欲しいです。
なのでその情報をmigrationsのテーブル追加スクリプトに追記します。
* creating priv/repo/migrations/20190401004125_create_credentials.exs
を編集します(環境によってタイムスタンプは違う)。
defmodule Hello.Repo.Migrations.CreateCredentials do use Ecto.Migration def change do create table(:credentials) do add :email, :string add :user_id, references(:users, on_delete: :delete_all), null: false # :nothing を :delete_all に timestamps() end create unique_index(:credentials, [:email]) create index(:credentials, [:user_id]) end end
, null: false
も追記されてるのは、ユーザーと紐付いてない謎の認証情報が発生しないようにということでしょう。
これで良さそうなのでマイグレーションします。
PS \hello> mix ecto.migrate Compiling 2 files (.ex) Generated hello app [info] == Running 20190401004125 Hello.Repo.Migrations.CreateCredentials.change/0 forward [info] create table credentials [info] create index credentials_email_index [info] create index credentials_user_id_index [info] == Migrated 20190401004125 in 0.0s
これでデータベース上のテーブルなどは良さそうです。
Contextで扱うスキーマの関係性
次にPhoenix側での関係性も記載しておきます。Contextのlib/hello/accounts/user.ex
に対応関係を追記します。
defmodule Hello.Accounts.User do use Ecto.Schema import Ecto.Changeset alias Hello.Accounts.Credential # 忘れないで(戒め) schema "users" do field :name, :string field :username, :string has_one :credential, Credential # 追加 timestamps() end ...
has_one
マクロはEctoの機能だそうで、他のスキーマとの関連性をEcto側で使える様にするものだそうです。
同様にcredential.ex
の方にも関連性を記載します。
defmodule Hello.Accounts.Credential do use Ecto.Schema import Ecto.Changeset alias Hello.Accounts.User # 忘れないで(戒め) schema "credentials" do field :email, :string belongs_to :user, User # 通常のフィールドからbelongs_toに変更 timestamps() end ...
belongs_to
マクロは先ほど同様Ectoのマクロで、User
に対して従属していることをEctoに知らせます。
この辺自動でやってくれないかなぁ。
preloadの設定
お次はgetなどの取得時にCredentialのデータを取っておくように書いておく設定をContextに書いておきます。
lib/hello/accounts.ex
を編集します。
... def list_users do User |> Repo.all() |> Repo.preload(:credential) end ... def get_user!(id) do User |> Repo.get!(id) |> Repo.preload(:credential) end ...
Repo.preload(:credential)
を入れておくことで%Accounts.User{}
の構造体の中にデータが載るようです。
あとなんか効率的にデータベースのデータが取得できる?かもしれないです。この辺はちょっとよく分かりませんでしたが、関連付いてて、十中八九使用するデータはpreloadしておくのが吉かもしれません。
入力のテンプレートに入力フォームを追加
ここまできてようやくウェブ上の方に機能を追加していきます。
まずは入力フォームにemailを記入できるようにlib/hello_web/templates/user/form.html.eex
を編集します。
... <div class="form-group"> <%= inputs_for f, :credential, fn cf -> %> <%= label cf, :email %> <%= text_input cf, :email %> <%= error_tag cf, :email %> <% end %> </div> <div> <%= submit "Save" %> </div> <% end %>
分かりにくいですが、基本的に<%= ○○ %>
を除けばElixirのコードです。
inputs_for
関数?はf
(ここではUserのフォーム)に関連付いた情報で:credential
の入力を作成する、という構文のようです。fn cf ->
以降は他の入力フォームと同じです。
一応ソースに直すのであれば
inputs_for f, :credential, fn cf -> label cf, :email text_input cf, :email error_tag cf, :email end
のようになってる感じです。各関数?マクロ?は入力のタグを追加するってところでしょうか。
出力のテンプレートに表示を追加
あとは表示を追加しておきましょう。
lib/hello_web/templates/user/show.html.eex
を編集します。
<h1>Show User</h1> <ul> <li> <strong>Name:</strong> <%= @user.name %> </li> <li> <strong>Username:</strong> <%= @user.username %> </li> <li> <strong>Email:</strong> <%= @user.credential.email %> </li> ...
このへんは楽勝ですね。
この時点でPhoenixは機能すると思います。一応mix phx.server
でhttp://localhost:4000/users/new
にアクセスしてみます。
('ω')。o(????????????)
UndefinedFunctionError at GET /users/new function Credential.__struct__/0 is undefined (module Credential is not available)
Credential
が使えない、なんでや。
ちょっとソースを見直した結果、Contextのそれぞれ(lib/hello/accounts/user.ex
とcredential.ex
)にエイリアスを書くのを忘れていた。アホス。
追記して再度アクセス。
ヨシ!
ContextのCreateとUpdateを変更(関連データのバリデーション)
とはいっても、現段階ではただ表示されているだけで実際に保存しようとすると
UndefinedFunctionError at GET /users/2 function nil.email/0 is undefined
になると思います。これはContextのupdate_user
などが古いままなのでメール情報を格納するステップが抜けてるので、「メール情報ないやんけ!」と怒られてるのでしょう。
この辺を追加します。Contextのlib/hello/accounts/accounts.ex
を編集します。
defmodule Hello.Accounts do @moduledoc """ The Accounts context. """ import Ecto.Query, warn: false alias Hello.Repo alias Hello.Accounts.{User, Credential} # Accountsの後ろに.をつけ忘れてた ... def create_user(attrs \\ %{}) do %User{} |> User.changeset(attrs) |> Ecto.Changeset.cast_assoc(:credential, with: &Credential.changeset/2) |> Repo.insert() end ... def update_user(%User{} = user, attrs) do user |> User.changeset(attrs) |> Ecto.Changeset.cast_assoc(:credential, with: &Credential.changeset/2) |> Repo.update() end ... # alias Hello.Accounts.Credential # 消してもヨシ、上で呼んでるので冗長 ...
Repo.insert
などする前のパイプラインにEcto.Changeset.cast_assoc(:credential, with: &Credential.changeset/2)
を追加しました。
Ecto.Changeset.cast_assoc/3
はどうやら第一引数に大元の関連付いた構造体(今回だとユーザーの構造体)、第二引数に関連データを示すアトム(今回だと:credential
、たぶんユーザー構造体に追加されたキーとして読み込んでる)、第三引数に関連データのchangeset
を渡してバリデーションするといった流れでしょう。
これで行けるそうです。やってみます。
SyntaxError lib/hello/accounts.ex:9: syntax error before: '{'
(╬´◓ω◔╬)
凡ミスです。
alias Hello.Accounts{User, Credential}となっていたので
alias Hello.Accounts.{User, Credential}`に修正。
行けました。メールアドレスをブランクで保存しようとしても、きちんとバリデーションではじかれます。
もっときっちりするならCredentialでメールアドレスの形式でバリデーションしてもいいかもしれません。
もう一項目行きたかったんですがちょっと長くなってるのでこの辺で区切ります。