このページの内容は以下のリポジトリに1日遅れで反映されます(記事執筆前に前日分をコミットしています)。
https://github.com/hkob/hkob_blog
はじめに
Rails Tips の 82 回目です。記事にユーザを追加したようにコメントにもユーザを登録してみます。
コメントにユーザを追加
記事にユーザを追加したようにコメントにも 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 が許可されているので、少し気にすべき部分がいくつかありました。