diff options
14 files changed, 75 insertions, 40 deletions
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index e957361ec3..f16a746b91 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -29,7 +29,7 @@ ``` config.active_record.database_selector = { delay: 2.seconds } config.active_record.database_resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver - config.active_record.database_operations = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session + config.active_record.database_resolver_context = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session ``` To change the database selection strategy, pass a custom class to the @@ -38,7 +38,7 @@ ``` config.active_record.database_selector = { delay: 10.seconds } config.active_record.database_resolver = MyResolver - config.active_record.database_operations = MyResolver::MyCookies + config.active_record.database_resolver_context = MyResolver::MyCookies ``` *Eileen M. Uchitelle* diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb index 68f53d5c1c..b0c0beac0e 100644 --- a/activerecord/lib/active_record/associations/collection_association.rb +++ b/activerecord/lib/active_record/associations/collection_association.rb @@ -347,7 +347,6 @@ module ActiveRecord add_to_target(record) do result = insert_record(record, true, raise) { @_was_loaded = loaded? - @association_ids = nil } end raise ActiveRecord::Rollback unless result @@ -384,6 +383,7 @@ module ActiveRecord delete_records(existing_records, method) if existing_records.any? @target -= records + @association_ids = nil records.each { |record| callback(:after_remove, record) } end @@ -424,7 +424,6 @@ module ActiveRecord unless owner.new_record? result &&= insert_record(record, true, raise) { @_was_loaded = loaded? - @association_ids = nil } end end @@ -447,6 +446,7 @@ module ActiveRecord if index target[index] = record elsif @_was_loaded || !loaded? + @association_ids = nil target << record end diff --git a/activerecord/lib/active_record/middleware/database_selector.rb b/activerecord/lib/active_record/middleware/database_selector.rb index 3ab50f5f6b..aad263695b 100644 --- a/activerecord/lib/active_record/middleware/database_selector.rb +++ b/activerecord/lib/active_record/middleware/database_selector.rb @@ -12,8 +12,8 @@ module ActiveRecord # # The resolver class defines when the application should switch (i.e. read # from the primary if a write occurred less than 2 seconds ago) and an - # operations class that sets a value that helps the resolver class decide - # when to switch. + # resolver context class that sets a value that helps the resolver class + # decide when to switch. # # Rails default middleware uses the request's session to set a timestamp # that informs the application when to read from a primary or read from a @@ -24,7 +24,7 @@ module ActiveRecord # # config.active_record.database_selector = { delay: 2.seconds } # config.active_record.database_resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver - # config.active_record.database_operations = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session + # config.active_record.database_resolver_context = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session # # New applications will include these lines commented out in the production.rb. # @@ -33,16 +33,16 @@ module ActiveRecord # # config.active_record.database_selector = { delay: 2.seconds } # config.active_record.database_resolver = MyResolver - # config.active_record.database_operations = MyResolver::MySession + # config.active_record.database_resolver_context = MyResolver::MySession class DatabaseSelector - def initialize(app, resolver_klass = Resolver, operations_klass = Resolver::Session, options = {}) + def initialize(app, resolver_klass = Resolver, context_klass = Resolver::Session, options = {}) @app = app @resolver_klass = resolver_klass - @operations_klass = operations_klass + @context_klass = context_klass @options = options end - attr_reader :resolver_klass, :operations_klass, :options + attr_reader :resolver_klass, :context_klass, :options # Middleware that determines which database connection to use in a multiple # database application. @@ -57,13 +57,13 @@ module ActiveRecord private def select_database(request, &blk) - operations = operations_klass.build(request) - database_resolver = resolver_klass.call(operations, options) + context = context_klass.call(request) + resolver = resolver_klass.call(context, options) if reading_request?(request) - database_resolver.read(&blk) + resolver.read(&blk) else - database_resolver.write(&blk) + resolver.write(&blk) end end diff --git a/activerecord/lib/active_record/middleware/database_selector/resolver.rb b/activerecord/lib/active_record/middleware/database_selector/resolver.rb index a84c292714..80b8cd7cae 100644 --- a/activerecord/lib/active_record/middleware/database_selector/resolver.rb +++ b/activerecord/lib/active_record/middleware/database_selector/resolver.rb @@ -18,18 +18,18 @@ module ActiveRecord class Resolver # :nodoc: SEND_TO_REPLICA_DELAY = 2.seconds - def self.call(resolver, options = {}) - new(resolver, options) + def self.call(context, options = {}) + new(context, options) end - def initialize(resolver, options = {}) - @resolver = resolver + def initialize(context, options = {}) + @context = context @options = options @delay = @options && @options[:delay] ? @options[:delay] : SEND_TO_REPLICA_DELAY @instrumenter = ActiveSupport::Notifications.instrumenter end - attr_reader :resolver, :delay, :instrumenter + attr_reader :context, :delay, :instrumenter def read(&blk) if read_from_primary? @@ -68,7 +68,7 @@ module ActiveRecord instrumenter.instrument("database_selector.active_record.wrote_to_primary") do yield ensure - resolver.update_last_write_timestamp + context.update_last_write_timestamp end end end @@ -82,7 +82,7 @@ module ActiveRecord end def time_since_last_write_ok? - Time.now - resolver.last_write_timestamp >= send_to_replica_delay + Time.now - context.last_write_timestamp >= send_to_replica_delay end end end diff --git a/activerecord/lib/active_record/middleware/database_selector/resolver/session.rb b/activerecord/lib/active_record/middleware/database_selector/resolver/session.rb index 33e0af5ee4..df7af054b7 100644 --- a/activerecord/lib/active_record/middleware/database_selector/resolver/session.rb +++ b/activerecord/lib/active_record/middleware/database_selector/resolver/session.rb @@ -10,7 +10,7 @@ module ActiveRecord # The last_write is used to determine whether it's safe to read # from the replica or the request needs to be sent to the primary. class Session # :nodoc: - def self.build(request) + def self.call(request) new(request.session) end diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb index aac49a92b4..a1d7c893bf 100644 --- a/activerecord/lib/active_record/railtie.rb +++ b/activerecord/lib/active_record/railtie.rb @@ -91,7 +91,7 @@ module ActiveRecord initializer "active_record.database_selector" do if options = config.active_record.delete(:database_selector) resolver = config.active_record.delete(:database_resolver) - operations = config.active_record.delete(:database_operations) + operations = config.active_record.delete(:database_resolver_context) config.app_middleware.use ActiveRecord::Middleware::DatabaseSelector, resolver, operations, options end end diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index 32f0609798..347d745d19 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -67,11 +67,7 @@ module ActiveRecord # user = users.new { |user| user.name = 'Oscar' } # user.name # => Oscar def new(attributes = nil, &block) - current_scope = klass.current_scope(true) - block = -> record do - klass.current_scope = current_scope - yield record if block_given? - end + block = klass.current_scope_restoring_block(&block) scoping { klass.new(attributes, &block) } end diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index 8f1065c1e7..91cfc4e849 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -307,8 +307,6 @@ module ActiveRecord return false if !conditions || limit_value == 0 - conditions = sanitize_forbidden_attributes(conditions) - if eager_loading? relation = apply_join_dependency(eager_loading: false) return relation.exists?(conditions) @@ -316,7 +314,7 @@ module ActiveRecord relation = construct_relation_for_exists(conditions) - skip_query_cache_if_necessary { connection.select_value(relation.arel, "#{name} Exists") } ? true : false + skip_query_cache_if_necessary { connection.select_one(relation.arel, "#{name} Exists") } ? true : false end # This method is called whenever no records are found with either a single @@ -354,7 +352,13 @@ module ActiveRecord end def construct_relation_for_exists(conditions) - relation = except(:select, :distinct, :order)._select!(ONE_AS_ONE).limit!(1) + conditions = sanitize_forbidden_attributes(conditions) + + if distinct_value && offset_value + relation = limit(1) + else + relation = except(:select, :distinct, :order)._select!(ONE_AS_ONE).limit!(1) + end case conditions when Array, Hash diff --git a/activerecord/lib/active_record/scoping.rb b/activerecord/lib/active_record/scoping.rb index 35e9dcbffc..1142a87d25 100644 --- a/activerecord/lib/active_record/scoping.rb +++ b/activerecord/lib/active_record/scoping.rb @@ -30,6 +30,14 @@ module ActiveRecord def current_scope=(scope) ScopeRegistry.set_value_for(:current_scope, self, scope) end + + def current_scope_restoring_block(&block) + current_scope = self.current_scope(true) + -> *args do + self.current_scope = current_scope + yield(*args) if block_given? + end + end end def populate_with_current_scope_attributes # :nodoc: diff --git a/activerecord/test/cases/associations/belongs_to_associations_test.rb b/activerecord/test/cases/associations/belongs_to_associations_test.rb index a61569420e..c01138c059 100644 --- a/activerecord/test/cases/associations/belongs_to_associations_test.rb +++ b/activerecord/test/cases/associations/belongs_to_associations_test.rb @@ -63,7 +63,8 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase ActiveRecord::SQLCounter.clear_log Client.find(3).firm ensure - assert ActiveRecord::SQLCounter.log_all.all? { |sql| /order by/i !~ sql }, "ORDER BY was used in the query" + sql_log = ActiveRecord::SQLCounter.log + assert sql_log.all? { |sql| /order by/i !~ sql }, "ORDER BY was used in the query: #{sql_log}" end def test_belongs_to_with_primary_key diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb index 5fdc5a92fc..bf44be1811 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -2004,6 +2004,21 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_not_predicate company.clients, :loaded? end + def test_ids_reader_cache_not_used_for_size_when_association_is_dirty + firm = Firm.create!(name: "Startup") + assert_equal 0, firm.client_ids.size + firm.clients.build + assert_equal 1, firm.clients.size + end + + def test_ids_reader_cache_should_be_cleared_when_collection_is_deleted + firm = companies(:first_firm) + assert_equal [2, 3, 11], firm.client_ids + client = firm.clients.first + firm.clients.delete(client) + assert_equal [3, 11], firm.client_ids + end + def test_zero_counter_cache_usage_on_unloaded_association car = Car.create!(name: "My AppliCar") assert_no_queries do diff --git a/activerecord/test/cases/associations/has_one_associations_test.rb b/activerecord/test/cases/associations/has_one_associations_test.rb index 3e5b5c1275..7bb629466d 100644 --- a/activerecord/test/cases/associations/has_one_associations_test.rb +++ b/activerecord/test/cases/associations/has_one_associations_test.rb @@ -37,8 +37,8 @@ class HasOneAssociationsTest < ActiveRecord::TestCase ActiveRecord::SQLCounter.clear_log companies(:first_firm).account ensure - log_all = ActiveRecord::SQLCounter.log_all - assert log_all.all? { |sql| /order by/i !~ sql }, "ORDER BY was used in the query: #{log_all}" + sql_log = ActiveRecord::SQLCounter.log + assert sql_log.all? { |sql| /order by/i !~ sql }, "ORDER BY was used in the query: #{sql_log}" end def test_has_one_cache_nils diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb index 6af2a43c7f..4040682280 100644 --- a/activerecord/test/cases/finder_test.rb +++ b/activerecord/test/cases/finder_test.rb @@ -271,6 +271,16 @@ class FinderTest < ActiveRecord::TestCase assert_equal true, Topic.exists?({}) end + def test_exists_with_distinct_and_offset_and_joins + assert Post.left_joins(:comments).distinct.offset(10).exists? + assert_not Post.left_joins(:comments).distinct.offset(11).exists? + end + + def test_exists_with_distinct_and_offset_and_select + assert Post.select(:body).distinct.offset(3).exists? + assert_not Post.select(:body).distinct.offset(4).exists? + end + # Ensure +exists?+ runs without an error by excluding distinct value. # See https://github.com/rails/rails/pull/26981. def test_exists_with_order_and_distinct diff --git a/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt index 94f7dd0c79..ed1cf09eeb 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt +++ b/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt @@ -107,9 +107,10 @@ Rails.application.configure do # The `database_resolver` class is used by the middleware to determine which # database is appropriate to use based on the time delay. # - # The `database_operations` class is used by the middleware to set timestamps - # for the last write to the primary. The resolver uses the operations class - # timestamps to determine how long to wait before reading from the replica. + # The `database_resolver_context` class is used by the middleware to set + # timestamps for the last write to the primary. The resolver uses the context + # class timestamps to determine how long to wait before reading from the + # replica. # # By default Rails will store a last write timestamp in the session. The # DatabaseSelector middleware is designed as such you can define your own @@ -117,5 +118,5 @@ Rails.application.configure do # these configuration options. # config.active_record.database_selector = { delay: 2.seconds } # config.active_record.database_resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver - # config.active_record.database_operations = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session + # config.active_record.database_resolver_context = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session end |