I've got a weird case of ActiveRecord_Relation magic doing something unintended. Given the following:
comment = Comment.find(params[:id])
# select only the original commenter's comments that match two scenarios
related_comments = comment.where(something: params[:something)
.union(comment.where(something_else: params[:something_else]))
.where(user_id: comment.user_id)
related_comments.count
#=> 4
# This is to demonstrate how many records I'm intending to update.
# Using .length would work too, for example's sake.
related_comments.update_all(receive_notifications: true)
#=> SQL (3.9ms) UPDATE "comments" SET "receive_notifications" = 't'
#=> WHERE "comments"."user_id" = $1 [["user_id", 11]] [sql_query]
#=> 12
From the looks of it, .update_all is running a single SQL query that is updating all comments with a user_id of 11. The .where(user_id: 11) was technically the last query, but shouldn't it be running a SQL query for only the relations of related_comments?
I have a workaround where I could do this, but it would require two SQL queries and seems overly cumbersome than necessary:
comment = Comment.find(params[:id])
# same as above
related_comment_ids = comment.where(something: params[:something)
.union(comment.where(something_else: params[:something_else]))
.where(user_id: comment.user_id).pluck(:id)
#=> [12, 14, 17, 18]
Comment.where(id: related_comment_ids).update_all(receive_notifications: true)
#=> SQL (5.7ms) UPDATE "comments" SET "receive_notifications" = 't'
#=> WHERE "comments"."id" IN (18, 14, 17, 12) [sql_query]
#=> 4
Latest comments (13)
I don't know what the magic/issue is with your first example, but I have an idea of how to make your workaround take a single query. I can't seem to get an example like this working with
union(and on Rails 5.2 it gives a deprecation warning, about removing the delegation down into AREL) so I can't test this well myself.But if you do NOT pluck the
id, and simply pass a relation into the where clause foridit will do a sub-select for the update. I tried it with a simpler case and got theupdate_allto use a sub_select. So I think you should be able to do this in one query with:You probably hit a limitation of ActiveRecord :-)
unionis not part of ActiveRecord, which might be the reason whyupdate_allis not able to work with it.There's a lot of magic in it :D
PostgreSQL itself is able to update rows from unions if you project the same exact row types from both queries:
In conclusion: I don't know what's going on 🧐
Haha yeah the magic is certainly mystifying me. Guess I'll put on my wizard robes and dive into the code.
Let us know if you find anything :D
The Ancestry gem. It's what we (dev.to) use for the comment tree.
@acflint running
.orgives me an error since the built insubtreeandancestorsmethods have some extra SQL in there. :(Ahh gotcha :) I'll keep note of that for next time!
You're totally right about not being able to call
.whereoncomment; the example I used is wrong. I'm actually doing this:.subtreeand.ancestorsare methods from a gem, and I initially thought putting that in the example would be confusing. My workaround-example made it more confusing. 🙃Hmm interesting, I'll give this a try when I get a chance.
Re: what I'm trying to do. I'm trying to combine two Active record queries and then do a third
wherequery to get only the commenter's comments. The final chained query should produce a single SQL query, and then I want toupdate_alla single collection/relation of records.The
whereis called on the ActiveRecord relation and not an individual ActiveRecord record, which is how I can chain thewhereafter doingunion.What happens if you tack
.allonto the firstrelated_comments =query?.update_allstill returns12:/What is the SQL generated by
related_comments.count. Count will change the query sometimes if Rails decides it doesn't need the join/includes. I'm guessing it's dropping your union -- or simply not doing it correctly. And that's what is leading to the difference in rows affected.edit: however your second solution suggests i'm wrong about that. still, would be curious to see the SQL if you just run the query to select them and for the count.
Here's the SQL for
related_comments.count:And for the
select:FWIW the code I'm using does not run
related_comments.count; that was to demonstrate how many records should be updated.