Phoenix入門 (第13章 Contexts その1)
どうも、靖宗です。
最近AWSばっかり触っててご無沙汰でしたが、若干落ち着きそうなのでPhoenixの学習を再開していきます。
今回はContextsということですが、これもまた結構長そうな項目です。
Contexts
この章(機能?)いままでの機能を組み合わせるにはどうするか、などでしょうか。
確かに各々の機能にだけ注目してもなかなか実装できないことってありますよね。
Thinking about design
Elixirの標準ライブラリを利用する際に、内部実装を気にする必要は特にありません。Logger.info/1
などは一度使い方を学べば特に内部で○○モジュールが動いてて・・・などと気にする人はそこまでいないと思います。(場合によっては必要だと思いますが)
Contextを利用することでPhoenixプロジェクトもElixirのモジュールのように外部から利用できるようにする機能という感じでしょうか。
この章では簡単なCMSを作成していくことで、このContextを学習しましょう!という方針のようです。
Adding an Accounts Context
まずはアカウントの実装からやっていくようです。
どんなシステムでもアカウントの登録や編集などはあると思いますし、わりかしこの辺のデザインがシステム全体に大きな影響を及ぼします。
前回のEctoの章で使ったプロジェクトを利用していくようですが、自分は新しく作り直します(作業してたプロジェクトがどっかいった)。
前回はユーザーのスキームを手動で作成しましたが、なにやらGeneratorsというもので生成するっぽいので、もし引き続きの人がいれば一旦消します。
$ rm lib/hello/user.ex $ rm priv/repo/migrations/*_create_users.exs
マイグレーションのファイルも消してるっぽいです。
データベースのリセットも行っています。
mix ecto.reset
自分は新しく作り直したのでmix ecto.create
まではいつも通りと同じです。
さて、ここまできてようやくContextを利用していくようです。GeneratorでアカウントのContextを作成していきます。
プロジェクトのルートフォルダでmix phx.gen.html
としていくと、どうやらCRUDのような機能まで実装してくれるっぽいです。
PS \hello> mix phx.gen.html Accounts User users name:string username:string:unique * creating lib/hello_web/controllers/user_controller.ex * creating lib/hello_web/templates/user/edit.html.eex * creating lib/hello_web/templates/user/form.html.eex * creating lib/hello_web/templates/user/index.html.eex * creating lib/hello_web/templates/user/new.html.eex * creating lib/hello_web/templates/user/show.html.eex * creating lib/hello_web/views/user_view.ex * creating test/hello_web/controllers/user_controller_test.exs * creating lib/hello/accounts/user.ex * creating priv/repo/migrations/20190331023135_create_users.exs * creating lib/hello/accounts.ex * injecting lib/hello/accounts.ex * creating test/hello/accounts/accounts_test.exs * injecting test/hello/accounts/accounts_test.exs Add the resource to your browser scope in lib/hello_web/router.ex: resources "/users", UserController Remember to update your repository by running migrations: $ mix ecto.migrate
今まではlib/hello_web/
側のPhoenixのウェブ機能側にしか色々ファイルは自動生成されませんでしたが、lib/hello/
側にもファイルが生成されました。これがContextでしょうか。
lib/hello/accounts/user.ex
にスキーマの定義ファイルも生成されています。
とりあえずウェブ側の実装をしちゃいます。
lib/hello_web/router.ex
を編集します。
... scope "/", HelloWeb do pipe_through :browser get "/", PageController, :index resources "/users", UserController end ...
先ほどのmix phx.gen.html
でuser関連のファイルは生成されているので、大体これで終わっちゃいます。やべえ。
スキーマの追加などされているのでデータベースをマイグレーションして起動してみます。
PS \hello> mix ecto.migrate Compiling 6 files (.ex) Generated hello app [info] == Running 20190331023135 Hello.Repo.Migrations.CreateUsers.change/0 forward [info] create table users [info] create index users_username_index [info] == Migrated 20190331023135 in 0.0s PS \hello> mix phx.server
追加したページのhttp://localhost:4000/users
にアクセスします。
入力フォームまで既に用意されています。
なんでもいいけどとっとと実装したいときとかめっちゃ便利そうですね。
バリデーションやエラー表示までデフォルトでやってくれます。
ちなみに成功する表示もバッチリです。
リストページからは編集や削除もできます。ありがたや。
デフォルトで生成されるCRUDのAPIとしてのベストプラクティスみたいなものという認識でも結構有り難い機能です。
Starting With Generators
ただ、一応今回の目的はPhoenixプロジェクトのウェブ表示の機能以外でもユーザーの情報が取り扱えるようにContexを利用してみることです。
これじゃPhoenixのウェブ表示便利機能やんけ!と思うかもしれませんが、ここでuser_controller.ex
を見てみます。
defmodule HelloWeb.UserController do use HelloWeb, :controller alias Hello.Accounts alias Hello.Accounts.User def index(conn, _params) do users = Accounts.list_users() render(conn, "index.html", users: users) end def new(conn, _params) do changeset = Accounts.change_user(%User{}) render(conn, "new.html", changeset: changeset) end def create(conn, %{"user" => user_params}) do case Accounts.create_user(user_params) do {:ok, user} -> conn |> put_flash(:info, "User created successfully.") |> redirect(to: Routes.user_path(conn, :show, user)) {:error, %Ecto.Changeset{} = changeset} -> render(conn, "new.html", changeset: changeset) end end ... end
そうなのです、
alias Hello.Accounts alias Hello.Accounts.User
で、AccountsのContextを利用してコントローラが実装されていることが分かります。
これによって、ウェブ表示の機能とデータベース周りの機能が分離されていることが分かります。
仮にデータベースがPostgreSQLからMySQLに変わろうがPhoenix側ではなんら問題無いですし、なんなら設計次第ではテーブル構造が変わっても吸収できる実装も可能ということです。
次にAccount
のContextであるlib/hello/accounts.ex
を見ていきます。
defmodule Hello.Accounts do @moduledoc """ The Accounts context. """ import Ecto.Query, warn: false alias Hello.Repo alias Hello.Accounts.User @doc """ Returns the list of users. ## Examples iex> list_users() [%User{}, ...] """ def list_users do Repo.all(User) end ... end
非常にシンプルなモジュールです。Repoに対する操作をラップするようなモジュールになっています。
このContextに関数を追加することで、操作をカスタマイズしたり、新しい機能を実装したりして、様々な箇所でその機能を共有するのでしょう。この辺はわりとオブジェクト指向っぽい(カプセル化)ですが、機能のみの実装ですので似て非なる物です。
ここで、ユーザー作成の関数Accounts.create_user/1
を見てみます。
@doc """ Creates a user. ## Examples iex> create_user(%{field: value}) {:ok, %User{}} iex> create_user(%{field: bad_value}) {:error, %Ecto.Changeset{}} """ def create_user(attrs \\ %{}) do %User{} |> User.changeset(attrs) |> Repo.insert() end
(すっかり忘れてましたがcreate_user(attrs \\ %{})
の\\ %{}
はデフォルトの引数は%{}
やで!という事を意味してます。)
スキーマの作成や更新はchangeset
でやるというのは前回やりました。
changeset
の実装を見てみます。
defmodule Hello.Accounts.User do use Ecto.Schema import Ecto.Changeset alias Hello.Accounts.User schema "users" do field :name, :string field :username, :string timestamps() end @doc false def changeset(%User{} = user, attrs) do user |> cast(attrs, [:name, :username]) |> validate_required([:name, :username]) |> unique_constraint(:username) end end
前回はスルーしましたが、changeset
の前に@doc
属性がfalseになっています。
これはどうやらドキュメントにこの情報が載らないようにするための属性だそうで、「プライベートな(他のAPIからは参照されない)実装」というところを意味するそうです。
(実際に外部から使えなくなる訳では無い)
なので、別のモジュールからuserの作成や更新をするときは、必ずAccount
のContextを通して行うという方針を採るようです。
このへんはローカルルールみたいなもんなんで指針だけ把握してれば良いんじゃ無いかと思います。
今回はこの辺で!