技術メモ

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

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

f:id:ysmn_deus:20190219130922p:plain

どうも、靖宗です。
前回かなり中途半端なところで終わりましたが、気にせず続けます。

Cross-context dependencies

前回はCMSってContextを作ろう!というところで終わってました。なので作って行きます。

スキーム

今回は

  • title:string タイトル。文字列。
  • body:string 本文。文字列。
  • views:integer 閲覧数?数値。

というモデルになっているようです。

mix phx.gen.htmlでContext生成

PS \hello> mix phx.gen.html CMS Page pages title:string body:text views:integer --web CMS
* creating lib/hello_web/controllers/cms/page_controller.ex
* creating lib/hello_web/templates/cms/page/edit.html.eex
* creating lib/hello_web/templates/cms/page/form.html.eex
* creating lib/hello_web/templates/cms/page/index.html.eex
* creating lib/hello_web/templates/cms/page/new.html.eex
* creating lib/hello_web/templates/cms/page/show.html.eex
* creating lib/hello_web/views/cms/page_view.ex
* creating test/hello_web/controllers/cms/page_controller_test.exs
* creating lib/hello/cms/page.ex
* creating priv/repo/migrations/20190403003143_create_pages.exs
* creating lib/hello/cms.ex
* injecting lib/hello/cms.ex
* creating test/hello/cms/cms_test.exs
* injecting test/hello/cms/cms_test.exs

Add the resource to your CMS :browser scope in lib/hello_web/router.ex:

    scope "/cms", HelloWeb.CMS, as: :cms do
      pipe_through :browser
      ...
      resources "/pages", PageController
    end


Remember to update your repository by running migrations:

    $ mix ecto.migrate

復習にはなりますがmix phx.gen.html Context名 モデル名(コントローラとかで使われる) スキーム名 スキーム (--web 名前空間)でリソースに必要なファイルやContextが生成できます。
--web 名前空間のオプションですが、今回Pageという名前のモデル?モジュール名を利用しているので、そのまま使うとPageControllerとかが被ってしまいます。
そういうときにはこのオプションを利用するようです。(基本的にContext毎に分けた方がいいのでは?とは思いますが、まだ慣れてないので今はそういう物だと考えておきます。)
生成されているファイルがlib/hello_web/controllers/cms/page_controller.exとなっている所からも重複が回避されているのが分かります。

スキームに合わせたテンプレートの調整

viewsという項目はユーザーが直接編集するものではないのでフォームのビューからは消しておきます。lib/hello_web/templates/cms/page/form.html.eexviewsに該当する箇所を消去します。

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

  <%= label f, :title %>
  <%= text_input f, :title %>
  <%= error_tag f, :title %>

  <%= label f, :body %>
  <%= textarea f, :body %>
  <%= error_tag f, :body %>

  <div>
    <%= submit "Save" %>
  </div>
<% end %>

あっても動くとは思いますがドキュメントに従いましょう。

スキームに合わせたchangesetの変更

上記と同様の理由でスキームに付属するchangesetのバリデーションも変更します。
Contextで対応するのかな?と思っていましたがchangesetで落としてしまうようです。
lib/hello/cms/page.exを編集します。

...
  @doc false
  def changeset(page, attrs) do
    page
    |> cast(attrs, [:title, :body]) # :viewsを消去
    |> validate_required([:title, :body]) # :viewsを消去
  end
end

マイグレーションファイルの調整

priv/repo/migrationsの該当するファイル(mix phx.gen.htmlで生成されたやつ)を編集します。

defmodule Hello.Repo.Migrations.CreatePages do
  use Ecto.Migration

  def change do
    create table(:pages) do
      add :title, :string
      add :body, :text
      add :views, :integer, default: 0 # この箇所にデフォルト値を設定

      timestamps()
    end

  end
end

今回はスキームの変更によるものでは無いですが、おおよそスキームを変更した場合はマイグレーションファイルの調整が必要になるでしょう。

ルーティングの追加(router.ex)

CMSのContextを追加したのでPageのリソースが見れる様にします。
lib/hello_web/router.exを修正します。

...
  scope "/", HelloWeb do
    pipe_through :browser

    get "/", PageController, :index
    resources "/users", UserController
    resources "/sessions", SessionController, only: [:new, :create, :delete], singleton: true
  end

  scope "/cms", HelloWeb.CMS, as: :cms do
    pipe_through [:browser, :authenticate_user]

    resources "/pages", PageController
  end
...

scope "/", HelloWeb do句を真似て書けば問題無いと思います。
ただし、今回はCMSに関連するページは認証が必要なようにしたいので:authenticate_userプラグを適応するようpipe_through [:browser, :authenticate_user]となっています。

マイグレートと認証機能の確認

ここまで来ると機能は実装されてないものの、だいたいの表示はできる筈です。
mix ecto.migrateでデータベースをマイグレートしてサーバーを起動してみます。

PS \hello> mix ecto.migrate
Compiling 6 files (.ex)
Generated hello app
[info] == Running 20190403003143 Hello.Repo.Migrations.CreatePages.change/0 forward
[info] create table pages
[info] == Migrated 20190403003143 in 0.0s
PS \hello> mix phx.server

生成したPageのURLはhttp://localhost:4000/cms/pagesです。ここにアクセスしてみます。

f:id:ysmn_deus:20190403100232p:plain

ログインしていないのでput_flashで怒られています。
http://localhost:4000/sessions/newにて、前回までに作成したユーザーでログインしてみます。

f:id:ysmn_deus:20190403102543p:plain

ヨシ!
認証系の機能がここまで簡単に追加できるのは驚愕です。
Plugの拡張性の高さヤバイ。

Authorの追加

ではPageを追加・・・と行きたいところですが、作成者の情報を紐付けておかないと面倒なことになりそうなので先にそちらを済ませましょう。
emailの情報をAccountsのContextに追加したようにphx.gen.contextCMSのContextにAuthorの情報を追加していきます。

スキームはこんなかんじ。

  • bio:text 出身?text形?昔のバージョンによく見られるっぽいんですが、たぶん:stringと同義。生成されたスキームでは:stringになってた。
  • role:string 役割。文字列。
  • genre:string ジャンル。文字列。
  • user_id:references:users:unique ユーザーID、ユーザーの情報と関連している
PS \hello> mix phx.gen.context CMS Author authors bio:text role:string genre:string user_id:references:users:unique
You are generating into an existing context.
The Hello.CMS 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/cms/author.ex
* creating priv/repo/migrations/20190403013056_create_authors.exs
* injecting lib/hello/cms.ex
* injecting test/hello/cms/cms_test.exs

Remember to update your repository by running migrations:

    $ mix ecto.migrate

UserとAuthorのContextを分けておくことで開発上いろんな利点がありそうです。
Authorに別情報が必要になってCMSのContextを編集する際でもAccounts側のUserは変更不要ですしCMS側にAcountsのUserデータ以外が流出することもないです。

マイグレーションファイルの調整

Accountsで認証情報を追加したときのように、従属関係にある際はon_deletenull: falseを設定しておいた方が良いです。(不用意にデータが残ってしまう可能性がある。)
なのでpriv/repo/migrationsに先ほどのmix phx.gen.contextで生成されたマイグレーションファイルを調整します。

defmodule Hello.Repo.Migrations.CreateAuthors do
  use Ecto.Migration

  def change do
    create table(:authors) do
      add :bio, :text
      add :role, :string
      add :genre, :string
      add :user_id, references(:users, on_delete: :delete_all), null: false # ここを修正

      timestamps()
    end

    create unique_index(:authors, [:user_id])
  end
end

これでAuthorに関しては問題ありませんが、まだ従属関係はあります。
PageがAuthorに関連付いているので、この関連付けをデータベースに登録するためにマイグレーションファイルを作成します。
mix ecto.gen.migrationで任意のマイグレーションファイルを作成できるようです。

PS \hello> mix ecto.gen.migration add_author_id_to_pages
Compiling 2 files (.ex)
Generated hello app
* creating priv/repo/migrations/20190403015018_add_author_id_to_pages.exs

いつものようなタイムスタンプ+名前.exsが生成されました。
一応中身を見てみます。

defmodule Hello.Repo.Migrations.AddAuthorIdToPages do
  use Ecto.Migration

  def change do

  end
end

このchange関数の中に処理を記載してマイグレーションすれば良さそうです。
いつものマイグレーションファイルはcreate table(hogehoge)doなどが記載されています。

今回はドキュメントに従い、下記のようにしました。

defmodule Hello.Repo.Migrations.AddAuthorIdToPages do
  use Ecto.Migration

  def change do
    alter table(:pages) do
      add :author_id, references(:authors, on_delete: :delete_all), null: false
    end

    create index(:pages, [:author_id])
  end
end

create tableでテーブル作成ですが、alter tableでテーブルにカラムを追加できるようです。
add以降は割といつも通りでしょうか。
作成者が削除されたらページも削除されてしまうのはなんだか寂しい気がしますが、今回はCMSを作る練習と言うことでスルーします。

マイグレーションファイルの調整が完了すれば、やることは一つ。mix ecto.migrateマイグレーションします。

PS \hello> mix ecto.migrate
warning: variable "null" does not exist and is being expanded to "null()", please use parentheses to remove the ambiguity or change the variable name
  priv/repo/migrations/20190403013056_create_authors.exs:9

** (CompileError) priv/repo/migrations/20190403013056_create_authors.exs:9: undefined function add/4
    (elixir) src/elixir_locals.erl:107: :elixir_locals."-ensure_no_undefined_local/3-lc$^0/1-0-"/2
    (elixir) src/elixir_locals.erl:107: anonymous fn/3 in :elixir_locals.ensure_no_undefined_local/3
    (stdlib) erl_eval.erl:680: :erl_eval.do_apply/6
    (elixir) lib/code.ex:715: Code.load_file/2
    (ecto_sql) lib/ecto/migrator.ex:489: Ecto.Migrator.load_migration/1
    (elixir) lib/enum.ex:1327: Enum."-map/2-lists^map/1-0-"/2
    (ecto_sql) lib/ecto/migrator.ex:435: Ecto.Migrator.do_migrate/4
    (ecto_sql) lib/ecto/migrator.ex:429: Ecto.Migrator.migrate/4
    (ecto_sql) lib/ecto/adapters/sql.ex:820: anonymous fn/3 in Ecto.Adapters.SQL.checkout_or_transaction/4
    (db_connection) lib/db_connection.ex:1415: DBConnection.run_transaction/4
    (ecto_sql) lib/ecto/adapters/sql.ex:727: Ecto.Adapters.SQL.lock_for_migrations/5
    (ecto_sql) lib/ecto/migrator.ex:318: Ecto.Migrator.lock_for_migrations/3
    (ecto_sql) lib/mix/tasks/ecto.migrate.ex:110: anonymous fn/4 in Mix.Tasks.Ecto.Migrate.run/2

null: falsenull, falseって打ってた、アホス。
修正して再実行。

PS \hello> mix ecto.migrate
[info] == Running 20190403013056 Hello.Repo.Migrations.CreateAuthors.change/0 forward
[info] create table authors
[info] create index authors_user_id_index
[info] == Migrated 20190403013056 in 0.0s
[info] == Running 20190403015018 Hello.Repo.Migrations.AddAuthorIdToPages.change/0 forward
[info] alter table pages
[info] create index pages_author_id_index
[info] == Migrated 20190403015018 in 0.0s

問題無さそう。
データベース周りの準備は良さそうなのでCMSとしての機能を実装していきます。

かなり長くなってるのでまた区切ります。
流石に次回アタリでケリをつけたい。