技術メモ

プログラミングとか電子工作とか

Phoenix入門 (第13章 Contexts その1)

f:id:ysmn_deus:20190219130922p:plain

どうも、靖宗です。
最近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にアクセスします。

f:id:ysmn_deus:20190331114745p:plain

入力フォームまで既に用意されています。

f:id:ysmn_deus:20190331114832p:plain

なんでもいいけどとっとと実装したいときとかめっちゃ便利そうですね。
バリデーションやエラー表示までデフォルトでやってくれます。

f:id:ysmn_deus:20190331114946p:plain

ちなみに成功する表示もバッチリです。

f:id:ysmn_deus:20190331115220p:plain

f:id:ysmn_deus:20190331115313p:plain
ちゃんとリストページにも追加されてる

リストページからは編集や削除もできます。ありがたや。
デフォルトで生成されるCRUDAPIとしてのベストプラクティスみたいなものという認識でも結構有り難い機能です。

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を通して行うという方針を採るようです。
このへんはローカルルールみたいなもんなんで指針だけ把握してれば良いんじゃ無いかと思います。

今回はこの辺で!