From a04486dc997979a2d87fc013d30b6e71a3df4a64 Mon Sep 17 00:00:00 2001 From: Pratik Naik Date: Tue, 12 Jan 2010 22:20:53 +0530 Subject: Delay building arel relation as long as possible for improved introspection --- activerecord/lib/active_record/associations.rb | 15 +- activerecord/lib/active_record/base.rb | 4 +- activerecord/lib/active_record/relation.rb | 72 +++++---- .../active_record/relation/calculation_methods.rb | 14 +- .../lib/active_record/relation/finder_methods.rb | 8 +- .../lib/active_record/relation/query_methods.rb | 165 +++++++++++---------- .../lib/active_record/relation/spawn_methods.rb | 89 +++++------ 7 files changed, 182 insertions(+), 185 deletions(-) diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index aceb83044b..d748500340 100755 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -2148,22 +2148,23 @@ module ActiveRecord end def relation + aliased = Arel::Table.new(table_name, :as => @aliased_table_name) + if reflection.macro == :has_and_belongs_to_many - [Arel::Table.new(table_alias_for(options[:join_table], aliased_join_table_name)), Arel::Table.new(table_name_and_alias)] + [Arel::Table.new(options[:join_table], :as => aliased_join_table_name), aliased] elsif reflection.options[:through] - [Arel::Table.new(table_alias_for(through_reflection.klass.table_name, aliased_join_table_name)), Arel::Table.new(table_name_and_alias)] + [Arel::Table.new(through_reflection.klass.table_name, :as => aliased_join_table_name), aliased] else - Arel::Table.new(table_name_and_alias) + aliased end end def join_relation(joining_relation, join = nil) if (relations = relation).is_a?(Array) - joining_relation. - joins(relations.first, Arel::OuterJoin).on(association_join.first). - joins(relations.last, Arel::OuterJoin).on(association_join.last) + joining_relation.joins(Relation::JoinOperation.new(relations.first, Arel::OuterJoin, association_join.first)). + joins(Relation::JoinOperation.new(relations.last, Arel::OuterJoin, association_join.last)) else - joining_relation.joins(relations, Arel::OuterJoin).on(association_join) + joining_relation.joins(Relation::JoinOperation.new(relations, Arel::OuterJoin, association_join)) end end diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index 026bf55aaa..0ecef4abb4 100755 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -1663,7 +1663,7 @@ module ActiveRecord #:nodoc: def build_association_joins(joins) join_dependency = ActiveRecord::Associations::ClassMethods::JoinDependency.new(self, joins, nil) - relation = active_relation.relation + relation = active_relation.table join_dependency.join_associations.map { |association| if (association_relation = association.relation).is_a?(Array) [Arel::InnerJoin.new(relation, association_relation.first, association.association_join.first).joins(relation), @@ -2039,7 +2039,7 @@ module ActiveRecord #:nodoc: def sanitize_sql_hash_for_conditions(attrs, default_table_name = self.table_name) attrs = expand_hash_conditions_for_aggregates(attrs) - table = Arel::Table.new(default_table_name, active_relation_engine) + table = Arel::Table.new(self.table_name, :engine => active_relation_engine, :as => default_table_name) builder = PredicateBuilder.new(active_relation_engine) builder.build_from_hash(attrs, table).map(&:to_sql).join(' AND ') end diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index 487b54f27d..33127194b0 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -1,19 +1,19 @@ module ActiveRecord class Relation - include QueryMethods, FinderMethods, CalculationMethods, SpawnMethods + JoinOperation = Struct.new(:relation, :join_class, :on) + ASSOCIATION_METHODS = [:includes, :eager_load, :preload] + MULTI_VALUE_METHODS = [:select, :group, :order, :joins, :where, :having] + SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :create_with, :from] + + include FinderMethods, CalculationMethods, SpawnMethods, QueryMethods delegate :length, :collect, :map, :each, :all?, :to => :to_a - attr_reader :relation, :klass - attr_writer :readonly, :table - attr_accessor :preload_associations, :eager_load_associations, :includes_associations, :create_with_attributes + attr_reader :table, :klass - def initialize(klass, relation) - @klass, @relation = klass, relation - @preload_associations = [] - @eager_load_associations = [] - @includes_associations = [] - @loaded, @readonly = false + def initialize(klass, table) + @klass, @table = klass, table + (ASSOCIATION_METHODS + MULTI_VALUE_METHODS).each {|v| instance_variable_set(:"@#{v}_values", [])} end def new(*args, &block) @@ -29,7 +29,7 @@ module ActiveRecord end def respond_to?(method, include_private = false) - return true if @relation.respond_to?(method, include_private) || Array.method_defined?(method) + return true if arel.respond_to?(method, include_private) || Array.method_defined?(method) if match = DynamicFinderMatch.match(method) return true if @klass.send(:all_attributes_exists?, match.attribute_names) @@ -43,33 +43,35 @@ module ActiveRecord def to_a return @records if loaded? - find_with_associations = @eager_load_associations.any? || references_eager_loaded_tables? + find_with_associations = @eager_load_values.any? || (@includes_values.any? && references_eager_loaded_tables?) @records = if find_with_associations begin @klass.send(:find_with_associations, { - :select => @relation.send(:select_clauses).join(', '), - :joins => @relation.joins(relation), - :group => @relation.send(:group_clauses).join(', '), + :select => arel.send(:select_clauses).join(', '), + :joins => arel.joins(arel), + :group => arel.send(:group_clauses).join(', '), :order => order_clause, :conditions => where_clause, - :limit => @relation.taken, - :offset => @relation.skipped, - :from => (@relation.send(:from_clauses) if @relation.send(:sources).present?) + :limit => arel.taken, + :offset => arel.skipped, + :from => (arel.send(:from_clauses) if arel.send(:sources).present?) }, - ActiveRecord::Associations::ClassMethods::JoinDependency.new(@klass, @eager_load_associations + @includes_associations, nil)) + ActiveRecord::Associations::ClassMethods::JoinDependency.new(@klass, @eager_load_values + @includes_values, nil)) rescue ThrowResult [] end else - @klass.find_by_sql(@relation.to_sql) + @klass.find_by_sql(arel.to_sql) end - preload = @preload_associations - preload += @includes_associations unless find_with_associations + preload = @preload_values + preload += @includes_values unless find_with_associations preload.each {|associations| @klass.send(:preload_associations, @records, associations) } - @records.each { |record| record.readonly! } if @readonly + # @readonly_value is true only if set explicity. @implicit_readonly is true if there are JOINS and no explicit SELECT. + readonly = @readonly_value.nil? ? @implicit_readonly : @readonly_value + @records.each { |record| record.readonly! } if readonly @loaded = true @records @@ -97,7 +99,7 @@ module ActiveRecord if block_given? to_a.many? { |*block_args| yield(*block_args) } else - @relation.send(:taken).present? ? to_a.many? : size > 1 + arel.send(:taken).present? ? to_a.many? : size > 1 end end @@ -107,7 +109,7 @@ module ActiveRecord end def delete_all - @relation.delete.tap { reset } + arel.delete.tap { reset } end def delete(id_or_array) @@ -124,28 +126,24 @@ module ActiveRecord end def reset - @first = @last = @to_sql = @order_clause = @scope_for_create = nil + @first = @last = @to_sql = @order_clause = @scope_for_create = @arel = nil @records = [] self end - def table - @table ||= Arel::Table.new(@klass.table_name, :engine => @klass.active_relation_engine) - end - def primary_key @primary_key ||= table[@klass.primary_key] end def to_sql - @to_sql ||= @relation.to_sql + @to_sql ||= arel.to_sql end protected def method_missing(method, *args, &block) - if @relation.respond_to?(method) - @relation.send(method, *args, &block) + if arel.respond_to?(method) + arel.send(method, *args, &block) elsif Array.method_defined?(method) to_a.send(method, *args, &block) elsif match = DynamicFinderMatch.match(method) @@ -168,7 +166,7 @@ module ActiveRecord def scope_for_create @scope_for_create ||= begin - @create_with_attributes || wheres.inject({}) do |hash, where| + @create_with_value || wheres.inject({}) do |hash, where| hash[where.operand1.name] = where.operand2.value if where.is_a?(Arel::Predicates::Equality) hash end @@ -176,15 +174,15 @@ module ActiveRecord end def where_clause(join_string = " AND ") - @relation.send(:where_clauses).join(join_string) + arel.send(:where_clauses).join(join_string) end def order_clause - @order_clause ||= @relation.send(:order_clauses).join(', ') + @order_clause ||= arel.send(:order_clauses).join(', ') end def references_eager_loaded_tables? - joined_tables = (tables_in_string(@relation.joins(relation)) + [table.name, table.table_alias]).compact.uniq + joined_tables = (tables_in_string(arel.joins(arel)) + [table.name, table.table_alias]).compact.uniq (tables_in_string(to_sql) - joined_tables).any? end diff --git a/activerecord/lib/active_record/relation/calculation_methods.rb b/activerecord/lib/active_record/relation/calculation_methods.rb index 5246c7bc5d..e6f62ee49a 100644 --- a/activerecord/lib/active_record/relation/calculation_methods.rb +++ b/activerecord/lib/active_record/relation/calculation_methods.rb @@ -25,7 +25,7 @@ module ActiveRecord operation = operation.to_s.downcase if operation == "count" - joins = @relation.joins(relation) + joins = arel.joins(arel) if joins.present? && joins =~ /LEFT OUTER/i distinct = true column_name = @klass.primary_key if column_name == :all @@ -40,7 +40,7 @@ module ActiveRecord distinct = options[:distinct] || distinct column_name = :all if column_name.blank? && operation == "count" - if @relation.send(:groupings).any? + if arel.send(:groupings).any? return execute_grouped_calculation(operation, column_name) else return execute_simple_calculation(operation, column_name, distinct) @@ -63,7 +63,7 @@ module ActiveRecord end def execute_grouped_calculation(operation, column_name) #:nodoc: - group_attr = @relation.send(:groupings).first.value + group_attr = arel.send(:groupings).first.value association = @klass.reflect_on_association(group_attr.to_sym) associated = association && association.macro == :belongs_to # only count belongs_to associations group_field = associated ? association.primary_key_name : group_attr @@ -165,12 +165,8 @@ module ActiveRecord column ? column.type_cast(value) : value end - def get_projection_name_from_chained_relations(relation = @relation) - if relation.respond_to?(:projections) && relation.projections.present? - relation.send(:select_clauses).join(', ') - elsif relation.respond_to?(:relation) - get_projection_name_from_chained_relations(relation.relation) - end + def get_projection_name_from_chained_relations + @select_values.join(", ") if @select_values.present? end end diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index c3e5f27838..3668b0997f 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -93,15 +93,15 @@ module ActiveRecord result = where(primary_key.in(ids)).all expected_size = - if @relation.taken && ids.size > @relation.taken - @relation.taken + if arel.taken && ids.size > arel.taken + arel.taken else ids.size end # 11 ids with limit 3, offset 9 should give 2 results. - if @relation.skipped && (ids.size - @relation.skipped < expected_size) - expected_size = ids.size - @relation.skipped + if arel.skipped && (ids.size - arel.skipped < expected_size) + expected_size = ids.size - arel.skipped end if result.size == expected_size diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index 5d7bf0b7bc..c07eca44e3 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -1,68 +1,42 @@ module ActiveRecord module QueryMethods - - def preload(*associations) - spawn.tap {|r| r.preload_associations += Array.wrap(associations) } - end - - def includes(*associations) - spawn.tap {|r| r.includes_associations += Array.wrap(associations) } - end - - def eager_load(*associations) - spawn.tap {|r| r.eager_load_associations += Array.wrap(associations) } - end - - def readonly(status = true) - spawn.tap {|r| r.readonly = status } - end - - def create_with(attributes = {}) - spawn.tap {|r| r.create_with_attributes = attributes } - end - - def select(selects) - if selects.present? - relation = spawn(@relation.project(selects)) - relation.readonly = @relation.joins(relation).present? ? false : @readonly - relation - else - spawn + extend ActiveSupport::Concern + + included do + (ActiveRecord::Relation::ASSOCIATION_METHODS + ActiveRecord::Relation::MULTI_VALUE_METHODS).each do |query_method| + attr_accessor :"#{query_method}_values" + + class_eval <<-CEVAL + def #{query_method}(*args) + spawn.tap do |new_relation| + new_relation.#{query_method}_values ||= [] + value = args.size > 1 ? [args] : Array.wrap(args) + new_relation.#{query_method}_values += value + end + end + CEVAL end - end - - def from(from) - from.present? ? spawn(@relation.from(from)) : spawn - end - def having(*args) - return spawn if args.blank? + ActiveRecord::Relation::SINGLE_VALUE_METHODS.each do |query_method| + attr_accessor :"#{query_method}_value" - if [String, Hash, Array].include?(args.first.class) - havings = @klass.send(:merge_conditions, args.size > 1 ? Array.wrap(args) : args.first) - else - havings = args.first + class_eval <<-CEVAL + def #{query_method}(value = true) + spawn.tap do |new_relation| + new_relation.#{query_method}_value = value + end + end + CEVAL end - - spawn(@relation.having(havings)) - end - - def group(groups) - groups.present? ? spawn(@relation.group(groups)) : spawn - end - - def order(orders) - orders.present? ? spawn(@relation.order(orders)) : spawn end def lock(locks = true) + relation = spawn case locks - when String - spawn(@relation.lock(locks)) - when TrueClass, NilClass - spawn(@relation.lock) + when String, TrueClass, NilClass + spawn.tap {|new_relation| new_relation.lock_value = locks || true } else - spawn + spawn.tap {|new_relation| new_relation.lock_value = false } end end @@ -70,7 +44,7 @@ module ActiveRecord relation = spawn relation.instance_variable_set(:@orders, nil) - order_clause = @relation.send(:order_clauses).join(', ') + order_clause = arel.send(:order_clauses).join(', ') if order_clause.present? relation.order(reverse_sql_order(order_clause)) else @@ -78,39 +52,74 @@ module ActiveRecord end end - def limit(limits) - limits.present? ? spawn(@relation.take(limits)) : spawn + def arel + @arel ||= build_arel end - def offset(offsets) - offsets.present? ? spawn(@relation.skip(offsets)) : spawn - end + def build_arel + arel = table - def on(join) - spawn(@relation.on(join)) - end + @joins_values.each do |j| + next if j.blank? - def joins(join, join_type = nil) - return spawn if join.blank? + @implicit_readonly = true - join_relation = case join - when String - @relation.join(join) - when Hash, Array, Symbol - if @klass.send(:array_of_strings?, join) - @relation.join(join.join(' ')) + case j + when Relation::JoinOperation + arel = arel.join(j.relation, j.join_class).on(j.on) + when Hash, Array, Symbol + if @klass.send(:array_of_strings?, j) + arel = arel.join(j.join(' ')) + else + arel = arel.join(@klass.send(:build_association_joins, j)) + end else - @relation.join(@klass.send(:build_association_joins, join)) + arel = arel.join(j) end - else - @relation.join(join, join_type) end - spawn(join_relation).tap { |r| r.readonly = true } + @where_values.each do |where| + if conditions = build_where(where) + arel = conditions.is_a?(String) ? arel.where(conditions) : arel.where(*conditions) + end + end + + @having_values.each do |where| + if conditions = build_where(where) + arel = conditions.is_a?(String) ? arel.having(conditions) : arel.having(*conditions) + end + end + + arel = arel.take(@limit_value) if @limit_value.present? + arel = arel.skip(@offset_value) if @offset_value.present? + + @group_values.each do |g| + arel = arel.group(g) if g.present? + end + + @order_values.each do |o| + arel = arel.order(o) if o.present? + end + + @select_values.each do |s| + @implicit_readonly = false + arel = arel.project(s) if s.present? + end + + arel = arel.from(@from_value) if @from_value.present? + + case @lock_value + when TrueClass + arel = arel.lock + when String + arel = arel.lock(@lock_value) + end + + arel end - def where(*args) - return spawn if args.blank? + def build_where(*args) + return if args.blank? builder = PredicateBuilder.new(Arel::Sql::Engine.new(@klass)) @@ -124,7 +133,7 @@ module ActiveRecord args.first end - conditions.is_a?(String) ? spawn(@relation.where(conditions)) : spawn(@relation.where(*conditions)) + conditions end private diff --git a/activerecord/lib/active_record/relation/spawn_methods.rb b/activerecord/lib/active_record/relation/spawn_methods.rb index 4ecee8c634..66eae69d92 100644 --- a/activerecord/lib/active_record/relation/spawn_methods.rb +++ b/activerecord/lib/active_record/relation/spawn_methods.rb @@ -1,49 +1,51 @@ module ActiveRecord module SpawnMethods - def spawn(relation = @relation) - relation = Relation.new(@klass, relation) - relation.readonly = @readonly - relation.preload_associations = @preload_associations - relation.eager_load_associations = @eager_load_associations - relation.includes_associations = @includes_associations - relation.create_with_attributes = @create_with_attributes - relation.table = table + def spawn(arel_table = self.table) + relation = Relation.new(@klass, arel_table) + + (Relation::ASSOCIATION_METHODS + Relation::MULTI_VALUE_METHODS).each do |query_method| + relation.send(:"#{query_method}_values=", send(:"#{query_method}_values")) + end + + Relation::SINGLE_VALUE_METHODS.each do |query_method| + relation.send(:"#{query_method}_value=", send(:"#{query_method}_value")) + end + relation end def merge(r) raise ArgumentError, "Cannot merge a #{r.klass.name} relation with #{@klass.name} relation" if r.klass != @klass - merged_relation = spawn(table).eager_load(r.eager_load_associations).preload(r.preload_associations).includes(r.includes_associations) - merged_relation.readonly = r.readonly - - [self.relation, r.relation].each do |arel| - merged_relation = merged_relation. - joins(arel.joins(arel)). - group(arel.groupings). - limit(arel.taken). - offset(arel.skipped). - select(arel.send(:select_clauses)). - from(arel.sources). - having(arel.havings). - lock(arel.locked) - end + merged_relation = spawn.eager_load(r.eager_load_values).preload(r.preload_values).includes(r.includes_values) + + merged_relation.readonly_value = r.readonly_value unless merged_relation.readonly_value + merged_relation.limit_value = r.limit_value unless merged_relation.limit_value + merged_relation.lock_value = r.lock_value unless merged_relation.lock_value + + merged_relation = merged_relation. + joins(r.joins_values). + group(r.group_values). + offset(r.offset_value). + select(r.select_values). + from(r.from_value). + having(r.having_values) - relation_order = r.send(:order_clause) - merged_order = relation_order.present? ? relation_order : order_clause - merged_relation = merged_relation.order(merged_order) + relation_order = r.order_values + merged_order = relation_order.present? ? relation_order : order_values + merged_relation.order_values = merged_order - merged_relation.create_with_attributes = @create_with_attributes + merged_relation.create_with_value = @create_with_value - if @create_with_attributes && r.create_with_attributes - merged_relation.create_with_attributes = @create_with_attributes.merge(r.create_with_attributes) + if @create_with_value && r.create_with_value + merged_relation.create_with_value = @create_with_value.merge(r.create_with_value) else - merged_relation.create_with_attributes = r.create_with_attributes || @create_with_attributes + merged_relation.create_with_value = r.create_with_value || @create_with_value end - merged_wheres = @relation.wheres + merged_wheres = @where_values - r.wheres.each do |w| + r.where_values.each do |w| if w.is_a?(Arel::Predicates::Equality) merged_wheres = merged_wheres.reject {|p| p.is_a?(Arel::Predicates::Equality) && p.operand1.name == w.operand1.name } end @@ -51,32 +53,23 @@ module ActiveRecord merged_wheres << w end - merged_relation.where(*merged_wheres) + merged_relation.where_values = merged_wheres + + merged_relation end alias :& :merge def except(*skips) result = Relation.new(@klass, table) - result.table = table - [:eager_load, :preload, :includes].each do |load_method| - result = result.send(load_method, send(:"#{load_method}_associations")) + (Relation::ASSOCIATION_METHODS + Relation::MULTI_VALUE_METHODS).each do |method| + result.send(:"#{method}_values=", send(:"#{method}_values")) unless skips.include?(method) end - result.readonly = self.readonly unless skips.include?(:readonly) - result.create_with_attributes = @create_with_attributes unless skips.include?(:create_with) - - result = result.joins(@relation.joins(@relation)) unless skips.include?(:joins) - result = result.group(@relation.groupings) unless skips.include?(:group) - result = result.limit(@relation.taken) unless skips.include?(:limit) - result = result.offset(@relation.skipped) unless skips.include?(:offset) - result = result.select(@relation.send(:select_clauses)) unless skips.include?(:select) - result = result.from(@relation.sources) unless skips.include?(:from) - result = result.order(order_clause) unless skips.include?(:order) - result = result.where(*@relation.wheres) unless skips.include?(:where) - result = result.having(*@relation.havings) unless skips.include?(:having) - result = result.lock(@relation.locked) unless skips.include?(:lock) + Relation::SINGLE_VALUE_METHODS.each do |method| + result.send(:"#{method}_value=", send(:"#{method}_value")) unless skips.include?(method) + end result end -- cgit v1.2.3