🔴

【Ruby】Railsのアソシエーションとリレーションを理解し、コメント機能を実装

2024/04/13に公開

はじめに:

プログラミング初学者がRailsアプリケーションを動かす際に必要な知識として、アソシエーションとリレーションがある。プログラミング初学者がVScodeを用いてRails環境の構築後、アソシエーションとリレーションの知識を用いて投稿コメント機能を実装できることを目標とした記事となっている。

環境:

環境構築がまだの方はこちらから
https://zenn.dev/code_journey_ys/articles/c0cc5dd5372036

  • windows 11
  • Vscode 1.87.2
  • Ubuntu 22.04
  • wsl 2.1.5.0
  • ruby 3.2.3
  • rails 6.1.7

アソシエーションとリレーションとは

1: アソシエーションとリレーションについて理解しよう。

以上のことから、Postsモデル(投稿に関連するモデル)とCommentモデル(コメント)を関連づければ、X(旧Twitter)やInstagramのような投稿1つに対して、多数のコメントを保存できるようになりそうであることが予測できる。

コメント機能を実装しよう。

1: モデルの作成し、アソシエーションを組んでみよう。

1: commentモデルを作成する。(postモデルとuserモデルは作成済み)

ターミナル
rails g model comment body:text
ターミナル(出力結果)
$ rails g model comment body:text
Running via Spring preloader in process 35843
      invoke  active_record
      create    db/migrate/20240413124219_create_comments.rb
      create    app/models/comment.rb
      invoke    test_unit
      create      test/models/comment_test.rb
      create      test/fixtures/comments.yml
references型とは(応用編)

2: 各モデルに必要なアソシエーションを記述する。
Railsでアソシエーションを組むためには、has_manybelongs_toメソッドを理解しなければならない。

Postモデル】

class Post < ApplicationRecord
  has_many   :comments, dependent: :destroy  # 追加
  belongs_to :user, dependent: :destroy     # 追加
  validates  :title, presence: true, length: { maximum: 20  }
  validates  :body,  presence: true, length: { maximum: 400 }
end

Commentモデル】

class Comment < ApplicationRecord
  belongs_to :post # 追加
  belongs_to :user # 追加
end

Userモデル】

class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable

  has_many :comments, dependent: :destroy # 追加
  has_many :posts, dependent: :destroy    # 追加
end

ここまでで、各モデルのアソシエーションを組むことができました。

2: マイグレーションファイルを確認し、リレーションを組んでみよう。

各モデルの関連付けは完了しましたが、どのカラムで関連を結ぶかの記述と確認ができていません。通常、post_iduser_idなど主キー(PK)となるものでリレーションを結んだ際には、マイグレーションファイルに新たなカラムが必要になるため、マイグレーションファイルに記述を追加し、マイグレーションを実行する必要があります。
1: 各マイグレーションファイルにリレーションを結ぶうえで必要な記述する。
20240101_create_posts.rb

db/migrate/20240101_create_posts.rb
class CreatePosts < ActiveRecord::Migration[6.1]
  def change
    create_table :posts do |t|
      t.integer  :user_id, null:false, default: 0  # 追加
      t.string :title
      t.text :body
      t.timestamps
    end
  end
end

20240101_create_comments.rb

db/migrate/20240101_create_comments.rb
class CreateComments < ActiveRecord::Migration[6.1]
  def change
    create_table :comments do |t|
      t.integer :user_id, null:false, default: 0   # 追加
      t.integer :post_id, null:false,  default: 0  # 追加
      t.text :body
      t.timestamps
    end
  end
end

20240101_devise_create_users.rb】※特に変更なし。

db/migrate/20240101_devise_create_users.rb
class DeviseCreateUsers < ActiveRecord::Migration[6.1]
  def change
    create_table :users do |t|
      ## Database authenticatable
      t.string :email,              null: false, default: ""
      t.string :encrypted_password, null: false, default: ""
      t.string :name,               null: false, default: ""
      ## Recoverable
      t.string   :reset_password_token
      t.datetime :reset_password_sent_at

      ## Rememberable
      t.datetime :remember_created_at

      ## Trackable
      # t.integer  :sign_in_count, default: 0, null: false
      # t.datetime :current_sign_in_at
      # t.datetime :last_sign_in_at
      # t.string   :current_sign_in_ip
      # t.string   :last_sign_in_ip

      ## Confirmable
      # t.string   :confirmation_token
      # t.datetime :confirmed_at
      # t.datetime :confirmation_sent_at
      # t.string   :unconfirmed_email # Only if using reconfirmable

      ## Lockable
      # t.integer  :failed_attempts, default: 0, null: false # Only if lock strategy is :failed_attempts
      # t.string   :unlock_token # Only if unlock strategy is :email or :both
      # t.datetime :locked_at

      t.timestamps null: false
    end

    add_index :users, :email,                unique: true
    add_index :users, :reset_password_token, unique: true
    # add_index :users, :confirmation_token,   unique: true
    # add_index :users, :unlock_token,         unique: true
  end
end

2: マイグレーションを実行する。

ターミナル
$ rails db:migrate:reset
ターミナル(出力結果)
== 20240405125535 CreatePosts: migrating ======================================
-- create_table(:posts)
   -> 0.0049s
== 20240405125535 CreatePosts: migrated (0.0054s) =============================

== 20240408152336 DeviseCreateUsers: migrating ================================
-- create_table(:users)
   -> 0.0067s
-- add_index(:users, :email, {:unique=>true})
   -> 0.0029s
-- add_index(:users, :reset_password_token, {:unique=>true})
   -> 0.0015s
== 20240408152336 DeviseCreateUsers: migrated (0.0136s) =======================

== 20240411111914 CreateComments: migrating ===================================
-- create_table(:comments)
   -> 0.0087s
== 20240411111914 CreateComments: migrated (0.0091s) ==========================

3: db/schema.rbファイルに正しくテーブルが定義されているか確認する。

db/schema.rb
ActiveRecord::Schema.define(version: 2024_04_11_111914) do
  create_table "comments", force: :cascade do |t|
    t.integer "user_id", default: 0, null: false
    t.text "body"
    t.datetime "created_at", precision: 6, null: false
    t.datetime "updated_at", precision: 6, null: false
  end

  create_table "posts", force: :cascade do |t|
    t.integer "user_id", default: 0, null: false
    t.string "title"
    t.text "body"
    t.datetime "created_at", precision: 6, null: false
    t.datetime "updated_at", precision: 6, null: false
  end

  create_table "users", force: :cascade do |t|
    t.string "email", default: "", null: false
    t.string "encrypted_password", default: "", null: false
    t.string "name", default: "", null: false
    t.string "reset_password_token"
    t.datetime "reset_password_sent_at"
    t.datetime "remember_created_at"
    t.datetime "created_at", precision: 6, null: false
    t.datetime "updated_at", precision: 6, null: false
    t.index ["email"], name: "index_users_on_email", unique: true
    t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
  end
end

3: コントローラを作成し、アクションなどを定義する。

1: commentsコントローラを作成する。

ターミナル
$ rails g controller comments

2: commentsコントローラにbefore_action,create,ストロングパラメータを定義する。

app/controllers/comments_controller.rb
class CommentsController < ApplicationController
  before_action :authenticate_user!
  
  def create
    # 投稿詳細ページで渡ってきているpost_idと同じpost_idの投稿をpostへ代入(1)
    @post = Post.find(params[:post_id])
  # 送られてきたcomment_paramsをログインしているユーザーのものと関連づける(2)
    comment = current_user.comments.new(comment_params)
  # (1)のpost_idと(2)のpost.idが同じであれば保存する
    comment.post_id = @post.id 
    comment.save
  # 投稿詳細ページへリダイレクトする。
    redirect_to post_path(@post)
  end

  private

  def comment_params
    params.require(:comment).permit(:body)
  end
end

4: ルーティングを設定する。

1: 変更前のルーティングファイルとルーティング状況を把握する。

config/routes.rb
Rails.application.routes.draw do
  devise_for :users, controllers: {
    registrations: 'users/registrations',
    sessions: 'users/sessions'
  }
  resources :posts
  resources :comments
end
ターミナル
$ rails routes -g comment
      Prefix Verb   URI Pattern                  Controller#Action
    comments GET    /comments(.:format)          comments#index
             POST   /comments(.:format)          comments#create
 new_comment GET    /comments/new(.:format)      comments#new
edit_comment GET    /comments/:id/edit(.:format) comments#edit
     comment GET    /comments/:id(.:format)      comments#show
             PATCH  /comments/:id(.:format)      comments#update
             PUT    /comments/:id(.:format)      comments#update
             DELETE /comments/:id(.:format)      comments#destroy

postscommentsコントローラへのルーティングは、resourcesでそれぞれ定義している。

2: Postコントローラのルーティングの中にCommentsコントローラのルーティングをネストする。

config/routes.rb(ネスト後)
Rails.application.routes.draw do
  devise_for :users, controllers: {
    registrations: 'users/registrations',
    sessions: 'users/sessions'
  }
 resources :posts do
    resources :comments, only: [:create]
  end
end
ターミナル(ネスト後)
$ rails routes -g comments
       Prefix Verb   URI Pattern                            Controller#Action
 post_comments POST   /posts/:post_id/comments(.:format)     comments#create

5: コメントフォーム(ビュー)を作成しよう。

1: 投稿詳細のビューファイルにコメントフォームとコメント表示一覧を作成する。

app/views/posts/show.html.erb
<h1>Hello</h1>
  <div>
    <div>
      <label for="article_title">Title</label><br>
      <%= @post.title %>
    </div>

    <div>
      <label for="article_body">Body</label><br>
      <%= @post.body %>
    </div>

    <div>
      <%= link_to "削除", post_path(@post), method: :delete, data: { confirm: "本当に削除しますか?" } %>
    </div>
  </div>
    
  <!--コメントフォームを追加-->
  <div>
    <%= form_with(model: [@post, Comment.new], url: post_comments_path(@post)) do |f| %>
    <div class="field">
      <%= f.label :body, "コメント追加" %>
      <%= f.text_area :body %>
    </div>
    <div class="actions">
      <%= f.submit "コメントする" %>
    </div>
    <% end %>
  </div>
 <!-- ここまで -->
コード解説
app/views/posts/show.html.erb
<%= form_with(model: [@post, Comment.new], url: post_comments_path(@post)) do |f| %>

・通常であれば、<%= form_with model: @comment ~ do |f| %>だが、model: [@post, Comment.new]となっている。この配列の記述をすることで、postcommentモデル間の関連付けを持たせることが可能となる。
このフォームに入力された内容が送信(submit)され、Createアクションが実行されたとき、新しいコメントとしてcommentモデルに保存されると同時に、関連するpostオブジェクトにも関連付けられて保存される。
@postはどこからでてきたのかと思う方がいらっしゃると思いますが、それはコントローラのメソッドで定義しているものです。(以下参照)

app/controllers/posts_controller.rb
    def show
      @post = Post.find(params[:id])
      # ↑この@postのこと
    end
    ~~
    private
    def post_params
      params.require(:post).permit(:title, :body)
    end

・投稿一覧(index)から投稿詳細(show)に遷移した際に、params[:id]でその投稿のid(post_id)が事前に送られている。あとはその投稿(post)とコメント(comment)を関連付けて保存すればよいという流れ。

2: http://localhost:3000/posts/1にアクセスし、フォームが正しく表示されるか確認する。

app/views/posts/show.html.erb

6: コメントを投稿してみよう。

1: 新規登録などユーザーの登録が完了し、ログインしている状態かつ1つ以上の投稿が完了している状態で、ブラウザの検索バーにhttp://localhost:3000/posts/1と入力し、投稿へアクセスする。➡5: コメントフォーム(ビュー)を作成しよう。の最後の画像が表示されていればよい。

2: 適当にコメントを行い、エラー画面にならないか確認する。

3: rails cを立ち上げ、コメントが投稿されているかをターミナルから確認する。

ターミナル
rails c
ターミナル
irb(main):001:0> Comment.all
ターミナル(出力結果)
$ rails c
Running via Spring preloader in process 58777
Loading development environment (Rails 6.1.7.7)
irb(main):001:0> Comment.all
   (1.6ms)  SELECT sqlite_version(*)
  Comment Load (0.3ms)  SELECT "comments".* FROM "comments"
=> 
[#<Comment:0x0000556d8cb0ded0
  id: 1,
  user_id: 1,
  post_id: 1,
  body: "test comment!",
  created_at: Sat, 13 Apr 2024 13:39:27.800416000 UTC +00:00,
  updated_at: Sat, 13 Apr 2024 13:39:27.800416000 UTC +00:00>]

7: コメントがブラウザで表示されるようにしよう。(コントローラとビュー)

現段階では、保存ができている。ビューで表示させるための記述を行っていないため、ブラウザでは見えない状況。
1: posts_controller.rbファイルのshowアクションにコメントを表示させるための、インスタンス変数を定義する。

posts_controller.rb
  def show
    @post = Post.find(params[:id])
    @comments = Comment.all   # 追加 (Commentテーブルにあるコメントを全て表示)
  end

2: app/views/posts/show.html.erbファイルの中にコメントを表示一覧させるための記述を追加する。

app/views/posts/show.html.erb
<h1>Hello</h1>
  <div>
    <div>
      <label for="article_title">Title</label><br>
      <%= @post.title %>
    </div>

    <div>
      <label for="article_body">Body</label><br>
      <%= @post.body %>
    </div>

    <div>
      <%= link_to "削除", post_path(@post), method: :delete, data: { confirm: "本当に削除しますか?" } %>
    </div>
  </div>

  <!--コメントフォーム-->
  <div>
    <%= form_with(model: [@post, Comment.new], url: post_comments_path(@post)) do |f| %>
    <div class="field">
      <%= f.label :body, "コメント追加" %>
      <%= f.text_area :body %>
    </div>
    <div class="actions">
      <%= f.submit "コメントする" %>
    </div>
    <% end %>
  </div>

 <!-- コメント一覧の表示追加 -->
  <div>
    <% @post.comments.each do |comment| %>
      <div>
        <p><%= comment.user.name %></p>
        <p><%= comment.body %></p>
        <p><%= comment.created_at.strftime('%Y/%m/%d') %></p>
      </div>
    <% end %>
  </div>
 <!-- ここまで -->

3: 表示を確認する。

app/views/posts/show.html.erb

まとめ

・コメント機能を実装するためには、【アソシエーション】・【リレーション】について理解する必要がある。
・モデル➡マイグレーションファイル➡コントローラ➡ビューの順でファイルを作成、編集を行うことでコメント機能が実装できる。

参考サイト

https://qiita.com/kouhei_matuura/items/16b77a25cf53d7ff232a
https://qiita.com/mmaumtjgj/items/cdc76572d392957c4299
https://railsdoc.com/rails_base
https://railsguides.jp/v7.1/getting_started.html

おわりに

今回、プログラミング初学者がRailsアプリケーションを動かす際に必要な知識であるアソシエーションとリレーションについて学んだ。データベースからデータを取ってきて表示するための基礎的な流れをつかむことでその他の機能に応用することが可能となると考える。

Discussion