From ccdf6b8d42b2d32e0618870c87b1de83c06542c7 Mon Sep 17 00:00:00 2001 From: Kasper Timm Hansen Date: Thu, 26 Nov 2020 14:15:18 +0100 Subject: [PATCH] Add where.associated to check association presence --- activerecord/CHANGELOG.md | 14 ++++++++++ .../active_record/relation/query_methods.rb | 28 +++++++++++++++++++ .../test/cases/relation/where_chain_test.rb | 16 +++++++++++ 3 files changed, 58 insertions(+) diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index e2adc7ba98..0ecbcdefc5 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,3 +1,17 @@ +* Add `where.associated` to check for the presence of an association. + + ```ruby + # Before: + account.users.joins(:contact).where.not(contact_id: nil) + + # After: + account.users.where.associated(:contact) + ``` + + Also mirrors `where.missing`. + + *Kasper Timm Hansen* + * Fix odd behavior of inverse_of with multiple belongs_to to same class. Fixes #35204. diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index fbf2865811..87cc1af703 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -48,6 +48,34 @@ module ActiveRecord @scope end + # Returns a new relation with joins and where clause to identify + # associated relations. + # + # For example, posts that are associated to a related author: + # + # Post.where.associated(:author) + # # SELECT "posts".* FROM "posts" + # # INNER JOIN "authors" ON "authors"."id" = "posts"."author_id" + # # WHERE "authors"."id" IS NOT NULL + # + # Additionally, multiple relations can be combined. This will return posts + # associated to both an author and any comments: + # + # Post.where.associated(:author, :comments) + # # SELECT "posts".* FROM "posts" + # # INNER JOIN "authors" ON "authors"."id" = "posts"."author_id" + # # INNER JOIN "comments" ON "comments"."post_id" = "posts"."id" + # # WHERE "authors"."id" IS NOT NULL AND "comments"."id" IS NOT NULL + def associated(*associations) + associations.each do |association| + reflection = @scope.klass._reflect_on_association(association) + @scope.joins!(association) + self.not(reflection.table_name => { reflection.association_primary_key => nil }) + end + + @scope + end + # Returns a new relation with left outer joins and where clause to identify # missing relations. # diff --git a/activerecord/test/cases/relation/where_chain_test.rb b/activerecord/test/cases/relation/where_chain_test.rb index 8cec633b39..23599ba4cc 100644 --- a/activerecord/test/cases/relation/where_chain_test.rb +++ b/activerecord/test/cases/relation/where_chain_test.rb @@ -12,6 +12,22 @@ module ActiveRecord class WhereChainTest < ActiveRecord::TestCase fixtures :posts, :comments, :authors, :humans, :essays + def test_associated_with_association + Post.where.associated(:author).tap do |relation| + assert_includes relation, posts(:welcome) + assert_includes relation, posts(:sti_habtm) + assert_not_includes relation, posts(:authorless) + end + end + + def test_associated_with_multiple_associations + Post.where.associated(:author, :comments).tap do |relation| + assert_includes relation, posts(:welcome) + assert_not_includes relation, posts(:sti_habtm) + assert_not_includes relation, posts(:authorless) + end + end + def test_missing_with_association assert posts(:authorless).author.blank? assert_equal [posts(:authorless)], Post.where.missing(:author).to_a