From bd3b28f7f181dce53e872daa23dda101498b8fb4 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Mon, 14 Apr 2014 16:26:28 -0700 Subject: cache scope building on associations SQL statements for querying associations are now cached --- .../associations/association_scope.rb | 55 +++++++++++++++++++--- .../associations/collection_association.rb | 16 ++++++- .../abstract/database_statements.rb | 6 ++- .../connection_adapters/mysql2_adapter.rb | 4 -- activerecord/lib/active_record/reflection.rb | 9 ++++ 5 files changed, 78 insertions(+), 12 deletions(-) (limited to 'activerecord/lib/active_record') diff --git a/activerecord/lib/active_record/associations/association_scope.rb b/activerecord/lib/active_record/associations/association_scope.rb index bb889a8f3b..f1a3b23d5a 100644 --- a/activerecord/lib/active_record/associations/association_scope.rb +++ b/activerecord/lib/active_record/associations/association_scope.rb @@ -1,12 +1,34 @@ module ActiveRecord module Associations class AssociationScope #:nodoc: - INSTANCE = new - def self.scope(association, connection) INSTANCE.scope association, connection end + class BindSubstitution + def initialize(block) + @block = block + end + + def bind_value(scope, column, value, alias_tracker) + substitute = alias_tracker.connection.substitute_at( + column, scope.bind_values.length) + scope.bind_values += [[column, @block.call(value)]] + substitute + end + end + + def self.create(&block) + block = block ? block : lambda { |val| val } + new BindSubstitution.new(block) + end + + def initialize(bind_substitution) + @bind_substitution = bind_substitution + end + + INSTANCE = create + def scope(association, connection) klass = association.klass reflection = association.reflection @@ -22,6 +44,30 @@ module ActiveRecord Arel::Nodes::InnerJoin end + def self.get_bind_values(owner, chain) + bvs = [] + chain.each_with_index do |reflection, i| + if reflection.source_macro == :belongs_to + foreign_key = reflection.foreign_key + else + foreign_key = reflection.active_record_primary_key + end + + if reflection == chain.last + bvs << owner[foreign_key] + + if reflection.type + bvs << owner.class.base_class.name + end + else + if reflection.type + bvs << chain[i + 1].klass.base_class.name + end + end + end + bvs + end + private def construct_tables(chain, klass, refl, alias_tracker) @@ -49,10 +95,7 @@ module ActiveRecord end def bind_value(scope, column, value, alias_tracker) - substitute = alias_tracker.connection.substitute_at( - column, scope.bind_values.length) - scope.bind_values += [[column, value]] - substitute + @bind_substitution.bind_value scope, column, value, alias_tracker end def bind(scope, table_name, column_name, value, tracker) diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb index 803e3ab9ab..b623f1375d 100644 --- a/activerecord/lib/active_record/associations/collection_association.rb +++ b/activerecord/lib/active_record/associations/collection_association.rb @@ -412,9 +412,23 @@ module ActiveRecord end private + def get_records + return scope.to_a if reflection.scope_chain.any?(&:any?) + + conn = klass.connection + sc = reflection.association_scope_cache(conn) do + StatementCache.create(conn) { |params| + as = AssociationScope.create { params.bind } + target_scope.merge as.scope(self, conn) + } + end + + binds = AssociationScope.get_bind_values(owner, reflection.chain) + sc.execute binds, klass, klass.connection + end def find_target - records = scope.to_a + records = get_records records.each { |record| set_inverse_instance(record) } records end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb index b7b9a4363e..bc47412405 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -19,7 +19,11 @@ module ActiveRecord # This is used in the StatementCache object. It returns an object that # can be used to query the database repeatedly. def cacheable_query(arel) # :nodoc: - ActiveRecord::StatementCache.query visitor, arel.ast + if prepared_statements + ActiveRecord::StatementCache.query visitor, arel.ast + else + ActiveRecord::StatementCache.partial_query visitor, arel.ast, collector + end end # Returns an ActiveRecord::Result instance. diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb index a9d260b98c..233af252d6 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -44,10 +44,6 @@ module ActiveRecord configure_connection end - def cacheable_query(arel) - ActiveRecord::StatementCache.partial_query visitor, arel.ast, collector - end - MAX_INDEX_LENGTH_FOR_UTF8MB4 = 191 def initialize_schema_migrations_table if @config[:encoding] == 'utf8mb4' diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index 1724ea95b0..3123839a10 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -1,3 +1,5 @@ +require 'thread' + module ActiveRecord # = Active Record Reflection module Reflection # :nodoc: @@ -199,6 +201,13 @@ module ActiveRecord @type = options[:as] && "#{options[:as]}_type" @foreign_type = options[:foreign_type] || "#{name}_type" @constructable = calculate_constructable(macro, options) + @association_scope_cache = {} + @scope_lock = Mutex.new + end + + def association_scope_cache(conn) + key = conn.prepared_statements + @association_scope_cache[key] ||= @scope_lock.synchronize { yield } end # Returns a new, unsaved instance of the associated class. +attributes+ will -- cgit v1.2.3