Ruby on Rails初心者の学習記録 Part5：復習

# はじめに
Ruby on Rails初心者の学習記録Part5です。
今までRubyの基礎文法から始まり、RailsにおけるMVCやCRUDについて学んできました。
今回は、["Getting Started with Rails"](https://guides.rubyonrails.org/getting_started.html) の ["8 Adding a Second Model"](https://guides.rubyonrails.org/getting_started.html#adding-a-second-model) を教材にコメント機能の実装を通して、今まで学んできたことを復習します。

# 1. モデルの作成
[Part2](https://qiita.com/Yasushi-Mo/items/5dfe81d39b5a153fe43c#2-%E3%83%A2%E3%83%87%E3%83%AB%E3%81%AE%E4%BD%9C%E6%88%90)で記事データを扱うために作成した`Article`モデルのようにコメントデータを扱う`Comment`モデルを作成します。`Comment`モデルには、記事への参照も追加します。
```
rails generate model Comment commenter:string body:text article:references
```
実行後、以下4つのファイルが作成されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/318188/00e31c0c-fe7a-5af8-928e-ef62a6c4a9b1.png)
`Comment`モデルのファイルは以下のようになっています。
```app/models/comment.rb
class Comment < ApplicationRecord
  belongs_to :article
end
```
モデル作成時のコマンド、記事への参照（`article:references`）を追加したので、`belongs_to :article`が記載されています。これにより Active Recordアソシエーション（関連づけ） というものが設定されます（詳細は次章で確認）。
また、`:references`はモデルの特殊なデータ型を表しています。指定されたモデル名の後ろに`_id`を追加した名前を持つカラムをデータベース内に作成します（マイグレーション後、実際に`db/schema.rb`で確認）。

また、マイグレーションファイルは[Part2](https://qiita.com/Yasushi-Mo/items/5dfe81d39b5a153fe43c#3-%E3%83%9E%E3%82%A4%E3%82%B0%E3%83%AC%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3)で確認したときと同じようにデータベーステーブルと一致するように作成されています。
```db/migrate/20230201002601_create_comments.rb
class CreateComments < ActiveRecord::Migration[7.0]
  def change
    create_table :comments do |t|
      t.string :commenter
      t.text :body
      t.references :article, null: false, foreign_key: true

      t.timestamps
    end
  end
end
```
`t.references`では、`article_id`というinteger型のカラムとそのインデックス、`articles`テーブルの`id`カラムを指す外部キー制約を作成します。
それでは、実際にマイグレーションを実行します。
```
rails db:migrate
```
Railsは現在のデータベースに対して、今まで実行されていないマイグレーションのみを実行します。そのため、実行結果のメッセージは、今回作成した`comments`のみ表示されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/318188/01f27ddf-095a-c644-d1d9-33b2c97bce4b.png)

そして、`db/schema.rb`には、`comments`が追加されました。
```db/schema.rb
ActiveRecord::Schema[7.0].define(version: 2023_02_01_002601) do
  create_table "articles", force: :cascade do |t|
    t.string "title"
    t.text "body"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
  end

  create_table "comments", force: :cascade do |t|
    t.string "commenter"
    t.text "body"
    t.integer "article_id", null: false
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.index ["article_id"], name: "index_comments_on_article_id"
  end

  add_foreign_key "comments", "articles"
end
```

# 2. モデルのアソシエーション（関連づけ）
次に先ほどのモデル追加時に出てきたActive Recordアソシエーションについてみていきます。この機能により、2つのモデル間にリレーションを宣言することができます。今回の記事とコメントというケースでは、以下のどちらかの方法でリレーションを設定できます。

>- Each comment belongs to one article.
>- One article can have many comments.

この方法は、Railsがアソシエーションを宣言する方法と似ています。例えば、先ほど作成した`Comment`モデルはコメントが1つの記事に紐づいています。
```app/models/comment.rb
class Comment < ApplicationRecord
  belongs_to :article
end
```
なお、この時点では`Comment`モデル側のみアソシエーションがなされています。そのため、`Article`モデル側も対応する必要があります。
```app/models/article.rb
class Article < ApplicationRecord
  has_many :comments

  validates :title, presence: true
  validates:body, presence: true, length: { minimum: 10 }
end
```
これら2つの宣言により、かなりの動作が自動化されます。例えば、記事が1件含まれている`@article`というインスタンス変数があれば、`@article.comments`と書くことでその記事に紐づいている全てのコメントを取得することができます。

:::note
より詳しいActive Recordアソシエーションの説明は、"[Active Record Associations](https://guides.rubyonrails.org/association_basics.html)"に記載があります。
:::

# 3. コメントのルーティング
[Part1](https://qiita.com/Yasushi-Mo/items/d24fd4abab2c1333c522#%E2%85%B2-%E3%83%AB%E3%83%BC%E3%83%86%E3%82%A3%E3%83%B3%E3%82%B0)や[Part3](https://qiita.com/Yasushi-Mo/items/931cfcef990647f1da18#2-resourceful-routing)で`articles`コントローラーに対して行ったときと同様、ルーティングを追加する必要があります。`config/routes.rb`に`comments`用のルーティングを追加します。
```config/routes.rb
Rails.application.routes.draw do
  root "articles#index"
  
  resources :articles do
    resources :comments
  end
end
```
ここでは、`comments`を`articles`内にネストされたリソースとして作成しています。これは、モデルの記述とは別の観点から記事とコメントのリレーションシップを階層的に捉えたものです。

# 4. コントローラーの作成
モデルを作成できたので、次はコントローラーを作成します。
[Part1](https://qiita.com/Yasushi-Mo/items/d24fd4abab2c1333c522#%E2%85%B2-%E3%83%AB%E3%83%BC%E3%83%86%E3%82%A3%E3%83%B3%E3%82%B0)のときと同じように以下のコマンドで作成します。
```
rails generate controller Comments
```
実行後、以下のファイルが作成されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/318188/913fc2f8-2938-bbc3-3995-bf59dd233303.png)
この状態だと記事とコメントは別画面になります。ただ、一般的なブログは記事とコメントが同じ画面であることが多いと思います。
そのため、まずは記事表示画面（`app/views/articles/show.html.erb`）に新たなコメント作成欄を追加します。
```app/views/articles/show.html.erb
<h1><%= @article.title %></h1>

<p><%= @article.body %></p>

<ul>
  <li><%= link_to "Edit", edit_article_path(@article) %></li>
  <li><%= link_to "Destroy", article_path(@article), data: {
    turbo_method: :delete,
    turbo_confirm: "Are you sure?"
  } %></li>
</ul>

<h2>Add a comment:</h2>
<%= form_with model: [ @article, @article.comments.build ] do |form| %>
  <p>
    <%= form.label :commenter %><br>
    <%= form.text_field :commenter %>
  </p>
  <p>
    <%= form.label :body %><br>
    <%= form.text_area :body %>
  </p>
  <P>
    <%= form.submit %>
  </p>
<% end %>
```
ここで追加されたフォームは、`CommentsController`の`create`アクションを呼び出すことでコメントを新規作成します。
また、`form_with`には配列を渡し、`/articles/1/comments`のようなネストしたルーティングをビルドします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/318188/684f350a-97af-d7cb-976b-750967524646.png)
次は`app/controllers/comments_controller.rb`に`create`アクションを作成し、実際に先ほど作成したフォームから呼び出せるようにします。
```app/controllers/comments_controller.rb
class CommentsController < ApplicationController
  def create
    @article = Article.find(params[:article_id])
    @comment = @article.comments.create(comment_params)
    redirect_to article_path(@article)
  end

  private
    def comment_params
      params.require(:comment).permit(:commenter, :body)
    end
end
```
記事のコントローラーより少し複雑になっています。これはネストを使ったことによる影響です。コメントのリクエストでは、コメントを記事に紐づける必要があります。そのため、`Article`モデルの`find`を最初に呼び、リクエストに含まれている記事を取得します。
加えて、このコードではアソシエーションにより利用できるようになったメソッドをいくつか利用しています。`@article.comments`では、コメントを保存するために`create`を利用しています。これにより、コメントが記事と自動的に紐づき。記事に対してコメントが従属するようになります。

次は作成したコメントを画面上に表示できるようにします。
新しいコメントを作成すると、`article_path(@article)`ヘルパーを利用してユーザーは元の記事の画面に戻ります。このヘルパーを呼び出すと`ArticlesController`の`show`アクションが呼び出され、記事画面（`show.html.erb`）がレンダリングされます。そのため、この画面にコメントを作成したコメントを表示できるようにします。
```app/views/articles/show.html.erb
<h1><%= @article.title %></h1>

<p><%= @article.body %></p>

<ul>
  <li><%= link_to "Edit", edit_article_path(@article) %></li>
  <li><%= link_to "Destroy", article_path(@article), data: {
    turbo_method: :delete,
    turbo_confirm: "Are you sure?"
  } %></li>
</ul>

<h2>Comments</h2>
<% @article.comments.each do |comment| %>
  <p>
    <strong>Commenter:</strong>
    <%= comment.commenter %>
  </p>

  <p>
    <strong>Comment:</strong>
    <%= comment.body %>
  </p>
<% end %>

<h2>Add a comment:</h2>
<%= form_with model: [ @article, @article.comments.build ] do |form| %>
  <p>
    <%= form.label :commenter %><br>
    <%= form.text_field :commenter %>
  </p>
  <p>
    <%= form.label :body %><br>
    <%= form.text_area :body %>
  </p>
  <P>
    <%= form.submit %>
  </p>
<% end %>
```
これでコメント機能完成です！
![ezgif-5-b0bdbf441d.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/318188/fab4aa41-535c-b657-0424-19f74691bfb5.gif)

# 最後に
今回はコメント機能の実装を通して、モデルやルーティング、コントローラーの作成について復習しました。また、新たにモデルのアソシエーションについても学習しました。
最後までお読みいただきありがとうございました！

- [Part1](https://qiita.com/Yasushi-Mo/items/d24fd4abab2c1333c522)
- [Part2：モデル](https://qiita.com/Yasushi-Mo/items/5dfe81d39b5a153fe43c)
- [Part3：CRUD（前編）](https://qiita.com/Yasushi-Mo/items/931cfcef990647f1da18)
- [Part4：CRUD（後編）](https://qiita.com/Yasushi-Mo/items/337d5e31e0ea1f56485f)

# 参考
- [Getting Started with Rails](https://guides.rubyonrails.org/getting_started.html)