has_many関連作成時に使えるようになるメソッド


has_many

モデルにこれを指定することで関連先のモデルのオブジェクトのCRUD処理を簡単にすることができる。その時に使うメソッドの挙動についてまとめる。

colletion

関連先のオブジェクト(objects)を返す。

collection << (object, …)

collectionにオブジェクトを1つ以上追加でき、自動で外部キーが設定される。この操作は、親のオブジェクトが新しいレコードでない限り、即座にSQL update操作が発火され関連先オブジェクトのvalidationやcallbackも走る。
https://api.rubyonrails.org/classes/ActiveRecord/Associations/CollectionProxy.html#method-i-3C-3C

collection.delete(object, …)

基本の動作としては、objectの外部キーをNULLに設定する。加えて、dependent: :destroyが指定されている場合、ActiveRecordを通してdestroyが実行され削除される。故に、callbackなどが走る。dependent: :delete_allが指定されていた場合、関連先のオブジェクトがデータベースから削除される。callbackなどは走らない。

また、:throughオプションが使用されていた場合、デフォルトでdependent: :delete_allを指定した場合を同じ挙動をする。これを変更するためには、dependent: :destroydependent: :nullify指定すれば良い。

collection.destroy(object, …)

collection.deleteと違い、ActiveRecordを通してdestroyを実行し削除する。よって、callbackなどが走る。これは、関連先オブジェクトの:dependentオプションの指定にかかわらず実行される。

collection=objects

collectionが持つ既存のオブジェクトを削除し指定したobjectsに置き換える。

collection_singular_ids

関連先オブジェクトのidを配列として返す。

collection_singular_ids=ids

指定されたidsでcollectionの内容を置き換える。元からあったオブジェクトは削除される。

具体的な動作

投稿用のテーブル

post.rb
class Post < ApplicationRecord
  has_many :tags, class_name: 'Tagging'
  has_many :programming_languages, through: :tags

  validates :title, :description, presence: true
end

プログラミング言語を保持するテーブル

programming_language.rb
class ProgrammingLanguage < ApplicationRecord
  has_many :tags, class_name: 'Tagging'
  has_many :posts, through: :tags

  validates :name, presence: true
  validates :name, uniqueness: true
end

上記2つの中間テーブル

tagging.rb
class Tagging < ApplicationRecord
  belongs_to :post
  belongs_to :programming_language

  validates :post_id, uniqueness: { scope: :programming_language_id }
end
example.rb
post.programming_languages_ids
# => [12, 13, 14]
post.programming_languages_ids = [1, 2]

# ProgrammingLanguage Load
# SELECT "programming_languages".* FROM "programming_languages" WHERE "programming_languages"."id" IN ($1, $2)  [["id", 1], ["id", 2]]
# ProgrammingLanguage Load 
# SELECT "programming_languages".* FROM "programming_languages" INNER JOIN "taggings" ON "programming_languages"."id" = "taggings"."programming_language_id" WHERE "taggings"."post_id" = $1  [["post_id", 2]]

# TRANSACTION
# Tagging Destroy
# DELETE FROM "taggings" WHERE "taggings"."post_id" = $1 AND "taggings"."programming_language_id" IN ($2, $3, $4)  [["post_id", 2], ["programming_language_id", 12], ["programming_language_id", 13], ["programming_language_id", 14]]

# Tagging Exists?
# SELECT 1 AS one FROM "taggings" WHERE "taggings"."post_id" = $1 AND "taggings"."programming_language_id" = $2 LIMIT $3  [["post_id", 2], ["programming_language_id", 1], ["LIMIT", 1]]

# Tagging Create
# INSERT INTO "taggings" ("post_id", "programming_language_id", "created_at", "updated_at") VALUES ($1, $2, $3, $4) RETURNING "id"  [["post_id", 2], ["programming_language_id", 1], ["created_at", "2021-10-19 10:47:48.071965"], ["updated_at", "2021-10-19 10:47:48.071965"]]

# Tagging Exists?
# SELECT 1 AS one FROM "taggings" WHERE "taggings"."post_id" = $1 AND "taggings"."programming_language_id" = $2 LIMIT $3  [["post_id", 2], ["programming_language_id", 2], ["LIMIT", 1]]

# Tagging Create
# INSERT INTO "taggings" ("post_id", "programming_language_id", "created_at", "updated_at") VALUES ($1, $2, $3, $4) RETURNING "id"  [["post_id", 2], ["programming_language_id", 2], ["created_at", "2021-10-19 10:47:48.081659"], ["updated_at", "2021-10-19 10:47:48.081659"]]

SQLを読み解いていく。

  1. 新たなcollectionとして追加するids(1, 2)を持つProgrammingLanguageレコードと現在collectionとして持つProgrammingLanguageレコードを取得している。

  2. PostとProgrammingLanguageを繋ぐ中間テーブルのレコードを削除する。

  3. tag.post_id = 2, tag.programming_language_id = 1のレコードがtagging テーブルに存在するか確認

  4. 存在しない場合、レコードをインサート

  5. tag.programming_language_id = 2の場合も同様の操作を繰り返す

collection.clear

collectionからオブジェクトを削除する。削除のされ方はdependentオプションによる。
:throughオプションが指定されていた場合、Join models(これは中間テーブルのことなのかイマイチ分かってない)は、callbackが走らずダイレクトにDBから削除される。

まとめ

collectionとしてオブジェクトを操作する場合にcallbackなどが実行されるのかされないのか、DBへの操作まで含むのか、それともただアプリケーション上にメモリとして保存するだけなのかを意識するのが重要。

参考記事