Let's say I have a classic example of a Post that has_many Tags through a Taggings join table. How would I go about upserting a specific tag to multiple posts at once that meet a certain criteria.
In pseudo code, I was thinking I'd first filter down any Posts that already have the desired tag.
tag_id = Tag.first # the desired tag
posts = Post.first(50) # example query of subset of posts
posts = posts.joins(:tags).where.not(tags: { id: tag_id }) # filter out posts that already have desired tag
Now I need to insert into the Taggings table all of the selected post ids and the desired tag id.
I was wondering if there is a "rails" way of doing this?
Thanks for your help!
Given:
tag = Tag.first # the desired tag. Note I changed to getting the object and calling the .id method later as your code was incorrect.
posts = Post.first(50) # example query of subset of posts
filtered_posts = posts.joins(:tags).where.not(tags: { id: tag.id }) # filter out posts that already have desired tag
One way:
filtered_posts.each do |p|
p.tags << tag
end
This cycles through the posts and created the join record for you. I would say this is very Rails-y because it uses the built in associations so that you don't even mention the taggings join table. Also I would assume you have some validation where a post can't be tagged twice with the same tag. This would prevent that. If for some reason you needed to add a tag to thousands of posts (seems unlikely but you never know) there are ways to bypass building an object for each post and make the changes using SQL. If you wanted a minimum of queries run you could do:
t_id = tag.id
#=> 33
post_ids = filtered_posts.ids # .ids is a built in method to get all ids without a new query
#=> [1, 23, 45, 94, ....]
insert_hash = post_ids.map {|p_id| {post_id: p_id, tag_id: t_id} }
#=> [{post_id: 1, tag_id: 33}, {post_id: 23, tag_id: 33}, {post_id: 45, tag_id: 33}, ...]
Tagging.upsert_all(insert_hash)
#=> Tagging Bulk Insert (8.3ms) INSERT INTO "taggings" ("post_id","tag_id","created_at","updated_at") VALUES (1, 33, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP), (23, 33, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)...
Reference the documentation for insert_all
if you use it since it bypasses validations, etc. in the normal Rails flow. https://api.rubyonrails.org/classes/ActiveRecord/Persistence/ClassMethods.html#method-i-upsert_all