diff options
Diffstat (limited to 'activerecord/lib/active_record/associations')
21 files changed, 186 insertions, 122 deletions
diff --git a/activerecord/lib/active_record/associations/alias_tracker.rb b/activerecord/lib/active_record/associations/alias_tracker.rb index 0248c7483c..84540a7000 100644 --- a/activerecord/lib/active_record/associations/alias_tracker.rb +++ b/activerecord/lib/active_record/associations/alias_tracker.rb @@ -5,12 +5,13 @@ module ActiveRecord # Keeps track of table aliases for ActiveRecord::Associations::ClassMethods::JoinDependency and # ActiveRecord::Associations::ThroughAssociationScope class AliasTracker # :nodoc: - attr_reader :aliases, :table_joins + attr_reader :aliases, :table_joins, :connection # table_joins is an array of arel joins which might conflict with the aliases we assign here - def initialize(table_joins = []) + def initialize(connection = ActiveRecord::Model.connection, table_joins = []) @aliases = Hash.new { |h,k| h[k] = initial_count_for(k) } @table_joins = table_joins + @connection = connection end def aliased_table_for(table_name, aliased_name = nil) @@ -70,10 +71,6 @@ module ActiveRecord def truncate(name) name.slice(0, connection.table_alias_length - 2) end - - def connection - ActiveRecord::Base.connection - end end end end diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb index d1e3ff8e38..e75003f261 100644 --- a/activerecord/lib/active_record/associations/association.rb +++ b/activerecord/lib/active_record/associations/association.rb @@ -25,9 +25,7 @@ module ActiveRecord def initialize(owner, reflection) reflection.check_validity! - @target = nil @owner, @reflection = owner, reflection - @updated = false reset reset_scope @@ -38,14 +36,14 @@ module ActiveRecord # post.comments.aliased_table_name # => "comments" # def aliased_table_name - reflection.klass.table_name + klass.table_name end # Resets the \loaded flag to +false+ and sets the \target to +nil+. def reset @loaded = false - IdentityMap.remove(target) if IdentityMap.enabled? && target @target = nil + @stale_state = nil end # Reloads the \target and returns +self+ on success. @@ -134,17 +132,8 @@ module ActiveRecord # ActiveRecord::RecordNotFound is rescued within the method, and it is # not reraised. The proxy is \reset and +nil+ is the return value. def load_target - if find_target? - begin - if IdentityMap.enabled? && association_class && association_class.respond_to?(:base_class) - @target = IdentityMap.get(association_class, owner[reflection.foreign_key]) - end - rescue NameError - nil - ensure - @target ||= find_target - end - end + @target = find_target if (@stale_state && stale_target?) || find_target? + loaded! unless loaded? target rescue ActiveRecord::RecordNotFound @@ -225,16 +214,10 @@ module ActiveRecord def stale_state end - def association_class - @reflection.klass - end - def build_record(attributes, options) reflection.build_association(attributes, options) do |record| - record.assign_attributes( - create_scope.except(*record.changed), - :without_protection => true - ) + attributes = create_scope.except(*(record.changed - [reflection.foreign_key])) + record.assign_attributes(attributes, :without_protection => true) end end end diff --git a/activerecord/lib/active_record/associations/association_scope.rb b/activerecord/lib/active_record/associations/association_scope.rb index 6f8b76abda..5a44d3a156 100644 --- a/activerecord/lib/active_record/associations/association_scope.rb +++ b/activerecord/lib/active_record/associations/association_scope.rb @@ -10,29 +10,47 @@ module ActiveRecord def initialize(association) @association = association - @alias_tracker = AliasTracker.new + @alias_tracker = AliasTracker.new klass.connection end def scope scope = klass.unscoped - scope = scope.extending(*Array.wrap(options[:extend])) + + scope.extending!(*Array(options[:extend])) # It's okay to just apply all these like this. The options will only be present if the # association supports that option; this is enforced by the association builder. - scope = scope.apply_finder_options(options.slice( - :readonly, :include, :order, :limit, :joins, :group, :having, :offset, :select)) + scope.merge!(options.slice( + :readonly, :references, :order, :limit, :joins, :group, :having, :offset, :select, :uniq)) - if options[:through] && !options[:include] - scope = scope.includes(source_options[:include]) + if options[:include] + scope.includes! options[:include] + elsif options[:through] + scope.includes! source_options[:include] end - scope = scope.uniq if options[:uniq] - add_constraints(scope) end private + def column_for(table_name, column_name) + columns = alias_tracker.connection.schema_cache.columns_hash[table_name] + columns[column_name] + end + + def bind_value(scope, column, value) + substitute = alias_tracker.connection.substitute_at( + column, scope.bind_values.length) + scope.bind_values += [[column, value]] + substitute + end + + def bind(scope, table_name, column_name, value) + column = column_for table_name, column_name + bind_value scope, column, value + end + def add_constraints(scope) tables = construct_tables @@ -67,10 +85,13 @@ module ActiveRecord conditions = self.conditions[i] if reflection == chain.last - scope = scope.where(table[key].eq(owner[foreign_key])) + bind_val = bind scope, table.table_name, key.to_s, owner[foreign_key] + scope = scope.where(table[key].eq(bind_val)) if reflection.type - scope = scope.where(table[reflection.type].eq(owner.class.base_class.name)) + value = owner.class.base_class.name + bind_val = bind scope, table.table_name, reflection.type.to_s, value + scope = scope.where(table[reflection.type].eq(bind_val)) end conditions.each do |condition| @@ -90,8 +111,11 @@ module ActiveRecord scope = scope.joins(join(foreign_table, constraint)) - unless conditions.empty? - scope = scope.where(sanitize(conditions, table)) + conditions.each do |condition| + condition = interpolate(condition) + condition = { (table.table_alias || table.name) => condition } unless i == 0 + + scope = scope.where(condition) end end end diff --git a/activerecord/lib/active_record/associations/belongs_to_association.rb b/activerecord/lib/active_record/associations/belongs_to_association.rb index 97f531d064..ddfc6f6c05 100644 --- a/activerecord/lib/active_record/associations/belongs_to_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_association.rb @@ -14,6 +14,11 @@ module ActiveRecord self.target = record end + def reset + super + @updated = false + end + def updated? @updated end @@ -72,7 +77,7 @@ module ActiveRecord end def stale_state - owner[reflection.foreign_key].to_s + owner[reflection.foreign_key] && owner[reflection.foreign_key].to_s end end end diff --git a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb index 2ee5dbbd70..88ce03a3cd 100644 --- a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb @@ -27,7 +27,8 @@ module ActiveRecord end def stale_state - [super, owner[reflection.foreign_type].to_s] + foreign_key = super + foreign_key && [foreign_key.to_s, owner[reflection.foreign_type].to_s] end end end diff --git a/activerecord/lib/active_record/associations/builder/association.rb b/activerecord/lib/active_record/associations/builder/association.rb index d4f59100e8..2059d8acdf 100644 --- a/activerecord/lib/active_record/associations/builder/association.rb +++ b/activerecord/lib/active_record/associations/builder/association.rb @@ -1,7 +1,7 @@ module ActiveRecord::Associations::Builder class Association #:nodoc: class_attribute :valid_options - self.valid_options = [:class_name, :foreign_key, :select, :conditions, :include, :extend, :readonly, :validate] + self.valid_options = [:class_name, :foreign_key, :select, :conditions, :include, :extend, :readonly, :validate, :references] # Set by subclasses class_attribute :macro @@ -51,5 +51,36 @@ module ActiveRecord::Associations::Builder association(name).writer(value) end end + + def dependent_restrict_raises? + ActiveRecord::Base.dependent_restrict_raises == true + end + + def dependent_restrict_deprecation_warning + if dependent_restrict_raises? + msg = "In the next release, `:dependent => :restrict` will not raise a `DeleteRestrictionError`. "\ + "Instead, it will add an error on the model. To fix this warning, make sure your code " \ + "isn't relying on a `DeleteRestrictionError` and then add " \ + "`config.active_record.dependent_restrict_raises = false` to your application config." + ActiveSupport::Deprecation.warn msg + end + end + + def define_restrict_dependency_method + name = self.name + mixin.redefine_method(dependency_method_name) do + # has_many or has_one associations + if send(name).respond_to?(:exists?) ? send(name).exists? : !send(name).nil? + if dependent_restrict_raises? + raise ActiveRecord::DeleteRestrictionError.new(name) + else + key = association(name).reflection.macro == :has_one ? "one" : "many" + errors.add(:base, :"restrict_dependent_destroy.#{key}", + :record => self.class.human_attribute_name(name).downcase) + return false + end + end + end + end end end diff --git a/activerecord/lib/active_record/associations/builder/belongs_to.rb b/activerecord/lib/active_record/associations/builder/belongs_to.rb index 1759a41d93..4183c222de 100644 --- a/activerecord/lib/active_record/associations/builder/belongs_to.rb +++ b/activerecord/lib/active_record/associations/builder/belongs_to.rb @@ -33,8 +33,10 @@ module ActiveRecord::Associations::Builder method_name = "belongs_to_counter_cache_before_destroy_for_#{name}" mixin.redefine_method(method_name) do - record = send(name) - record.class.decrement_counter(cache_column, record.id) unless record.nil? + unless marked_for_destruction? + record = send(name) + record.class.decrement_counter(cache_column, record.id) unless record.nil? + end end model.before_destroy(method_name) diff --git a/activerecord/lib/active_record/associations/builder/collection_association.rb b/activerecord/lib/active_record/associations/builder/collection_association.rb index 35f9a3ae8e..768f70b6c9 100644 --- a/activerecord/lib/active_record/associations/builder/collection_association.rb +++ b/activerecord/lib/active_record/associations/builder/collection_association.rb @@ -32,7 +32,7 @@ module ActiveRecord::Associations::Builder private def wrap_block_extension - options[:extend] = Array.wrap(options[:extend]) + options[:extend] = Array(options[:extend]) if block_extension silence_warnings do @@ -51,7 +51,7 @@ module ActiveRecord::Associations::Builder # TODO : why do i need method_defined? I think its because of the inheritance chain model.class_attribute full_callback_name.to_sym unless model.method_defined?(full_callback_name) - model.send("#{full_callback_name}=", Array.wrap(options[callback_name.to_sym])) + model.send("#{full_callback_name}=", Array(options[callback_name.to_sym])) end def define_readers diff --git a/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb b/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb index 30fc44b4c2..0b634ab944 100644 --- a/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb +++ b/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb @@ -18,7 +18,7 @@ module ActiveRecord::Associations::Builder model.send(:include, Module.new { class_eval <<-RUBY, __FILE__, __LINE__ + 1 def destroy_associations - association(#{name.to_sym.inspect}).delete_all + association(#{name.to_sym.inspect}).delete_all_on_destroy super end RUBY diff --git a/activerecord/lib/active_record/associations/builder/has_many.rb b/activerecord/lib/active_record/associations/builder/has_many.rb index d29a525b9e..9ddfd433e4 100644 --- a/activerecord/lib/active_record/associations/builder/has_many.rb +++ b/activerecord/lib/active_record/associations/builder/has_many.rb @@ -21,6 +21,7 @@ module ActiveRecord::Associations::Builder ":nullify or :restrict (#{options[:dependent].inspect})" end + dependent_restrict_deprecation_warning if options[:dependent] == :restrict send("define_#{options[:dependent]}_dependency_method") model.before_destroy dependency_method_name end @@ -31,12 +32,7 @@ module ActiveRecord::Associations::Builder mixin.redefine_method(dependency_method_name) do send(name).each do |o| # No point in executing the counter update since we're going to destroy the parent anyway - counter_method = ('belongs_to_counter_cache_before_destroy_for_' + self.class.name.downcase).to_sym - if o.respond_to?(counter_method) - class << o - self - end.send(:define_method, counter_method, Proc.new {}) - end + o.mark_for_destruction end send(name).delete_all @@ -46,15 +42,14 @@ module ActiveRecord::Associations::Builder def define_delete_all_dependency_method name = self.name mixin.redefine_method(dependency_method_name) do - send(name).delete_all + association(name).delete_all_on_destroy end end - alias :define_nullify_dependency_method :define_delete_all_dependency_method - def define_restrict_dependency_method + def define_nullify_dependency_method name = self.name mixin.redefine_method(dependency_method_name) do - raise ActiveRecord::DeleteRestrictionError.new(name) unless send(name).empty? + send(name).delete_all end end diff --git a/activerecord/lib/active_record/associations/builder/has_one.rb b/activerecord/lib/active_record/associations/builder/has_one.rb index 7a6cd3890f..bc8a212bee 100644 --- a/activerecord/lib/active_record/associations/builder/has_one.rb +++ b/activerecord/lib/active_record/associations/builder/has_one.rb @@ -34,15 +34,12 @@ module ActiveRecord::Associations::Builder ":nullify or :restrict (#{options[:dependent].inspect})" end + dependent_restrict_deprecation_warning if options[:dependent] == :restrict send("define_#{options[:dependent]}_dependency_method") model.before_destroy dependency_method_name end end - def dependency_method_name - "has_one_dependent_#{options[:dependent]}_for_#{name}" - end - def define_destroy_dependency_method name = self.name mixin.redefine_method(dependency_method_name) do @@ -52,11 +49,8 @@ module ActiveRecord::Associations::Builder alias :define_delete_dependency_method :define_destroy_dependency_method alias :define_nullify_dependency_method :define_destroy_dependency_method - def define_restrict_dependency_method - name = self.name - mixin.redefine_method(dependency_method_name) do - raise ActiveRecord::DeleteRestrictionError.new(name) unless send(name).nil? - end + def dependency_method_name + "has_one_dependent_#{options[:dependent]}_for_#{name}" end end end diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb index af37909c89..14aa557b6c 100644 --- a/activerecord/lib/active_record/associations/collection_association.rb +++ b/activerecord/lib/active_record/associations/collection_association.rb @@ -1,5 +1,3 @@ -require 'active_support/core_ext/array/wrap' - module ActiveRecord module Associations # = Active Record Association Collection @@ -49,23 +47,31 @@ module ActiveRecord end else column = "#{reflection.quoted_table_name}.#{reflection.association_primary_key}" + relation = scoped - scoped.select(column).map! do |record| - record.send(reflection.association_primary_key) + including = (relation.eager_load_values + relation.includes_values).uniq + + if including.any? + join_dependency = ActiveRecord::Associations::JoinDependency.new(reflection.klass, including, []) + relation = join_dependency.join_associations.inject(relation) do |r, association| + association.join_relation(r) + end end + + relation.pluck(column) end end # Implements the ids writer method, e.g. foo.item_ids= for Foo.has_many :items def ids_writer(ids) pk_column = reflection.primary_key_column - ids = Array.wrap(ids).reject { |id| id.blank? } + ids = Array(ids).reject { |id| id.blank? } ids.map! { |i| pk_column.type_cast(i) } replace(klass.find(ids).index_by { |r| r.id }.values_at(*ids)) end def reset - @loaded = false + super @target = [] end @@ -152,6 +158,13 @@ module ActiveRecord end end + # Called when the association is declared as :dependent => :delete_all. This is + # an optimised version which avoids loading the records into memory. Not really + # for public consumption. + def delete_all_on_destroy + scoped.delete_all + end + # Destroy all the records from this association. # # See destroy for more info. @@ -385,12 +398,7 @@ module ActiveRecord return memory if persisted.empty? persisted.map! do |record| - # Unfortunately we cannot simply do memory.delete(record) since on 1.8 this returns - # record rather than memory.at(memory.index(record)). The behavior is fixed in 1.9. - mem_index = memory.index(record) - - if mem_index - mem_record = memory.delete_at(mem_index) + if mem_record = memory.delete(record) (record.attribute_names - mem_record.changes.keys).each do |name| mem_record[name] = record[name] @@ -536,7 +544,7 @@ module ActiveRecord # If using a custom finder_sql, #find scans the entire collection. def find_by_scan(*args) expects_array = args.first.kind_of?(Array) - ids = args.flatten.compact.uniq.map { |arg| arg.to_i } + ids = args.flatten.compact.map{ |arg| arg.to_i }.uniq if ids.size == 1 id = ids.first diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb index 80bc4990d2..ad029d1101 100644 --- a/activerecord/lib/active_record/associations/collection_proxy.rb +++ b/activerecord/lib/active_record/associations/collection_proxy.rb @@ -41,8 +41,7 @@ module ActiveRecord delegate :group, :order, :limit, :joins, :where, :preload, :eager_load, :includes, :from, :lock, :readonly, :having, :pluck, :to => :scoped - delegate :target, :load_target, :loaded?, :scoped, - :to => :@association + delegate :target, :load_target, :loaded?, :to => :@association delegate :select, :find, :first, :last, :build, :create, :create!, @@ -53,7 +52,7 @@ module ActiveRecord def initialize(association) @association = association - Array.wrap(association.options[:extend]).each { |ext| proxy_extend(ext) } + Array(association.options[:extend]).each { |ext| proxy_extend(ext) } end alias_method :new, :build @@ -62,6 +61,17 @@ module ActiveRecord @association end + def scoped(options = nil) + association = @association + scope = association.scoped + + scope.extending! do + define_method(:proxy_association) { association } + end + scope.merge!(options) if options + scope + end + def respond_to?(name, include_private = false) super || (load_target && target.respond_to?(name, include_private)) || @@ -76,9 +86,8 @@ module ActiveRecord proxy_association.send :add_to_target, r yield(r) if block_given? end - end - if target.respond_to?(method) || (!proxy_association.klass.respond_to?(method) && Class.respond_to?(method)) + elsif target.respond_to?(method) || (!proxy_association.klass.respond_to?(method) && Class.respond_to?(method)) if load_target if target.respond_to?(method) target.send(method, *args, &block) @@ -121,6 +130,19 @@ module ActiveRecord proxy_association.reload self end + + # Define array public methods because we know it should be invoked over + # the target, so we can have a performance improvement using those methods + # in association collections + Array.public_instance_methods.each do |m| + unless method_defined?(m) + class_eval <<-RUBY, __FILE__, __LINE__ + 1 + def #{m}(*args, &block) + target.public_send(:#{m}, *args, &block) if load_target + end + RUBY + end + end end end end diff --git a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb index 1f917f58f2..a4cea99372 100644 --- a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb +++ b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb @@ -32,6 +32,10 @@ module ActiveRecord record end + # ActiveRecord::Relation#delete_all needs to support joins before we can use a + # SQL-only implementation. + alias delete_all_on_destroy delete_all + private def count_records diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb index c5b90e873a..059e6c77bc 100644 --- a/activerecord/lib/active_record/associations/has_many_association.rb +++ b/activerecord/lib/active_record/associations/has_many_association.rb @@ -89,12 +89,8 @@ module ActiveRecord records.each { |r| r.destroy } update_counter(-records.length) unless inverse_updates_counter_cache? else - scope = scoped - - unless records == load_target - keys = records.map { |r| r[reflection.association_primary_key] } - scope = scoped.where(reflection.association_primary_key => keys) - end + keys = records.map { |r| r[reflection.association_primary_key] } + scope = scoped.where(reflection.association_primary_key => keys) if method == :delete_all update_counter(-scope.delete_all) @@ -103,7 +99,7 @@ module ActiveRecord end end end - + def foreign_key_present? owner.attribute_present?(reflection.association_primary_key) end diff --git a/activerecord/lib/active_record/associations/has_many_through_association.rb b/activerecord/lib/active_record/associations/has_many_through_association.rb index 7e6e3be382..53d49fef2e 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -54,6 +54,10 @@ module ActiveRecord record end + # ActiveRecord::Relation#delete_all needs to support joins before we can use a + # SQL-only implementation. + alias delete_all_on_destroy delete_all + private def through_association @@ -69,7 +73,9 @@ module ActiveRecord # association def build_through_record(record) @through_records[record.object_id] ||= begin - through_record = through_association.build(construct_join_attributes(record)) + ensure_mutable + + through_record = through_association.build through_record.send("#{source_reflection.name}=", record) through_record end diff --git a/activerecord/lib/active_record/associations/join_dependency.rb b/activerecord/lib/active_record/associations/join_dependency.rb index 6c878f0f00..cd366ac8b7 100644 --- a/activerecord/lib/active_record/associations/join_dependency.rb +++ b/activerecord/lib/active_record/associations/join_dependency.rb @@ -13,7 +13,7 @@ module ActiveRecord @join_parts = [JoinBase.new(base)] @associations = {} @reflections = [] - @alias_tracker = AliasTracker.new(joins) + @alias_tracker = AliasTracker.new(base.connection, joins) @alias_tracker.aliased_name_for(base.table_name) # Updates the count for base.table_name to 1 build(associations) end @@ -184,7 +184,7 @@ module ActiveRecord macro = join_part.reflection.macro if macro == :has_one - return if record.association_cache.key?(join_part.reflection.name) + return record.association(join_part.reflection.name).target if record.association_cache.key?(join_part.reflection.name) association = join_part.instantiate(row) unless row[join_part.aliased_primary_key].nil? set_target_and_inverse(join_part, association, record) else diff --git a/activerecord/lib/active_record/associations/join_dependency/join_association.rb b/activerecord/lib/active_record/associations/join_dependency/join_association.rb index 03963ab060..0d7d28e458 100644 --- a/activerecord/lib/active_record/associations/join_dependency/join_association.rb +++ b/activerecord/lib/active_record/associations/join_dependency/join_association.rb @@ -95,8 +95,11 @@ module ActiveRecord conditions = self.conditions[i].dup conditions << { reflection.type => foreign_klass.base_class.name } if reflection.type - unless conditions.empty? - constraint = constraint.and(sanitize(conditions, table)) + conditions.each do |condition| + condition = active_record.send(:sanitize_sql, interpolate(condition), table.table_alias || table.name) + condition = Arel.sql(condition) unless condition.is_a?(Arel::Node) + + constraint = constraint.and(condition) end relation.from(join(table, constraint)) diff --git a/activerecord/lib/active_record/associations/join_helper.rb b/activerecord/lib/active_record/associations/join_helper.rb index f83138195c..cea6ad6944 100644 --- a/activerecord/lib/active_record/associations/join_helper.rb +++ b/activerecord/lib/active_record/associations/join_helper.rb @@ -40,16 +40,6 @@ module ActiveRecord def join(table, constraint) table.create_join(table, table.create_on(constraint), join_type) end - - def sanitize(conditions, table) - conditions = conditions.map do |condition| - condition = active_record.send(:sanitize_sql, interpolate(condition), table.table_alias || table.name) - condition = Arel.sql(condition) unless condition.is_a?(Arel::Node) - condition - end - - conditions.length == 1 ? conditions.first : Arel::Nodes::And.new(conditions) - end end end end diff --git a/activerecord/lib/active_record/associations/preloader/association.rb b/activerecord/lib/active_record/associations/preloader/association.rb index 779f8164cc..b4c3908b10 100644 --- a/activerecord/lib/active_record/associations/preloader/association.rb +++ b/activerecord/lib/active_record/associations/preloader/association.rb @@ -77,7 +77,7 @@ module ActiveRecord # Some databases impose a limit on the number of ids in a list (in Oracle it's 1000) # Make several smaller queries if necessary or make one query if the adapter supports it sliced = owner_keys.each_slice(model.connection.in_clause_length || owner_keys.size) - records = sliced.map { |slice| records_for(slice) }.flatten + records = sliced.map { |slice| records_for(slice).to_a }.flatten end # Each record may have multiple owners, and vice-versa @@ -93,10 +93,11 @@ module ActiveRecord end def build_scope - scope = klass.scoped + scope = klass.unscoped + scope.default_scoped = true - scope = scope.where(process_conditions(options[:conditions])) - scope = scope.where(process_conditions(preload_options[:conditions])) + scope = scope.where(interpolate(options[:conditions])) + scope = scope.where(interpolate(preload_options[:conditions])) scope = scope.select(preload_options[:select] || options[:select] || table[Arel.star]) scope = scope.includes(preload_options[:include] || options[:include]) @@ -112,13 +113,11 @@ module ActiveRecord scope end - def process_conditions(conditions) + def interpolate(conditions) if conditions.respond_to?(:to_proc) - conditions = klass.send(:instance_eval, &conditions) - end - - if conditions - klass.send(:sanitize_sql, conditions) + klass.send(:instance_eval, &conditions) + else + conditions end end end diff --git a/activerecord/lib/active_record/associations/through_association.rb b/activerecord/lib/active_record/associations/through_association.rb index f95e5337c2..be890e5767 100644 --- a/activerecord/lib/active_record/associations/through_association.rb +++ b/activerecord/lib/active_record/associations/through_association.rb @@ -37,9 +37,7 @@ module ActiveRecord # situation it is more natural for the user to just create or modify their join records # directly as required. def construct_join_attributes(*records) - if source_reflection.macro != :belongs_to - raise HasManyThroughCantAssociateThroughHasOneOrManyReflection.new(owner, reflection) - end + ensure_mutable join_attributes = { source_reflection.foreign_key => @@ -64,7 +62,7 @@ module ActiveRecord # properly support stale-checking for nested associations. def stale_state if through_reflection.macro == :belongs_to - owner[through_reflection.foreign_key].to_s + owner[through_reflection.foreign_key] && owner[through_reflection.foreign_key].to_s end end @@ -73,6 +71,12 @@ module ActiveRecord !owner[through_reflection.foreign_key].nil? end + def ensure_mutable + if source_reflection.macro != :belongs_to + raise HasManyThroughCantAssociateThroughHasOneOrManyReflection.new(owner, reflection) + end + end + def ensure_not_nested if reflection.nested? raise HasManyThroughNestedAssociationsAreReadonly.new(owner, reflection) |