Elixir+Phoenixに触ってみる（デモ作成編2）

# やること
前回に引き続き、デモアプリを作っていく。
今回は記事に対するコメント機能を追加する。

# コメントモデルを追加する
コメントのスキーマはこんな感じ

    commnets   
        id: int,
        post_id: int,
        name: string(255),
        content: text,
        inserted_at: datetime,
        updated_at: datetime


コメントの表示及び投稿フォームは記事の詳細ページに配置する予定なので、今回はモデルのみを追加。

    $ mix phoenix.gen.model Comment comments post_id:references:posts name:string content:string
    * creating priv/repo/migrations/20160209074344_create_comment.exs
    * creating web/models/comment.ex
    * creating test/models/comment_test.exs

    Remember to update your repository by running migrations:

        $ mix ecto.migrate

    $ mix ecto.migrate
    Compiled web/models/comment.ex
    Generated phoenix_sample app

    16:52:27.852 [info]  == Running PhoenixSample.Repo.Migrations.CreateComment.change/0 forward

    16:52:27.853 [info]  create table comments

    16:52:27.861 [info]  create index comments_post_id_index

    16:52:27.898 [info]  == Migrated in 0.3s
    $ 

モデル同士の関連を定義する場合は、作成時の型としてreferencesを使う。
生成された、web/models/comment.exを確認してみると

    schema "comments" do
        field :name, :string
        field :content, :string
        belongs_to :post, PhoenixSample.Post

        timestamps
    end

と親に当たる記事に対する参照が定義されていることが確認できる。

# コメント表示機能の追加
## Postモデルの変更
コメントから記事への参照は定義されているが、逆方向の参照が定義されていないので追加する。
web/models/post.exを以下のように変更

    schema "posts" do
        field :title, :string
        field :content, :string

        # add reference to comments
        has_many :comments, PhoenixSample.Comment                                                                                                                                                    

        timestamps
    end

## テンプレートの変更
コメントは記事の詳細ページにて表示するので、web/templates/post/show.html.eexの最後に以下の記述を追加

    <h3>Comments</h3>
    <ul>
      <%= for comment <- @post.comments do %>
        <li><%= comment.name %>: <%= comment.content %> </li>
      <%= end %>
    </ul>




## 画面で確認
コメント投稿機能はまだ無いので、DBに直接コメントレコードを追加して、記事詳細ページを開いてみると…
![スクリーンショット 2016-02-09 17.51.14.png](https://qiita-image-store.s3.amazonaws.com/0/111295/ca82e556-4435-0bcd-36f3-82a61f8205c4.png)

Ecto.Association.NotLoadedで検索してみると、ロードしたモデルの関連先のモデルもビューで使用したいときは、その関連先のモデルもコントローラで明示的にロードしてやる必要があるらしい（はっきりとした情報でもないのですが…）
なので、コントローラ(web/controllers/post_controller.ex)の33行目付近に、commentsをロードする記述を追加

    def show(conn, %{"id" => id}) do
        post = Repo.get!(Post, id) |> Repo.preload(:comments) # add comment preload 

        render(conn, "show.html", post: post)
    end

再度、記事ページにアクセスしてみる。
![スクリーンショット 2016-02-09 17.59.19.png](https://qiita-image-store.s3.amazonaws.com/0/111295/845318e0-5b1f-1227-09f9-4b7182e3ace7.png)

今度は無事成功。

# コメント投稿機能の追加
コメント投稿機能を追加する。
コメント投稿フォームは、記事詳細ページに配置し、投稿後は同じ記事詳細ページヘ遷移する。

## コントローラの修正
コメントは、独立したリソースではなく記事に従属するリソースなので、コメント追加のためのメソッドもPostコントローラに追加することにする。

web/controllers/post_controller.exにAliasを追加して

    alias PhoenixSample.Comment

以下のようなメソッドを追加

    def add_comment(conn, %{"id" => id, "comment" => post_params}) do
      post = Repo.get!(Post, id)
      changeset = Comment.changeset(%Comment{post_id: post.id}, post_params)

      case Repo.insert(changeset) do
        {:ok, _post} ->
          conn
          |> put_flash(:info, "Comment create successfully.")
          |> redirect(to: post_path(conn, :show, post))
        {:error, changeset} ->
          conn
          |> put_flash(:info, "Error occured.")
          |> redirect(to: post_path(conn, :show, post))
      end
    end

## ルートの追加
コメントの投稿には

    POST /posts/:id/comment

というルートを割り当てたい。
web/router.exの21行目付近に新しいルートを追加。

    scope "/", PhoenixSample do
      pipe_through :browser # Use the default browser stack

      get "/", PageController, :index
      resources "/posts", PostController
      post "/posts/:id/comments", PostController, :add_comment  # add comment route
    end

確認のために、ルート一覧を確認

    $ mix phoenix.routes
    page_path  GET     /                    PhoenixSample.PageController :index
    post_path  GET     /posts               PhoenixSample.PostController :index
    post_path  GET     /posts/:id/edit      PhoenixSample.PostController :edit
    post_path  GET     /posts/new           PhoenixSample.PostController :new
    post_path  GET     /posts/:id           PhoenixSample.PostController :show
    post_path  POST    /posts               PhoenixSample.PostController :create
    post_path  PATCH   /posts/:id           PhoenixSample.PostController :update
               PUT     /posts/:id           PhoenixSample.PostController :update
    post_path  DELETE  /posts/:id           PhoenixSample.PostController :delete
    post_path  POST    /posts/:id/comments  PhoenixSample.PostController :add_comment # <- 追加されている


## テンプレートの修正
テンプレートにコメント投稿フォームを追加する。
まず、newメソッドを参考に、showメソッドを以下のように修正する。

    def show(conn, %{"id" => id}) do
      post = Repo.get!(Post, id) |> Repo.preload(:comments)

      comment_changeset = Comment.changeset(%Comment{})

      render(conn, "show.html", post: post, comment_changeset: comment_changeset)
    end

そして、web/templates/post/show.html.eexの最後に以下のような記述を追加。

    <%= form_for @comment_changeset, post_path(@conn, :add_comment, @post), fn f -> %>
      <div class="form-group">
        <span>名前: <%= text_input f, :name, size: 10 %></span>
        <span><%= text_input f, :content, size: 50 %></span>
        <%= error_tag f, :name %>
        <%= error_tag f, :content %>
      </div>
      <div class="form-group">
        <%= submit "コメントする", class: "btn btn-primary" %>
      </div>
    <%= end %>

## 確認
ブラウザで記事詳細ページを開いてみると
![スクリーンショット 2016-02-09 19.31.53.png](https://qiita-image-store.s3.amazonaws.com/0/111295/86c8adfe-025d-af79-e10e-fb63826a3c8e.png)
このように、コメント投稿フォームが追加されていることが確認できると思います。
試しにコメントを投稿してみると…
![スクリーンショット 2016-02-09 19.33.23.png](https://qiita-image-store.s3.amazonaws.com/0/111295/82ee2aed-7ad5-4d7a-7d95-7ea1c64dd5fd.png)
このように、無事コメントの投稿ができることが確認されました。
やったね。

# 終わりに
以上で、当初想定していた機能は一通り実装することができました。
Phoenixを触るのは全くの初めての状態から開始でしたが、ところどころ躓いたりもしましたが、概ね問題なくここまで終わらせることができました。
Railsやそれに影響を受けたフレームワークを扱ったことがある人なら、さほど苦労なく導入最初の段階は超えられると思われます。（その先はまだ不明）

今後は、このプロジェクトをベースに様々な機能の実験をしていこうと思います。