1
0
Fork 0
mirror of https://github.com/rails/rails.git synced 2022-11-09 12:12:34 -05:00

Optimize ActiveRecord::Relation#include? on an unloaded relation

Perform an efficient existence query through #exists? instead of loading the entire relation into memory and searching it.

Before:

    > Person.where(name: "David").include?(david)
    SELECT `people`.* FROM `people` WHERE `people`.`name` = 'David'
    => true

After:

    > Person.where(name: "David").include?(david)
    SELECT 1 AS one FROM `people` WHERE `people`.`name` = 'David' AND `people`.`id` = 1 LIMIT 1
    => true
This commit is contained in:
George Claghorn 2020-10-02 21:52:07 -04:00 committed by GitHub
parent 614580270d
commit 166b63eb67
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 94 additions and 1 deletions

View file

@ -319,6 +319,21 @@ module ActiveRecord
skip_query_cache_if_necessary { connection.select_rows(relation.arel, "#{name} Exists?").size == 1 } skip_query_cache_if_necessary { connection.select_rows(relation.arel, "#{name} Exists?").size == 1 }
end end
# Returns true if the relation contains the given record or false otherwise.
#
# No query is performed if the relation is loaded; the given record is
# compared to the records in memory. If the relation is unloaded, an
# efficient existence query is performed, as in #exists?.
def include?(record)
if loaded?
records.include?(record)
else
record.is_a?(klass) && exists?(record.id)
end
end
alias :member? :include?
# This method is called whenever no records are found with either a single # This method is called whenever no records are found with either a single
# id or multiple ids and raises an ActiveRecord::RecordNotFound exception. # id or multiple ids and raises an ActiveRecord::RecordNotFound exception.
# #

View file

@ -375,6 +375,84 @@ class FinderTest < ActiveRecord::TestCase
end end
end end
def test_include_on_unloaded_relation_with_match
assert_sql(/1 AS one.*LIMIT/) do
assert_equal true, Customer.where(name: "David").include?(customers(:david))
end
end
def test_include_on_unloaded_relation_without_match
assert_sql(/1 AS one.*LIMIT/) do
assert_equal false, Customer.where(name: "David").include?(customers(:mary))
end
end
def test_include_on_unloaded_relation_with_mismatched_class
topic = topics(:first)
assert Customer.exists?(topic.id)
assert_no_queries do
assert_equal false, Customer.where(name: "David").include?(topic)
end
end
def test_include_on_loaded_relation_with_match
customers = Customer.where(name: "David").load
david = customers(:david)
assert_no_queries do
assert_equal true, customers.include?(david)
end
end
def test_include_on_loaded_relation_without_match
customers = Customer.where(name: "David").load
mary = customers(:mary)
assert_no_queries do
assert_equal false, customers.include?(mary)
end
end
def test_member_on_unloaded_relation_with_match
assert_sql(/1 AS one.*LIMIT/) do
assert_equal true, Customer.where(name: "David").member?(customers(:david))
end
end
def test_member_on_unloaded_relation_without_match
assert_sql(/1 AS one.*LIMIT/) do
assert_equal false, Customer.where(name: "David").member?(customers(:mary))
end
end
def test_member_on_unloaded_relation_with_mismatched_class
topic = topics(:first)
assert Customer.exists?(topic.id)
assert_no_queries do
assert_equal false, Customer.where(name: "David").member?(topic)
end
end
def test_member_on_loaded_relation_with_match
customers = Customer.where(name: "David").load
david = customers(:david)
assert_no_queries do
assert_equal true, customers.member?(david)
end
end
def test_member_on_loaded_relation_without_match
customers = Customer.where(name: "David").load
mary = customers(:mary)
assert_no_queries do
assert_equal false, customers.member?(mary)
end
end
def test_find_by_array_of_one_id def test_find_by_array_of_one_id
assert_kind_of(Array, Topic.find([ 1 ])) assert_kind_of(Array, Topic.find([ 1 ]))
assert_equal(1, Topic.find([ 1 ]).length) assert_equal(1, Topic.find([ 1 ]).length)

View file

@ -48,7 +48,7 @@ module ActiveRecord
QUERYING_METHODS = QUERYING_METHODS =
ActiveRecord::Batches.public_instance_methods(false) + ActiveRecord::Batches.public_instance_methods(false) +
ActiveRecord::Calculations.public_instance_methods(false) + ActiveRecord::Calculations.public_instance_methods(false) +
ActiveRecord::FinderMethods.public_instance_methods(false) - [:raise_record_not_found_exception!] + ActiveRecord::FinderMethods.public_instance_methods(false) - [:include?, :member?, :raise_record_not_found_exception!] +
ActiveRecord::SpawnMethods.public_instance_methods(false) - [:spawn, :merge!] + ActiveRecord::SpawnMethods.public_instance_methods(false) - [:spawn, :merge!] +
ActiveRecord::QueryMethods.public_instance_methods(false).reject { |method| ActiveRecord::QueryMethods.public_instance_methods(false).reject { |method|
method.end_with?("=", "!", "value", "values", "clause") method.end_with?("=", "!", "value", "values", "clause")