Phoenix 1.3のrc-2が先月リリースされそろそろPhoenix 1.3にアプデも考えだしてきた今日この頃、まだアップデートはしないけど1.3正式版がリリースされることに備えてディレクトリ構造とか学んでおきたい
ということでざっと慣れるためにPhoenix 1.3を触ってみます
What's changed in Phoenix 1.3
Phoenix 1.3での主な変更点は以下です
-
web
ディレクトリがlib/<project>/web
以下になった - デフォルトのジェネレータにContextという概念が追加されアプリケーション境界を分割出来るようになった
- Fallback Action Plugが追加された
- Controllerの構成がシンプルに出来るようになった
ディレクトリ構造
まず大きく変わった点としてデフォルトのディレクトリ構造が変わったということもあるので、phoenixプロジェクトを一から作成してみます
プロジェクト作成のコマンドの名前がphoenix.new
からphx.new
に変更になり1.2と1.3で意識的に変えたことが分かりますね
$ mix phx.new my_app
* creating my_app/config/config.exs
* creating my_app/config/dev.exs
* creating my_app/config/prod.exs
* creating my_app/config/prod.secret.exs
* creating my_app/config/test.exs
* creating my_app/lib/my_app/application.ex
* creating my_app/lib/my_app/web/channels/user_socket.ex
* creating my_app/lib/my_app/web/views/error_helpers.ex
* creating my_app/lib/my_app/web/views/error_view.ex
* creating my_app/lib/my_app/web/endpoint.ex
* creating my_app/lib/my_app/web/router.ex
* creating my_app/lib/my_app/web/web.ex
* creating my_app/mix.exs
* creating my_app/README.md
* creating my_app/test/support/channel_case.ex
* creating my_app/test/support/conn_case.ex
* creating my_app/test/test_helper.exs
* creating my_app/test/my_app/web/views/error_view_test.exs
* creating my_app/lib/my_app/web/gettext.ex
* creating my_app/priv/gettext/en/LC_MESSAGES/errors.po
* creating my_app/priv/gettext/errors.pot
* creating my_app/lib/my_app/repo.ex
* creating my_app/priv/repo/seeds.exs
* creating my_app/test/support/data_case.ex
* creating my_app/lib/my_app/web/controllers/page_controller.ex
* creating my_app/lib/my_app/web/templates/layout/app.html.eex
* creating my_app/lib/my_app/web/templates/page/index.html.eex
* creating my_app/lib/my_app/web/views/layout_view.ex
* creating my_app/lib/my_app/web/views/page_view.ex
* creating my_app/test/my_app/web/controllers/page_controller_test.exs
* creating my_app/test/my_app/web/views/layout_view_test.exs
* creating my_app/test/my_app/web/views/page_view_test.exs
* creating my_app/.gitignore
* creating my_app/assets/brunch-config.js
* creating my_app/assets/css/app.css
* creating my_app/assets/css/phoenix.css
* creating my_app/assets/js/app.js
* creating my_app/assets/js/socket.js
* creating my_app/assets/package.json
* creating my_app/assets/static/robots.txt
* creating my_app/assets/static/images/phoenix.png
* creating my_app/assets/static/favicon.ico
Fetch and install dependencies? [Yn] n
We are almost there! The following steps are missing:
$ cd my_app
$ mix deps.get
$ cd assets && npm install && node node_modules/brunch/bin/brunch build
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
構造を確認してみます
$ tree
.
├── assets
│ ├── brunch-config.js
│ ├── css
│ │ ├── app.css
│ │ └── phoenix.css
│ ├── js
│ │ ├── app.js
│ │ └── socket.js
│ ├── package.json
│ ├── static
│ │ ├── favicon.ico
│ │ ├── images
│ │ │ └── phoenix.png
│ │ └── robots.txt
│ └── vendor
├── config
│ ├── config.exs
│ ├── dev.exs
│ ├── prod.exs
│ ├── prod.secret.exs
│ └── test.exs
├── lib
│ └── my_app
│ ├── application.ex
│ ├── repo.ex
│ └── web
│ ├── channels
│ │ └── user_socket.ex
│ ├── controllers
│ │ └── page_controller.ex
│ ├── endpoint.ex
│ ├── gettext.ex
│ ├── router.ex
│ ├── templates
│ │ ├── layout
│ │ │ └── app.html.eex
│ │ └── page
│ │ └── index.html.eex
│ ├── views
│ │ ├── error_helpers.ex
│ │ ├── error_view.ex
│ │ ├── layout_view.ex
│ │ └── page_view.ex
│ └── web.ex
├── mix.exs
├── priv
│ ├── gettext
│ │ ├── en
│ │ │ └── LC_MESSAGES
│ │ │ └── errors.po
│ │ └── errors.pot
│ └── repo
│ ├── migrations
│ └── seeds.exs
├── README.md
└── test
├── my_app
│ └── web
│ ├── channels
│ ├── controllers
│ │ └── page_controller_test.exs
│ └── views
│ ├── error_view_test.exs
│ ├── layout_view_test.exs
│ └── page_view_test.exs
├── support
│ ├── channel_case.ex
│ ├── conn_case.ex
│ └── data_case.ex
└── test_helper.exs
29 directories, 41 files
まず大きな点として1.2でルートにあったweb
がlib
以下にまとめられたことで通常のElixirライブラリと同じような構成になっています。
1.2ではRailsライクなフレームワークという側面が強かったですが1.3になってElixirライブラリ標準を意識してきた感がありますね。ちなみにPhoenix 1.2でなぜweb
というディレクトリなのかというと、技術的な制限からそうなったそうです。
Context
Phoenix 1.3からはデフォルトのジェネレータにmix phx.gen.context
が追加され、Contextという概念が持ち込まれました。
DDDとかやられている方にはお馴染みの概念かもしれませんが簡単に説明するとDDDではある一定のドメイン知識のまとまりを表すのにコンテキストという言葉が使われています。
Elixir ForumでもDDDのContextの概念を持ち込むのは新規参入者の壁になるのでは?という議論もされていたContextですが実際使ってみた感じ必ずしもDDDと同じように考える必要はなく、どちらかというとカジュアルにアプリケーションの境界を考える手段として提供されているものだと思いました。
個人的にもDDDに関しては「りろんはしってる」レベルなので最初はガチガチなDDD来たらどうしようと身構えていたけど実際使ってみるとなるほどという感想になりました。
ここは実際にphoenixジェネレータのContext関係のコマンドで出力されるファイルを見た方が理解が早いと思うので見てみましょう
$ mix phx.gen.context Accounts User users name:string
* creating ./lib/my_app/accounts/user.ex
* creating priv/repo/migrations/20170625042439_create_accounts_user.exs
* creating ./lib/my_app/accounts/accounts.ex
* injecting ./lib/my_app/accounts/accounts.ex
* creating ./test/my_app/accounts/accounts_test.exs
* injecting ./test/my_app/accounts/accounts_test.exs
Remember to update your repository by running migrations:
$ mix ecto.migrate
phx.gen.context
に第一引数でコンテキスト名、第二第三でコンテキスト内に生成するモデルのsingularとpluralな名前、最後に任意の数のフィールド名とフィールド型のペアを渡しています。
結果、生成されたファイルを見ると第一引数のコンテキスト名(accounts
)でPhoenixプロジェクト内にディレクトリが作られ、その中にスキーマ(user.ex
)及びコンテキスト(accounts.ex
)が作られていることが分かります。
スキーマは以下のような感じ。必要最低限ですね。
$ cat user.ex
defmodule MyApp.Accounts.User do
use Ecto.Schema
import Ecto.Changeset
alias DevApp.Accounts.User
schema "accounts_users" do
field :name, :string
timestamps()
end
@doc false
def changeset(%User{} = user, attrs) do
user
|> cast(attrs, [:name])
|> validate_required([:name])
end
end
accounts.ex
としてコンテキスト名のファイルが作られています
$ cat accounts.ex
defmodule MyApp.Accounts do
@moduledoc """
The boundary for the Accounts system.
"""
import Ecto.Query, warn: false
alias MyApp.Repo
alias MyApp.Accounts.User
@doc """
Returns the list of users.
## Examples
iex> list_users()
[%User{}, ...]
"""
def list_users do
Repo.all(User)
end
@doc """
Gets a single user.
Raises `Ecto.NoResultsError` if the User does not exist.
## Examples
iex> get_user!(123)
%User{}
iex> get_user!(456)
** (Ecto.NoResultsError)
"""
def get_user!(id), do: Repo.get!(User, id)
@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
@doc """
Updates a user.
## Examples
iex> update_user(user, %{field: new_value})
{:ok, %User{}}
iex> update_user(user, %{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def update_user(%User{} = user, attrs) do
user
|> User.changeset(attrs)
|> Repo.update()
end
@doc """
Deletes a User.
## Examples
iex> delete_user(user)
{:ok, %User{}}
iex> delete_user(user)
{:error, %Ecto.Changeset{}}
"""
def delete_user(%User{} = user) do
Repo.delete(user)
end
@doc """
Returns an `%Ecto.Changeset{}` for tracking user changes.
## Examples
iex> change_user(user)
%Ecto.Changeset{source: %User{}}
"""
def change_user(%User{} = user) do
User.changeset(user, %{})
end
end
The boundary for the Accounts system.
というmoduledocから見る通りアカウントシステムというバウンダリを提供するモジュールという位置づけなのが分かります。
各コンテキスト内にスキーマを定義出来るので例えばアカウントシステムとバックエンドシステムでモデルを切り変えたい場合もコンテキストを切り替えるだけでいいことが分かります。
実際の例としてはhex.pmのリポジトリが既にPhoenix 1.3に切り替わっていて運用されているので例として参考になると思います。
Difference between DDD's Context
ちなみにPhoenix作者のChris McCordは説明のためにDDDを引用しましたがDDDのContextとは違うものだとJoséは説明しているので、DDDのコンテキストはこうだからといってPhoenixのContextもこうあるべきだという議論は恐らく生産的ではないのでやめておいた方がよさそうです。
Cross context relationship
コンテキストを超えてリレーションが必要になるケース、例えばSalesコンテキストのOrderモデルがAccountsコンテキストのUserモデルにN:1のリレーションを貼りたいというようなケースはどうすればいいのかということがForumでも議論されていましたが、こういう場合はそもそもなぜコンテキストを超える必要があるのかを考える必要があります。
Salesコンテキスト内にUserモデルを定義して同じテーブルを参照することはphoenixでは可能なのでそうすれば解決します。冗長なコードになると考えるかもしれませんが、コンテキストが正しく分割されていればそのモデルで表される知識は別の知識になるので正しいでしょう。逆にコンテキストの分割が間違っていたら同じドメイン知識が複数の箇所で表現されるので抽象化には失敗するでしょう。
将来的にそのコンテキストだけ切り出してUmbrella Appにしたい場合もコンテキストが正しく分割されていればそこまで難しくないし、逆にコンテキストが正しく分割されていなければ切り出すのが難しくなるのでリファクタリングが必要になるはずです。
コンテキストという概念を導入することでよくなるのはたぶんこういうアプリケーションの境界について考える機会が増えるということなのだと思います。
オレオレ抽象化に頼らず正しいバウンダリが設定されているか考えることでよりよい抽象化に繋がるという、DDDの概念のいいとこ取りみたいな感じですがこういう節操なく他のツールのいいとこどりをするのもPhoenixのいいところだと思うので個人的には支持したいです。
オレオレコンテキストが増えそうなのでしばらくはCoC(Convention over Configuration)にのっとってなるべくジェネレータ経由で…
Umbrella
Elixirデフォルトで提供されているUmbrella Project(傘のように一つのアプリ内に複数のアプリを配置するプロジェクト構成)での抽象化ではプロジェクトをサブモジュールとして扱えるのでmix.exsのDependencyを分割出来たり各サブモジュール単位でリリースを行えるのでより複雑なシステムでは有効になりそうですが、プロジェクト初期には少しオーバースペックでOver Optimizeになるんじゃないか?と手を出しづらいところがありました。
Contextはそれよりはソフトな抽象化でモノリシックなプロジェクト内でコンテキストの切り替えを行うことでUmbrella Projectで行っていたアプリ単位での切り替えをコンテキストの切り替えで簡易的に出来るようにしたという印象です。
Phoenix 1.3ではphx.new
に--umbrella
オプションが追加され、Umbrella構成でPhoenixプロジェクトを作成することも出来ます。
$ mix phx.new my_app --umbrella
* creating my_app_umbrella/.gitignore
* creating my_app_umbrella/config/config.exs
* creating my_app_umbrella/config/dev.exs
* creating my_app_umbrella/config/test.exs
* creating my_app_umbrella/config/prod.exs
* creating my_app_umbrella/mix.exs
* creating my_app_umbrella/README.md
* creating my_app_umbrella/apps/my_app_web/config/config.exs
* creating my_app_umbrella/apps/my_app_web/config/dev.exs
* creating my_app_umbrella/apps/my_app_web/config/prod.exs
* creating my_app_umbrella/apps/my_app_web/config/prod.secret.exs
* creating my_app_umbrella/apps/my_app_web/config/test.exs
* creating my_app_umbrella/apps/my_app_web/lib/my_app_web.ex
* creating my_app_umbrella/apps/my_app_web/lib/my_app_web/application.ex
* creating my_app_umbrella/apps/my_app_web/lib/my_app_web/channels/user_socket.ex
* creating my_app_umbrella/apps/my_app_web/lib/my_app_web/endpoint.ex
* creating my_app_umbrella/apps/my_app_web/lib/my_app_web/router.ex
* creating my_app_umbrella/apps/my_app_web/lib/my_app_web/views/error_helpers.ex
* creating my_app_umbrella/apps/my_app_web/lib/my_app_web/views/error_view.ex
* creating my_app_umbrella/apps/my_app_web/mix.exs
* creating my_app_umbrella/apps/my_app_web/README.md
* creating my_app_umbrella/apps/my_app_web/test/test_helper.exs
* creating my_app_umbrella/apps/my_app_web/test/support/channel_case.ex
* creating my_app_umbrella/apps/my_app_web/test/support/conn_case.ex
* creating my_app_umbrella/apps/my_app_web/test/my_app_web/views/error_view_test.exs
* creating my_app_umbrella/apps/my_app_web/lib/my_app_web/gettext.ex
* creating my_app_umbrella/apps/my_app_web/priv/gettext/en/LC_MESSAGES/errors.po
* creating my_app_umbrella/apps/my_app_web/priv/gettext/errors.pot
* creating my_app_umbrella/apps/my_app_web/lib/my_app_web/controllers/page_controller.ex
* creating my_app_umbrella/apps/my_app_web/lib/my_app_web/templates/layout/app.html.eex
* creating my_app_umbrella/apps/my_app_web/lib/my_app_web/templates/page/index.html.eex
* creating my_app_umbrella/apps/my_app_web/lib/my_app_web/views/layout_view.ex
* creating my_app_umbrella/apps/my_app_web/lib/my_app_web/views/page_view.ex
* creating my_app_umbrella/apps/my_app_web/test/my_app_web/controllers/page_controller_test.exs
* creating my_app_umbrella/apps/my_app_web/test/my_app_web/views/layout_view_test.exs
* creating my_app_umbrella/apps/my_app_web/test/my_app_web/views/page_view_test.exs
* creating my_app_umbrella/apps/my_app_web/.gitignore
* creating my_app_umbrella/apps/my_app_web/assets/brunch-config.js
* creating my_app_umbrella/apps/my_app_web/assets/css/app.css
* creating my_app_umbrella/apps/my_app_web/assets/css/phoenix.css
* creating my_app_umbrella/apps/my_app_web/assets/js/app.js
* creating my_app_umbrella/apps/my_app_web/assets/js/socket.js
* creating my_app_umbrella/apps/my_app_web/assets/package.json
* creating my_app_umbrella/apps/my_app_web/assets/static/robots.txt
* creating my_app_umbrella/apps/my_app_web/assets/static/images/phoenix.png
* creating my_app_umbrella/apps/my_app_web/assets/static/favicon.ico
* creating my_app_umbrella/apps/my_app/config/config.exs
* creating my_app_umbrella/apps/my_app/config/dev.exs
* creating my_app_umbrella/apps/my_app/config/prod.exs
* creating my_app_umbrella/apps/my_app/config/prod.secret.exs
* creating my_app_umbrella/apps/my_app/config/test.exs
* creating my_app_umbrella/apps/my_app/lib/my_app/application.ex
* creating my_app_umbrella/apps/my_app/test/test_helper.exs
* creating my_app_umbrella/apps/my_app/README.md
* creating my_app_umbrella/apps/my_app/mix.exs
* creating my_app_umbrella/apps/my_app/.gitignore
* creating my_app_umbrella/apps/my_app/lib/my_app/repo.ex
* creating my_app_umbrella/apps/my_app/priv/repo/seeds.exs
* creating my_app_umbrella/apps/my_app/test/support/data_case.ex
Fetch and install dependencies? [Yn] n
We are almost there! The following steps are missing:
$ cd my_app_umbrella
$ mix deps.get
$ cd assets && npm install && node node_modules/brunch/bin/brunch build
Then configure your database in apps/my_app/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
ディレクトリツリーを見てみます
$ tree
.
├── apps
│ ├── my_app
│ │ ├── config
│ │ │ ├── config.exs
│ │ │ ├── dev.exs
│ │ │ ├── prod.exs
│ │ │ ├── prod.secret.exs
│ │ │ └── test.exs
│ │ ├── lib
│ │ │ └── my_app
│ │ │ ├── application.ex
│ │ │ └── repo.ex
│ │ ├── mix.exs
│ │ ├── priv
│ │ │ └── repo
│ │ │ ├── migrations
│ │ │ └── seeds.exs
│ │ ├── README.md
│ │ └── test
│ │ ├── support
│ │ │ └── data_case.ex
│ │ └── test_helper.exs
│ └── my_app_web
│ ├── assets
│ │ ├── brunch-config.js
│ │ ├── css
│ │ │ ├── app.css
│ │ │ └── phoenix.css
│ │ ├── js
│ │ │ ├── app.js
│ │ │ └── socket.js
│ │ ├── package.json
│ │ ├── static
│ │ │ ├── favicon.ico
│ │ │ ├── images
│ │ │ │ └── phoenix.png
│ │ │ └── robots.txt
│ │ └── vendor
│ ├── config
│ │ ├── config.exs
│ │ ├── dev.exs
│ │ ├── prod.exs
│ │ ├── prod.secret.exs
│ │ └── test.exs
│ ├── lib
│ │ ├── my_app_web
│ │ │ ├── application.ex
│ │ │ ├── channels
│ │ │ │ └── user_socket.ex
│ │ │ ├── controllers
│ │ │ │ └── page_controller.ex
│ │ │ ├── endpoint.ex
│ │ │ ├── gettext.ex
│ │ │ ├── router.ex
│ │ │ ├── templates
│ │ │ │ ├── layout
│ │ │ │ │ └── app.html.eex
│ │ │ │ └── page
│ │ │ │ └── index.html.eex
│ │ │ └── views
│ │ │ ├── error_helpers.ex
│ │ │ ├── error_view.ex
│ │ │ ├── layout_view.ex
│ │ │ └── page_view.ex
│ │ └── my_app_web.ex
│ ├── mix.exs
│ ├── priv
│ │ └── gettext
│ │ ├── en
│ │ │ └── LC_MESSAGES
│ │ │ └── errors.po
│ │ └── errors.pot
│ ├── README.md
│ └── test
│ ├── my_app_web
│ │ ├── channels
│ │ ├── controllers
│ │ │ └── page_controller_test.exs
│ │ └── views
│ │ ├── error_view_test.exs
│ │ ├── layout_view_test.exs
│ │ └── page_view_test.exs
│ ├── support
│ │ ├── channel_case.ex
│ │ └── conn_case.ex
│ └── test_helper.exs
├── config
│ ├── config.exs
│ ├── dev.exs
│ ├── prod.exs
│ └── test.exs
├── mix.exs
└── README.md
37 directories, 56 files
出力されたファイルを見てみるとルートディレクトリにapps
ディレクトリが作成されその中にUmbrella傘下内の一アプリとしてphoenixプロジェクトが作成されているのが分かります。
デフォルトでは<project>
と<project>_web
ディレクトリがapps
内に作成され、<project>
内にはEctoの依存のみ存在しDBのインターフェースになっていることが分かります。また<project>_web
ディレクトリは--no-ecto
オプションで作成した時のようにEctoがdependencyに存在せず素のphoenixプロジェクトになっているのが分かります。
実際にこうしろという規則はありませんがUmbrella Projectのお手本みたいな構成なのでPhoenixプロジェクトが成長してきてUmbrella化したいような場合は参考になるかもしれません。
Umbrellaをいつ使うかについてはPhoenix作者のChris McCordがLonestar Elixir Confで話していますが、必ずしも全てに当てはまるわけではないけどContextがそれ専用のストレージを持つ場合はUmbrella、それ以外なら同一App内にすると考えている、と言っています。
Microservicesを念頭においているのかもしれませんが、Contextで適切に分割出来ていればUmbrella化からのMicroservices化への道も想像出来るのでモノリス時からContextを使うことは有用だと思います。
まとめ
Phoenix 1.3でのディレクトリ構造やContext、Umbrella Projectについて確認しました。
Phoenix 1.2まではRailsライクな構成が新規参入のしやすさに一役買っていたところもあると思いますがOOでないElixirにそうなる必然性はないといえばないので、Umbrellaプロジェクトのオーバースペック感も否めなかったところにPhoenix 1.3のContextという概念は調度いい落とし所を作ったなという印象です。
※2017-06-27現在Phoenix 1.3-rc2で正式版はリリースされていないのでこの記事の内容は変わる可能性があります