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: :destroy
やdependent: :nullify
指定すれば良い。
collection.destroy(object, …)
collection.delete
と違い、ActiveRecordを通してdestroy
を実行し削除する。よって、callbackなどが走る。これは、関連先オブジェクトの:dependent
オプションの指定にかかわらず実行される。
collection=objects
collectionが持つ既存のオブジェクトを削除し指定したobjectsに置き換える。
collection_singular_ids
関連先オブジェクトのidを配列として返す。
collection_singular_ids=ids
指定されたidsでcollectionの内容を置き換える。元からあったオブジェクトは削除される。
具体的な動作
投稿用のテーブル
class Post < ApplicationRecord
has_many :tags, class_name: 'Tagging'
has_many :programming_languages, through: :tags
validates :title, :description, presence: true
end
プログラミング言語を保持するテーブル
class ProgrammingLanguage < ApplicationRecord
has_many :tags, class_name: 'Tagging'
has_many :posts, through: :tags
validates :name, presence: true
validates :name, uniqueness: true
end
上記2つの中間テーブル
class Tagging < ApplicationRecord
belongs_to :post
belongs_to :programming_language
validates :post_id, uniqueness: { scope: :programming_language_id }
end
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を読み解いていく。
-
新たなcollectionとして追加するids(1, 2)を持つProgrammingLanguageレコードと現在collectionとして持つProgrammingLanguageレコードを取得している。
-
PostとProgrammingLanguageを繋ぐ中間テーブルのレコードを削除する。
-
tag.post_id = 2
,tag.programming_language_id = 1
のレコードがtagging テーブルに存在するか確認 -
存在しない場合、レコードをインサート
-
tag.programming_language_id = 2
の場合も同様の操作を繰り返す
collection.clear
collectionからオブジェクトを削除する。削除のされ方はdependent
オプションによる。
:through
オプションが指定されていた場合、Join models(これは中間テーブルのことなのかイマイチ分かってない)は、callbackが走らずダイレクトにDBから削除される。
まとめ
collectionとしてオブジェクトを操作する場合にcallbackなどが実行されるのかされないのか、DBへの操作まで含むのか、それともただアプリケーション上にメモリとして保存するだけなのかを意識するのが重要。
参考記事