コメントにユーザを追加: 小林研 Rails Tips (82)

このページの内容は以下のリポジトリに1日遅れで反映されます(記事執筆前に前日分をコミットしています)。

https://github.com/hkob/hkob_blog

はじめに

Rails Tips の 82 回目です。記事にユーザを追加したようにコメントにもユーザを登録してみます。

Rails をはじめよう - Railsガイド

コメントにユーザを追加

記事にユーザを追加したようにコメントにも user_id を追加します。また、必要なくなる commenter は削除します。

$ bin/rails g migration add_user_id_to_comment user_id:integer
      invoke  active_record
      create    db/migrate/20240219111006_add_user_id_to_comment.rb
$ bin/rails g migration remove_commenter_from_comment commenter:string
      invoke  active_record
      create    db/migrate/20240219111023_remove_commenter_from_comment.rb

記事と異なり、コメントの user_id に null: false をつけるのをやめてみます。ユーザが削除された時に、ユーザなしのコメントを残すようにするためです。belongs_to の optional の説明をしてみたいという理由からです。この状態で migration してみます。

class AddUserIdToComment < ActiveRecord::Migration[7.1]
  def change
    add_column :comments, :user_id, :integer
    add_index :comments, :user_id
  end
end

commenter の削除はそのままで問題ないです。

class RemoveCommenterFromComment < ActiveRecord::Migration[7.1]
  def change
    remove_column :comments, :commenter, :string
  end
end

マイグレーションしたら comment の yaml を修正します。特別に user の存在しないコメントも追加しておきます。

comment1:
  user: hkob
  body: "comment 1 for article 1"
  article: article1
  status: "public"

no_user:
  body: "comment 2 for article 1"
  article: article1
  status: "public"

can_delete:
  user: can_delete
  body: "comment 2 for article can_delete"
  article: can_delete
  status: "archived"

comment_spec を次のように修正します。user の関連のテストを追加しています。ここで、親である user が削除された時に、user_id に nil が入るかどうかのテストをしています。この shared example はこちらで紹介しています。

context "属性に関する共通テスト" do
    subject { can_delete }
    it_behaves_like "存在制約", %i[body article_id]
    it_behaves_like "値限定制約", :status, %w[public private archived], %w[NG]
    it_behaves_like "削除可能制約"
    it_behaves_like "関連確認", :comment, has_many: %i[article user]
    it_behaves_like "親削除時に自分も削除", :comment, %i[article]
    it_behaves_like "親削除時にnullを設定", :comment, %i[user]

comment.rb から commenter を外し、代わりに belongs_to で user を追加します。ただし、user が削除されることもあるため、optional: true として nil を許可します。

class Comment < ApplicationRecord
  include Visible

  validates :body, presence: true, length: { minimum: 10 }
  belongs_to :article
  belongs_to :user, optional: true

  scope :order_created_at_desc, -> { order(created_at: :desc) }
end

user の方は has_many の dependent に nullify を記述します。これで user が削除された時に、comment の user_id には nil が入ります。

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

  has_many :articles, dependent: :restrict_with_error
  has_many :comments, dependent: :nullify
end

これでテストが通ったと思ったのですが、一つ問題がありました。can_delete の user である can_delete ユーザが削除できませんでした。この原因は articles.yml にありました。記事の can_delete の user を can_delete に設定していたからです。article が設定されているユーザは削除できないので、can_delete という名前は問題があります。新しく、cannot_delete ユーザを users.yml に作成し、can_delete 記事のユーザを cannot_delete ユーザに書き換えました。

can_delete:
  title: "This is the second article that can be deleted"
  body: "This is the body of the second article"
  status: "private"
  user: cannot_delete

これで属性に関するテストは通過しました。また、article 同様 owned_by? と user_name メソッドのテストも追加します。

  context "複数の Comment オブジェクトについて" do
    let!(:targets) { comments(*%i[comment1 no_user can_delete]) }
    subject { targets }

    it_behaves_like "配列メソッド呼び出し" do
      let(:test_set) do
        {
          archived?: [nil, [false, false, true]],
          owned_by?: [
            [nil], [false, false, false],
            comment.user, [true, false, false]
          ],
          user_name: [nil, %w[hkob 削除済 can_delete]],
        }
      end
    end
  end

これらが通過するように実装を記述します。user が nil になることもあるため、article とは少し気にする部分が多くあります。削除済と表示したいため、単純な delegate ではなくメソッド化しています。

  # @param [User, nil] u 確認するユーザー
  # @return [TrueClass, FalseClass] ユーザーが所有していたら true
  def owned_by?(u)
    user_id.present? && user_id == u&.id
  end

  # @return [String] ユーザー名
  def user_name
    user&.name || "削除済"
  end

おわりに

記事と同様にコメントにもユーザを追加しました。ただし、こちらは nil が許可されているので、少し気にすべき部分がいくつかありました。