diff options
Diffstat (limited to 'activerecord/lib/active_record')
38 files changed, 784 insertions, 986 deletions
diff --git a/activerecord/lib/active_record/aggregations.rb b/activerecord/lib/active_record/aggregations.rb index 8cd7389005..90d3b58c78 100644 --- a/activerecord/lib/active_record/aggregations.rb +++ b/activerecord/lib/active_record/aggregations.rb @@ -4,9 +4,7 @@ module ActiveRecord extend ActiveSupport::Concern def clear_aggregation_cache #:nodoc: - self.class.reflect_on_all_aggregations.to_a.each do |assoc| - instance_variable_set "@#{assoc.name}", nil - end if self.persisted? + @aggregation_cache.clear if persisted? end # Active Record implements aggregation through a macro-like class method called +composed_of+ @@ -222,53 +220,32 @@ module ActiveRecord private def reader_method(name, class_name, mapping, allow_nil, constructor) - module_eval do - define_method(name) do |*args| - force_reload = args.first || false - - unless instance_variable_defined?("@#{name}") - instance_variable_set("@#{name}", nil) - end - - if (instance_variable_get("@#{name}").nil? || force_reload) && (!allow_nil || mapping.any? {|pair| !read_attribute(pair.first).nil? }) - attrs = mapping.collect {|pair| read_attribute(pair.first)} - object = case constructor - when Symbol - class_name.constantize.send(constructor, *attrs) - when Proc, Method - constructor.call(*attrs) - else - raise ArgumentError, 'Constructor must be a symbol denoting the constructor method to call or a Proc to be invoked.' - end - instance_variable_set("@#{name}", object) - end - instance_variable_get("@#{name}") + define_method(name) do + if @aggregation_cache[name].nil? && (!allow_nil || mapping.any? {|pair| !read_attribute(pair.first).nil? }) + attrs = mapping.collect {|pair| read_attribute(pair.first)} + object = constructor.respond_to?(:call) ? + constructor.call(*attrs) : + class_name.constantize.send(constructor, *attrs) + @aggregation_cache[name] = object end + @aggregation_cache[name] end - end def writer_method(name, class_name, mapping, allow_nil, converter) - module_eval do - define_method("#{name}=") do |part| - if part.nil? && allow_nil - mapping.each { |pair| self[pair.first] = nil } - instance_variable_set("@#{name}", nil) - else - unless part.is_a?(class_name.constantize) || converter.nil? - part = case converter - when Symbol - class_name.constantize.send(converter, part) - when Proc, Method - converter.call(part) - else - raise ArgumentError, 'Converter must be a symbol denoting the converter method to call or a Proc to be invoked.' - end - end - - mapping.each { |pair| self[pair.first] = part.send(pair.last) } - instance_variable_set("@#{name}", part.freeze) + define_method("#{name}=") do |part| + if part.nil? && allow_nil + mapping.each { |pair| self[pair.first] = nil } + @aggregation_cache[name] = nil + else + unless part.is_a?(class_name.constantize) || converter.nil? + part = converter.respond_to?(:call) ? + converter.call(part) : + class_name.constantize.send(converter, part) end + + mapping.each { |pair| self[pair.first] = part.send(pair.last) } + @aggregation_cache[name] = part.freeze end end end diff --git a/activerecord/lib/active_record/association_preload.rb b/activerecord/lib/active_record/association_preload.rb index ecf7b6c210..cba4bab3ef 100644 --- a/activerecord/lib/active_record/association_preload.rb +++ b/activerecord/lib/active_record/association_preload.rb @@ -126,21 +126,21 @@ module ActiveRecord parent_records.each do |parent_record| association_proxy = parent_record.send(reflection_name) association_proxy.loaded - association_proxy.target.push(*Array.wrap(associated_record)) + association_proxy.target.concat(Array.wrap(associated_record)) association_proxy.send(:set_inverse_instance, associated_record) end end def add_preloaded_record_to_collection(parent_records, reflection_name, associated_record) parent_records.each do |parent_record| - parent_record.send("set_#{reflection_name}_target", associated_record) + parent_record.send(:association_proxy, reflection_name).target = associated_record end end - def set_association_collection_records(id_to_record_map, reflection_name, associated_records, key) + def set_association_collection_records(id_to_parent_map, reflection_name, associated_records, key) associated_records.each do |associated_record| - mapped_records = id_to_record_map[associated_record[key].to_s] - add_preloaded_records_to_collection(mapped_records, reflection_name, associated_record) + parent_records = id_to_parent_map[associated_record[key].to_s] + add_preloaded_records_to_collection(parent_records, reflection_name, associated_record) end end @@ -158,14 +158,15 @@ module ActiveRecord seen_keys[seen_key] = true mapped_records = id_to_record_map[seen_key] mapped_records.each do |mapped_record| - association_proxy = mapped_record.send("set_#{reflection_name}_target", associated_record) + association_proxy = mapped_record.send(:association_proxy, reflection_name) + association_proxy.target = associated_record association_proxy.send(:set_inverse_instance, associated_record) end end id_to_record_map.each do |id, records| - next if seen_keys.include?(id.to_s) - records.each {|record| record.send("set_#{reflection_name}_target", nil) } + next if seen_keys.include?(id) + add_preloaded_record_to_collection(records, reflection_name, nil) end end @@ -174,29 +175,23 @@ module ActiveRecord # <tt>(id_to_record_map, ids)</tt> where +id_to_record_map+ is the Hash, # and +ids+ is an Array of record IDs. def construct_id_map(records, primary_key=nil) - id_to_record_map = {} - ids = [] - records.each do |record| + records.group_by do |record| primary_key ||= record.class.primary_key - ids << record[primary_key] - mapped_records = (id_to_record_map[ids.last.to_s] ||= []) - mapped_records << record + record[primary_key].to_s end - ids.uniq! - return id_to_record_map, ids end def preload_has_and_belongs_to_many_association(records, reflection, preload_options={}) left = reflection.klass.arel_table - id_to_record_map, ids = construct_id_map(records) + id_to_record_map = construct_id_map(records) + records.each {|record| record.send(reflection.name).loaded} options = reflection.options right = Arel::Table.new(options[:join_table]).alias('t0') - join_condition = left[reflection.klass.primary_key].eq( right[reflection.association_foreign_key]) @@ -205,7 +200,7 @@ module ActiveRecord # FIXME: options[:select] is always nil in the tests. Do we really # need it? options[:select] || left[Arel.star], - right[reflection.primary_key_name].as( + right[reflection.foreign_key].as( Arel.sql('the_parent_record_id')) ] @@ -218,34 +213,42 @@ module ActiveRecord custom_conditions = append_conditions(reflection, preload_options) - all_associated_records = associated_records(ids) do |some_ids| - method = in_or_equal(some_ids) - conditions = right[reflection.primary_key_name].send(*method) - conditions = custom_conditions.inject(conditions) do |ast, cond| - ast.and cond - end - - associated_records_proxy.where(conditions).to_a - end + klass = associated_records_proxy.klass - set_association_collection_records(id_to_record_map, reflection.name, all_associated_records, 'the_parent_record_id') + associated_records(id_to_record_map.keys) { |some_ids| + method = in_or_equal(some_ids) + conditions = right.create_and( + [right[reflection.foreign_key].send(*method)] + + custom_conditions) + + relation = associated_records_proxy.where(conditions) + klass.connection.select_all(relation.arel.to_sql, 'SQL', relation.bind_values) + }.map! { |row| + parent_records = id_to_record_map[row['the_parent_record_id'].to_s] + associated_record = klass.instantiate row + add_preloaded_records_to_collection( + parent_records, reflection.name, associated_record) + associated_record + } end def preload_has_one_association(records, reflection, preload_options={}) - return if records.first.send("loaded_#{reflection.name}?") - id_to_record_map, ids = construct_id_map(records, reflection.options[:primary_key]) + return if records.first.send(:association_proxy, reflection.name).loaded? + id_to_record_map = construct_id_map(records, reflection.options[:primary_key]) options = reflection.options - records.each {|record| record.send("set_#{reflection.name}_target", nil)} + + add_preloaded_record_to_collection(records, reflection.name, nil) + if options[:through] through_records = preload_through_records(records, reflection, options[:through]) unless through_records.empty? through_reflection = reflections[options[:through]] - through_primary_key = through_reflection.primary_key_name + through_primary_key = through_reflection.foreign_key source = reflection.source_reflection.name through_records.first.class.preload_associations(through_records, source) if through_reflection.macro == :belongs_to - id_to_record_map = construct_id_map(records, through_primary_key).first + id_to_record_map = construct_id_map(records, through_primary_key) through_primary_key = through_reflection.klass.primary_key end @@ -255,7 +258,7 @@ module ActiveRecord end end else - set_association_single_records(id_to_record_map, reflection.name, find_associated_records(ids, reflection, preload_options), reflection.primary_key_name) + set_association_single_records(id_to_record_map, reflection.name, find_associated_records(id_to_record_map.keys, reflection, preload_options), reflection.foreign_key) end end @@ -263,8 +266,8 @@ module ActiveRecord return if records.first.send(reflection.name).loaded? options = reflection.options - primary_key_name = reflection.through_reflection_primary_key_name - id_to_record_map, ids = construct_id_map(records, primary_key_name || reflection.options[:primary_key]) + foreign_key = reflection.through_reflection_foreign_key + id_to_record_map = construct_id_map(records, foreign_key || reflection.options[:primary_key]) records.each {|record| record.send(reflection.name).loaded} if options[:through] @@ -280,14 +283,14 @@ module ActiveRecord end else - set_association_collection_records(id_to_record_map, reflection.name, find_associated_records(ids, reflection, preload_options), - reflection.primary_key_name) + set_association_collection_records(id_to_record_map, reflection.name, find_associated_records(id_to_record_map.keys, reflection, preload_options), + reflection.foreign_key) end end def preload_through_records(records, reflection, through_association) if reflection.options[:source_type] - interface = reflection.source_reflection.options[:foreign_type] + interface = reflection.source_reflection.foreign_type preload_options = {:conditions => ["#{connection.quote_column_name interface} = ?", reflection.options[:source_type]]} records.compact! @@ -317,20 +320,17 @@ module ActiveRecord end def preload_belongs_to_association(records, reflection, preload_options={}) - return if records.first.send("loaded_#{reflection.name}?") + return if records.first.send(:association_proxy, reflection.name).loaded? options = reflection.options - primary_key_name = reflection.primary_key_name klasses_and_ids = {} if options[:polymorphic] - polymorph_type = options[:foreign_type] - # Construct a mapping from klass to a list of ids to load and a mapping of those ids back # to their parent_records records.each do |record| - if klass = record.send(polymorph_type) - klass_id = record.send(primary_key_name) + if klass = record.send(reflection.foreign_type) + klass_id = record.send(reflection.foreign_key) if klass_id id_map = klasses_and_ids[klass.constantize] ||= {} (id_map[klass_id.to_s] ||= []) << record @@ -339,25 +339,30 @@ module ActiveRecord end else id_map = records.group_by do |record| - key = record.send(primary_key_name) + key = record.send(reflection.foreign_key) key && key.to_s end - id_map.delete nil klasses_and_ids[reflection.klass] = id_map unless id_map.empty? end klasses_and_ids.each do |klass, _id_map| - table = klass.arel_table primary_key = (reflection.options[:primary_key] || klass.primary_key).to_s - method = in_or_equal(_id_map.keys) - conditions = table[primary_key].send(*method) + keys = _id_map.keys.compact - custom_conditions = append_conditions(reflection, preload_options) - conditions = custom_conditions.inject(conditions) do |ast, cond| - ast.and cond - end + unless keys.empty? + table = klass.arel_table + method = in_or_equal(keys) + conditions = table[primary_key].send(*method) - associated_records = klass.unscoped.where(conditions).apply_finder_options(options.slice(:include, :select, :joins, :order)).to_a + custom_conditions = append_conditions(reflection, preload_options) + conditions = custom_conditions.inject(conditions) do |ast, cond| + ast.and cond + end + + associated_records = klass.unscoped.where(conditions).apply_finder_options(options.slice(:include, :select, :joins, :order)).to_a + else + associated_records = [] + end set_association_single_records(_id_map, reflection.name, associated_records, primary_key) end @@ -369,14 +374,14 @@ module ActiveRecord conditions = [] - key = reflection.primary_key_name + key = reflection.foreign_key if interface = reflection.options[:as] key = "#{interface}_id" conditions << table["#{interface}_type"].eq(base_class.sti_name) end - conditions += append_conditions(reflection, preload_options) + conditions.concat append_conditions(reflection, preload_options) find_options = { :select => preload_options[:select] || options[:select] || table[Arel.star], @@ -388,9 +393,7 @@ module ActiveRecord associated_records(ids) do |some_ids| method = in_or_equal(some_ids) - where = conditions.inject(table[key].send(*method)) do |ast, cond| - ast.and cond - end + where = table.create_and(conditions + [table[key].send(*method)]) reflection.klass.scoped.apply_finder_options(find_options.merge(:conditions => where)).to_a end @@ -413,7 +416,7 @@ module ActiveRecord in_clause_length = connection.in_clause_length || ids.size records = [] ids.each_slice(in_clause_length) do |some_ids| - records += yield(some_ids) + records.concat yield(some_ids) end records end diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index b49cf8de95..891ac52f8a 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -119,8 +119,8 @@ module ActiveRecord # These classes will be loaded when associations are created. # So there is no need to eager load them. autoload :AssociationCollection, 'active_record/associations/association_collection' + autoload :SingularAssociation, 'active_record/associations/singular_association' autoload :AssociationProxy, 'active_record/associations/association_proxy' - autoload :HasAssociation, 'active_record/associations/has_association' autoload :ThroughAssociation, 'active_record/associations/through_association' autoload :BelongsToAssociation, 'active_record/associations/belongs_to_association' autoload :BelongsToPolymorphicAssociation, 'active_record/associations/belongs_to_polymorphic_association' @@ -132,24 +132,40 @@ module ActiveRecord # Clears out the association cache. def clear_association_cache #:nodoc: - self.class.reflect_on_all_associations.to_a.each do |assoc| - instance_variable_set "@#{assoc.name}", nil - end if self.persisted? + @association_cache.clear if persisted? end + # :nodoc: + attr_reader :association_cache + + protected + + # Returns the proxy for the given association name, instantiating it if it doesn't + # already exist + def association_proxy(name) + association = association_instance_get(name) + + if association.nil? + reflection = self.class.reflect_on_association(name) + association = reflection.proxy_class.new(self, reflection) + association_instance_set(name, association) + end + + association + end + private # Returns the specified association instance if it responds to :loaded?, nil otherwise. def association_instance_get(name) - ivar = "@#{name}" - if instance_variable_defined?(ivar) - association = instance_variable_get(ivar) + if @association_cache.key? name + association = @association_cache[name] association if association.respond_to?(:loaded?) end end # Set the specified association instance. def association_instance_set(name, association) - instance_variable_set("@#{name}", association) + @association_cache[name] = association end # Associations are a set of macro-like class methods for tying objects together through @@ -193,7 +209,7 @@ module ActiveRecord # other=(other) | X | X | X # build_other(attributes={}) | X | | X # create_other(attributes={}) | X | | X - # other.create!(attributes={}) | | | X + # create_other!(attributes={}) | X | | X # # ===Collection associations (one-to-many / many-to-many) # | | | has_many @@ -333,26 +349,31 @@ module ActiveRecord # === One-to-one associations # # * Assigning an object to a +has_one+ association automatically saves that object and - # the object being replaced (if there is one), in order to update their primary - # keys - except if the parent object is unsaved (<tt>new_record? == true</tt>). - # * If either of these saves fail (due to one of the objects being invalid) the assignment - # statement returns +false+ and the assignment is cancelled. + # the object being replaced (if there is one), in order to update their foreign + # keys - except if the parent object is unsaved (<tt>new_record? == true</tt>). + # * If either of these saves fail (due to one of the objects being invalid), an + # <tt>ActiveRecord::RecordNotSaved</tt> exception is raised and the assignment is + # cancelled. # * If you wish to assign an object to a +has_one+ association without saving it, - # use the <tt>association.build</tt> method (documented below). + # use the <tt>build_association</tt> method (documented below). The object being + # replaced will still be saved to update its foreign key. # * Assigning an object to a +belongs_to+ association does not save the object, since - # the foreign key field belongs on the parent. It does not save the parent either. + # the foreign key field belongs on the parent. It does not save the parent either. # # === Collections # # * Adding an object to a collection (+has_many+ or +has_and_belongs_to_many+) automatically - # saves that object, except if the parent object (the owner of the collection) is not yet - # stored in the database. + # saves that object, except if the parent object (the owner of the collection) is not yet + # stored in the database. # * If saving any of the objects being added to a collection (via <tt>push</tt> or similar) - # fails, then <tt>push</tt> returns +false+. + # fails, then <tt>push</tt> returns +false+. + # * If saving fails while replacing the collection (via <tt>association=</tt>), an + # <tt>ActiveRecord::RecordNotSaved</tt> exception is raised and the assignment is + # cancelled. # * You can add an object to a collection without automatically saving it by using the - # <tt>collection.build</tt> method (documented below). + # <tt>collection.build</tt> method (documented below). # * All unsaved (<tt>new_record? == true</tt>) members of the collection are automatically - # saved when the parent is saved. + # saved when the parent is saved. # # === Association callbacks # @@ -999,12 +1020,7 @@ module ActiveRecord reflection = create_has_many_reflection(association_id, options, &extension) configure_dependency_for_has_many(reflection) add_association_callbacks(reflection.name, reflection.options) - - if options[:through] - collection_accessor_methods(reflection, HasManyThroughAssociation) - else - collection_accessor_methods(reflection, HasManyAssociation) - end + collection_accessor_methods(reflection) end # Specifies a one-to-one association with another class. This method should only be used @@ -1022,12 +1038,14 @@ module ActiveRecord # [build_association(attributes = {})] # Returns a new object of the associated type that has been instantiated # with +attributes+ and linked to this object through a foreign key, but has not - # yet been saved. <b>Note:</b> This ONLY works if an association already exists. - # It will NOT work if the association is +nil+. + # yet been saved. # [create_association(attributes = {})] # Returns a new object of the associated type that has been instantiated # with +attributes+, linked to this object through a foreign key, and that # has already been saved (if it passed the validation). + # [create_association!(attributes = {})] + # Does the same as <tt>create_association</tt>, but raises <tt>ActiveRecord::RecordInvalid</tt> + # if the record is invalid. # # (+association+ is replaced with the symbol passed as the first argument, so # <tt>has_one :manager</tt> would add among others <tt>manager.nil?</tt>.) @@ -1039,6 +1057,7 @@ module ActiveRecord # * <tt>Account#beneficiary=(beneficiary)</tt> (similar to <tt>beneficiary.account_id = account.id; beneficiary.save</tt>) # * <tt>Account#build_beneficiary</tt> (similar to <tt>Beneficiary.new("account_id" => id)</tt>) # * <tt>Account#create_beneficiary</tt> (similar to <tt>b = Beneficiary.new("account_id" => id); b.save; b</tt>) + # * <tt>Account#create_beneficiary!</tt> (similar to <tt>b = Beneficiary.new("account_id" => id); b.save!; b</tt>) # # === Options # @@ -1116,14 +1135,12 @@ module ActiveRecord def has_one(association_id, options = {}) if options[:through] reflection = create_has_one_through_reflection(association_id, options) - association_accessor_methods(reflection, ActiveRecord::Associations::HasOneThroughAssociation) else reflection = create_has_one_reflection(association_id, options) - association_accessor_methods(reflection, HasOneAssociation) - association_constructor_method(:build, reflection, HasOneAssociation) - association_constructor_method(:create, reflection, HasOneAssociation) + association_constructor_methods(reflection) configure_dependency_for_has_one(reflection) end + association_accessor_methods(reflection) end # Specifies a one-to-one association with another class. This method should only be used @@ -1145,6 +1162,9 @@ module ActiveRecord # Returns a new object of the associated type that has been instantiated # with +attributes+, linked to this object through a foreign key, and that # has already been saved (if it passed the validation). + # [create_association!(attributes = {})] + # Does the same as <tt>create_association</tt>, but raises <tt>ActiveRecord::RecordInvalid</tt> + # if the record is invalid. # # (+association+ is replaced with the symbol passed as the first argument, so # <tt>belongs_to :author</tt> would add among others <tt>author.nil?</tt>.) @@ -1156,6 +1176,7 @@ module ActiveRecord # * <tt>Post#author=(author)</tt> (similar to <tt>post.author_id = author.id</tt>) # * <tt>Post#build_author</tt> (similar to <tt>post.author = Author.new</tt>) # * <tt>Post#create_author</tt> (similar to <tt>post.author = Author.new; post.author.save; post.author</tt>) + # * <tt>Post#create_author!</tt> (similar to <tt>post.author = Author.new; post.author.save!; post.author</tt>) # The declaration can also include an options hash to specialize the behavior of the association. # # === Options @@ -1177,6 +1198,11 @@ module ActiveRecord # association will use "person_id" as the default <tt>:foreign_key</tt>. Similarly, # <tt>belongs_to :favorite_person, :class_name => "Person"</tt> will use a foreign key # of "favorite_person_id". + # [:foreign_type] + # Specify the column used to store the associated object's type, if this is a polymorphic + # association. By default this is guessed to be the name of the association with a "_type" + # suffix. So a class that defines a <tt>belongs_to :taggable, :polymorphic => true</tt> + # association will use "taggable_type" as the default <tt>:foreign_type</tt>. # [:primary_key] # Specify the method that returns the primary key of associated object used for the association. # By default this is id. @@ -1235,12 +1261,10 @@ module ActiveRecord def belongs_to(association_id, options = {}) reflection = create_belongs_to_reflection(association_id, options) - if reflection.options[:polymorphic] - association_accessor_methods(reflection, BelongsToPolymorphicAssociation) - else - association_accessor_methods(reflection, BelongsToAssociation) - association_constructor_method(:build, reflection, BelongsToAssociation) - association_constructor_method(:create, reflection, BelongsToAssociation) + association_accessor_methods(reflection) + + unless reflection.options[:polymorphic] + association_constructor_methods(reflection) end add_counter_cache_callbacks(reflection) if options[:counter_cache] @@ -1277,12 +1301,6 @@ module ActiveRecord # end # end # - # Deprecated: Any additional fields added to the join table will be placed as attributes when - # pulling records out through +has_and_belongs_to_many+ associations. Records returned from join - # tables with additional attributes will be marked as readonly (because we can't save changes - # to the additional attributes). It's strongly recommended that you upgrade any - # associations with attributes to a real join model (see introduction). - # # Adds the following methods for retrieval and query: # # [collection(force_reload = false)] @@ -1425,7 +1443,7 @@ module ActiveRecord # 'DELETE FROM developers_projects WHERE active=1 AND developer_id = #{id} AND project_id = #{record.id}' def has_and_belongs_to_many(association_id, options = {}, &extension) reflection = create_has_and_belongs_to_many_reflection(association_id, options, &extension) - collection_accessor_methods(reflection, HasAndBelongsToManyAssociation) + collection_accessor_methods(reflection) # Don't use a before_destroy callback since users' before_destroy # callbacks will be executed after the association is wiped out. @@ -1457,57 +1475,29 @@ module ActiveRecord table_name_prefix + join_table + table_name_suffix end - def association_accessor_methods(reflection, association_proxy_class) + def association_accessor_methods(reflection) redefine_method(reflection.name) do |*params| force_reload = params.first unless params.empty? - association = association_instance_get(reflection.name) - - if association.nil? || force_reload || association.stale_target? - association = association_proxy_class.new(self, reflection) - retval = force_reload ? reflection.klass.uncached { association.reload } : association.reload - if retval.nil? and association_proxy_class == BelongsToAssociation - association_instance_set(reflection.name, nil) - return nil - end - association_instance_set(reflection.name, association) - end - - association.target.nil? ? nil : association - end + association = association_proxy(reflection.name) - redefine_method("loaded_#{reflection.name}?") do - association = association_instance_get(reflection.name) - association && association.loaded? - end - - redefine_method("#{reflection.name}=") do |new_value| - association = association_instance_get(reflection.name) - - if association.nil? || association.target != new_value - association = association_proxy_class.new(self, reflection) + if force_reload + reflection.klass.uncached { association.reload } + elsif !association.loaded? || association.stale_target? + association.reload end - association.replace(new_value) - association_instance_set(reflection.name, new_value.nil? ? nil : association) + association.target.nil? ? nil : association end - redefine_method("set_#{reflection.name}_target") do |target| - return if target.nil? and association_proxy_class == BelongsToAssociation - association = association_proxy_class.new(self, reflection) - association.target = target - association_instance_set(reflection.name, association) + redefine_method("#{reflection.name}=") do |record| + association_proxy(reflection.name).replace(record) end end - def collection_reader_method(reflection, association_proxy_class) + def collection_reader_method(reflection) redefine_method(reflection.name) do |*params| force_reload = params.first unless params.empty? - association = association_instance_get(reflection.name) - - unless association - association = association_proxy_class.new(self, reflection) - association_instance_set(reflection.name, association) - end + association = association_proxy(reflection.name) if force_reload reflection.klass.uncached { association.reload } @@ -1527,15 +1517,12 @@ module ActiveRecord end end - def collection_accessor_methods(reflection, association_proxy_class, writer = true) - collection_reader_method(reflection, association_proxy_class) + def collection_accessor_methods(reflection, writer = true) + collection_reader_method(reflection) if writer redefine_method("#{reflection.name}=") do |new_value| - # Loads proxy class instance (defined in collection_reader_method) if not already loaded - association = send(reflection.name) - association.replace(new_value) - association + association_proxy(reflection.name).replace(new_value) end redefine_method("#{reflection.name.to_s.singularize}_ids=") do |new_value| @@ -1547,21 +1534,17 @@ module ActiveRecord end end - def association_constructor_method(constructor, reflection, association_proxy_class) - redefine_method("#{constructor}_#{reflection.name}") do |*params| - attributees = params.first unless params.empty? - replace_existing = params[1].nil? ? true : params[1] - association = association_instance_get(reflection.name) - - unless association - association = association_proxy_class.new(self, reflection) - association_instance_set(reflection.name, association) - end - - if association_proxy_class == HasOneAssociation - association.send(constructor, attributees, replace_existing) - else - association.send(constructor, attributees) + def association_constructor_methods(reflection) + constructors = { + "build_#{reflection.name}" => "build", + "create_#{reflection.name}" => "create", + "create_#{reflection.name}!" => "create!" + } + + constructors.each do |name, proxy_name| + redefine_method(name) do |*params| + attributes = params.first unless params.empty? + association_proxy(reflection.name).send(proxy_name, attributes) end end end @@ -1612,16 +1595,14 @@ module ActiveRecord # - set the foreign key to NULL if the option is set to :nullify # - do not delete the parent record if there is any child record if the # option is set to :restrict - # - # The +extra_conditions+ parameter, which is not used within the main - # Active Record codebase, is meant to allow plugins to define extra - # finder conditions. - def configure_dependency_for_has_many(reflection, extra_conditions = nil) - if reflection.options.include?(:dependent) + def configure_dependency_for_has_many(reflection) + if reflection.options[:dependent] + method_name = "has_many_dependent_for_#{reflection.name}" + case reflection.options[:dependent] - when :destroy - method_name = "has_many_dependent_destroy_for_#{reflection.name}".to_sym - define_method(method_name) do + when :destroy, :delete_all, :nullify + define_method(method_name) do + if reflection.options[:dependent] == :destroy send(reflection.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 @@ -1633,35 +1614,21 @@ module ActiveRecord o.destroy end end - before_destroy method_name - when :delete_all - before_destroy do |record| - self.class.send(:delete_all_has_many_dependencies, - record, - reflection.name, - reflection.klass, - reflection.dependent_conditions(record, self.class, extra_conditions)) - end - when :nullify - before_destroy do |record| - self.class.send(:nullify_has_many_dependencies, - record, - reflection.name, - reflection.klass, - reflection.primary_key_name, - reflection.dependent_conditions(record, self.class, extra_conditions)) - end - when :restrict - method_name = "has_many_dependent_restrict_for_#{reflection.name}".to_sym - define_method(method_name) do - unless send(reflection.name).empty? - raise DeleteRestrictionError.new(reflection) - end + + # AssociationProxy#delete_all looks at the :dependent option and acts accordingly + send(reflection.name).delete_all + end + when :restrict + define_method(method_name) do + unless send(reflection.name).empty? + raise DeleteRestrictionError.new(reflection) end - before_destroy method_name - else - raise ArgumentError, "The :dependent option expects either :destroy, :delete_all, :nullify or :restrict (#{reflection.options[:dependent].inspect})" + end + else + raise ArgumentError, "The :dependent option expects either :destroy, :delete_all, :nullify or :restrict (#{reflection.options[:dependent].inspect})" end + + before_destroy method_name end end @@ -1686,7 +1653,7 @@ module ActiveRecord class_eval <<-eoruby, __FILE__, __LINE__ + 1 def #{method_name} association = #{reflection.name} - association.update_attribute(#{reflection.primary_key_name.inspect}, nil) if association + association.update_attribute(#{reflection.foreign_key.inspect}, nil) if association end eoruby when :restrict @@ -1724,14 +1691,6 @@ module ActiveRecord end end - def delete_all_has_many_dependencies(record, reflection_name, association_class, dependent_conditions) - association_class.delete_all(dependent_conditions) - end - - def nullify_has_many_dependencies(record, reflection_name, association_class, primary_key_name, dependent_conditions) - association_class.update_all("#{primary_key_name} = NULL", dependent_conditions) - end - mattr_accessor :valid_keys_for_has_many_association @@valid_keys_for_has_many_association = [ :class_name, :table_name, :foreign_key, :primary_key, @@ -1780,13 +1739,7 @@ module ActiveRecord def create_belongs_to_reflection(association_id, options) options.assert_valid_keys(valid_keys_for_belongs_to_association) - reflection = create_reflection(:belongs_to, association_id, options, self) - - if options[:polymorphic] - reflection.options[:foreign_type] ||= reflection.class_name.underscore + "_type" - end - - reflection + create_reflection(:belongs_to, association_id, options, self) end mattr_accessor :valid_keys_for_has_and_belongs_to_many_association @@ -1806,7 +1759,7 @@ module ActiveRecord reflection = create_reflection(:has_and_belongs_to_many, association_id, options, self) - if reflection.association_foreign_key == reflection.primary_key_name + if reflection.association_foreign_key == reflection.foreign_key raise HasAndBelongsToManyAssociationForeignKeyNeeded.new(reflection) end diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb index 108e316672..b75e02c66b 100644 --- a/activerecord/lib/active_record/associations/association_collection.rb +++ b/activerecord/lib/active_record/associations/association_collection.rb @@ -18,8 +18,6 @@ module ActiveRecord # If you need to work on all current children, new and existing records, # +load_target+ and the +loaded+ flag are your friends. class AssociationCollection < AssociationProxy #:nodoc: - include HasAssociation - delegate :group, :order, :limit, :joins, :where, :preload, :eager_load, :includes, :from, :lock, :readonly, :having, :to => :scoped def select(select = nil) @@ -31,42 +29,11 @@ module ActiveRecord end end - def scoped - with_scope(@scope) { @reflection.klass.scoped } - end - def find(*args) - options = args.extract_options! - - # If using a custom finder_sql, scan the entire collection. if @reflection.options[:finder_sql] - expects_array = args.first.kind_of?(Array) - ids = args.flatten.compact.uniq.map { |arg| arg.to_i } - - if ids.size == 1 - id = ids.first - record = load_target.detect { |r| id == r.id } - expects_array ? [ record ] : record - else - load_target.select { |r| ids.include?(r.id) } - end + find_by_scan(*args) else - merge_options_from_reflection!(options) - construct_find_options!(options) - - with_scope(:find => @scope[:find].slice(:conditions, :order)) do - relation = @reflection.klass.send(:construct_finder_arel, options, @reflection.klass.send(:current_scoped_methods)) - - case args.first - when :first, :last - relation.send(args.first) - when :all - records = relation.all - @reflection.options[:uniq] ? uniq(records) : records - else - relation.find(*args) - end - end + find_by_sql(*args) end end @@ -186,14 +153,13 @@ module ActiveRecord @reflection.klass.count_by_sql(custom_counter_sql) else - if @reflection.options[:uniq] # This is needed because 'SELECT count(DISTINCT *)..' is not valid SQL. column_name = "#{@reflection.quoted_table_name}.#{@reflection.klass.primary_key}" unless column_name options.merge!(:distinct => true) end - value = @reflection.klass.send(:with_scope, @scope) { @reflection.klass.count(column_name, options) } + value = scoped.count(column_name, options) limit = @reflection.options[:limit] offset = @reflection.options[:offset] @@ -348,7 +314,11 @@ module ActiveRecord transaction do delete(@target - other_array) - concat(other_array - @target) + + unless concat(other_array - @target) + raise RecordNotSaved, "Failed to replace #{@reflection.name} because one or more of the " + "new records could not be saved." + end end end @@ -359,46 +329,43 @@ module ActiveRecord loaded? ? @target.include?(record) : exists?(record) end - def proxy_respond_to?(method, include_private = false) + def respond_to?(method, include_private = false) super || @reflection.klass.respond_to?(method, include_private) end protected - def construct_find_options!(options) + + def association_scope + options = @reflection.options.slice(:order, :limit, :joins, :group, :having, :offset) + super.apply_finder_options(options) + end + + def select_value + super || uniq_select_value + end + + def uniq_select_value + @reflection.options[:uniq] && "DISTINCT #{@reflection.quoted_table_name}.*" end def load_target - if !@owner.new_record? || foreign_key_present + if (!@owner.new_record? || foreign_key_present?) && !loaded? + targets = [] + begin - unless loaded? - if @target.is_a?(Array) && @target.any? - @target = find_target.map do |f| - i = @target.index(f) - if i - @target.delete_at(i).tap do |t| - keys = ["id"] + t.changes.keys + (f.attribute_names - t.attribute_names) - f.attributes.except(*keys).each do |k,v| - t.send("#{k}=", v) - end - end - else - f - end - end + @target - else - @target = find_target - end - end + targets = find_target rescue ActiveRecord::RecordNotFound reset end + + @target = merge_target_lists(targets, @target) end - loaded if target + loaded target end - def method_missing(method, *args) + def method_missing(method, *args, &block) match = DynamicFinderMatch.match(method) if match && match.creator? attributes = match.attribute_names @@ -410,15 +377,9 @@ module ActiveRecord elsif @reflection.klass.scopes[method] @_scopes_cache ||= {} @_scopes_cache[method] ||= {} - @_scopes_cache[method][args] ||= with_scope(@scope) { @reflection.klass.send(method, *args) } + @_scopes_cache[method][args] ||= scoped.readonly(nil).send(method, *args) else - with_scope(@scope) do - if block_given? - @reflection.klass.send(method, *args) { |*block_args| yield(*block_args) } - else - @reflection.klass.send(method, *args) - end - end + scoped.readonly(nil).send(method, *args, &block) end end @@ -438,7 +399,7 @@ module ActiveRecord end def reset_target! - @target = Array.new + @target = [] end def reset_scopes_cache! @@ -473,6 +434,27 @@ module ActiveRecord end private + def merge_target_lists(loaded, existing) + return loaded if existing.empty? + return existing if loaded.empty? + + loaded.map do |f| + i = existing.index(f) + if i + existing.delete_at(i).tap do |t| + keys = ["id"] + t.changes.keys + (f.attribute_names - t.attribute_names) + # FIXME: this call to attributes causes many NoMethodErrors + attributes = f.attributes + (attributes.keys - keys).each do |k| + t.send("#{k}=", attributes[k]) + end + end + else + f + end + end + existing + end + # Do the relevant stuff to insert the given record into the association collection. The # force param specifies whether or not an exception should be raised on failure. The # validate param specifies whether validation should be performed (if force is false). @@ -488,9 +470,7 @@ module ActiveRecord ensure_owner_is_persisted! transaction do - with_scope(:create => @scope[:create].merge(scoped.where_values_hash)) do - build_record(attrs, &block) - end + scoped.scoping { build_record(attrs, &block) } end end @@ -543,14 +523,32 @@ module ActiveRecord def include_in_memory?(record) if @reflection.is_a?(ActiveRecord::Reflection::ThroughReflection) - @owner.send(proxy_reflection.through_reflection.name.to_sym).any? do |source| + @owner.send(proxy_reflection.through_reflection.name).any? { |source| target = source.send(proxy_reflection.source_reflection.name) target.respond_to?(:include?) ? target.include?(record) : target == record - end + } || @target.include?(record) else @target.include?(record) end end + + # 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 } + + if ids.size == 1 + id = ids.first + record = load_target.detect { |r| id == r.id } + expects_array ? [ record ] : record + else + load_target.select { |r| ids.include?(r.id) } + end + end + + def find_by_sql(*args) + scoped.find(*args) + end end end end diff --git a/activerecord/lib/active_record/associations/association_proxy.rb b/activerecord/lib/active_record/associations/association_proxy.rb index 6720d83199..addc64cb42 100644 --- a/activerecord/lib/active_record/associations/association_proxy.rb +++ b/activerecord/lib/active_record/associations/association_proxy.rb @@ -7,14 +7,15 @@ module ActiveRecord # This is the root class of all association proxies ('+ Foo' signifies an included module Foo): # # AssociationProxy - # BelongsToAssociation - # BelongsToPolymorphicAssociation - # AssociationCollection + HasAssociation + # SingularAssociaton + # HasOneAssociation + # HasOneThroughAssociation + ThroughAssociation + # BelongsToAssociation + # BelongsToPolymorphicAssociation + # AssociationCollection # HasAndBelongsToManyAssociation # HasManyAssociation # HasManyThroughAssociation + ThroughAssociation - # HasOneAssociation + HasAssociation - # HasOneThroughAssociation + ThroughAssociation # # Association proxies in Active Record are middlemen between the object that # holds the association, known as the <tt>@owner</tt>, and the actual associated @@ -50,10 +51,9 @@ module ActiveRecord # is computed directly through SQL and does not trigger by itself the # instantiation of the actual post records. class AssociationProxy #:nodoc: - alias_method :proxy_respond_to?, :respond_to? alias_method :proxy_extend, :extend - delegate :to_param, :to => :proxy_target - instance_methods.each { |m| undef_method m unless m.to_s =~ /^(?:nil\?|send|object_id|to_a)$|^__|^respond_to_missing|proxy_/ } + + instance_methods.each { |m| undef_method m unless m.to_s =~ /^(?:nil\?|send|object_id|to_a)$|^__|^respond_to|proxy_/ } def initialize(owner, reflection) @owner, @reflection = owner, reflection @@ -64,6 +64,10 @@ module ActiveRecord construct_scope end + def to_param + proxy_target.to_param + end + # Returns the owner of the proxy. def proxy_owner @owner @@ -75,21 +79,15 @@ module ActiveRecord @reflection end - # Returns the \target of the proxy, same as +target+. - def proxy_target - @target - end - # Does the proxy or its \target respond to +symbol+? def respond_to?(*args) - proxy_respond_to?(*args) || (load_target && @target.respond_to?(*args)) + super || (load_target && @target.respond_to?(*args)) end # Forwards <tt>===</tt> explicitly to the \target because the instance method # removal above doesn't catch it. Loads the \target if needed. def ===(other) - load_target - other === @target + other === load_target end # Returns the name of the table of the related class: @@ -116,6 +114,7 @@ module ActiveRecord # Reloads the \target and returns +self+ on success. def reload reset + construct_scope load_target self unless @target.nil? end @@ -127,23 +126,25 @@ module ActiveRecord # Asserts the \target has been loaded setting the \loaded flag to +true+. def loaded - @loaded = true + @loaded = true + @stale_state = stale_state end # The target is stale if the target no longer points to the record(s) that the # relevant foreign_key(s) refers to. If stale, the association accessor method - # on the owner will reload the target. It's up to subclasses to implement this - # method if relevant. + # on the owner will reload the target. It's up to subclasses to implement the + # state_state method if relevant. # # Note that if the target has not been loaded, it is not considered stale. def stale_target? - false + loaded? && @stale_state != stale_state end # Returns the target of this proxy, same as +proxy_target+. - def target - @target - end + attr_reader :target + + # Returns the \target of the proxy, same as +target+. + alias :proxy_target :target # Sets the target of this proxy to <tt>\target</tt>, and the \loaded flag to +true+. def target=(target) @@ -153,17 +154,16 @@ module ActiveRecord # Forwards the call to the target. Loads the \target if needed. def inspect - load_target - @target.inspect + load_target.inspect end def send(method, *args) - if proxy_respond_to?(method) - super - else - load_target - @target.send(method, *args) - end + return super if respond_to?(method) + load_target.send(method, *args) + end + + def scoped + target_scope & @association_scope end protected @@ -176,69 +176,85 @@ module ActiveRecord @reflection.klass.send(:sanitize_sql, sql, table_name) end - # Merges into +options+ the ones coming from the reflection. - def merge_options_from_reflection!(options) - options.reverse_merge!( - :group => @reflection.options[:group], - :having => @reflection.options[:having], - :limit => @reflection.options[:limit], - :offset => @reflection.options[:offset], - :joins => @reflection.options[:joins], - :include => @reflection.options[:include], - :select => @reflection.options[:select], - :readonly => @reflection.options[:readonly] - ) - end - - # Forwards +with_scope+ to the reflection. - def with_scope(*args, &block) - @reflection.klass.send :with_scope, *args, &block + # Construct the scope for this association. + # + # Note that the association_scope is merged into the targed_scope only when the + # scoped method is called. This is because at that point the call may be surrounded + # by scope.scoping { ... } or with_scope { ... } etc, which affects the scope which + # actually gets built. + def construct_scope + @association_scope = association_scope if target_klass end - # Construct the scope used for find/create queries on the target - def construct_scope - @scope = { - :find => construct_find_scope, - :create => construct_create_scope - } + def association_scope + scope = target_klass.unscoped + scope = scope.create_with(creation_attributes) + scope = scope.apply_finder_options(@reflection.options.slice(:conditions, :readonly, :include)) + scope = scope.select(select_value) if select_value = self.select_value + scope.where(construct_owner_conditions) end - # Implemented by subclasses - def construct_find_scope - raise NotImplementedError + def select_value + @reflection.options[:select] end # Implemented by (some) subclasses - def construct_create_scope - {} + def creation_attributes + { } end def aliased_table - @reflection.klass.arel_table + target_klass.arel_table end # Set the inverse association, if possible def set_inverse_instance(record) if record && invertible_for?(record) - record.send("set_#{inverse_reflection_for(record).name}_target", @owner) + inverse = record.send(:association_proxy, inverse_reflection_for(record).name) + inverse.target = @owner end end - private - # Forwards any missing method call to the \target. - def method_missing(method, *args) - if load_target - unless @target.respond_to?(method) - message = "undefined method `#{method.to_s}' for \"#{@target}\":#{@target.class.to_s}" - raise NoMethodError, message - end + # This class of the target. belongs_to polymorphic overrides this to look at the + # polymorphic_type field on the owner. + def target_klass + @reflection.klass + end + + # Can be overridden (i.e. in ThroughAssociation) to merge in other scopes (i.e. the + # through association's scope) + def target_scope + target_klass.scoped + end - if block_given? - @target.send(method, *args) { |*block_args| yield(*block_args) } - else - @target.send(method, *args) + # Returns a hash linking the owner to the association represented by the reflection + def construct_owner_attributes(reflection = @reflection) + attributes = {} + if reflection.macro == :belongs_to + attributes[reflection.association_primary_key] = @owner[reflection.foreign_key] + else + attributes[reflection.foreign_key] = @owner[reflection.active_record_primary_key] + + if reflection.options[:as] + attributes["#{reflection.options[:as]}_type"] = @owner.class.base_class.name end end + attributes + end + + # Builds an array of arel nodes from the owner attributes hash + def construct_owner_conditions(table = aliased_table, reflection = @reflection) + conditions = construct_owner_attributes(reflection).map do |attr, value| + table[attr].eq(value) + end + table.create_and(conditions) + end + + # Sets the owner attributes on the given record + def set_owner_attributes(record) + if @owner.persisted? + construct_owner_attributes.each { |key, value| record[key] = value } + end end # Loads the \target if needed and returns it. @@ -252,23 +268,36 @@ 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 - return nil unless defined?(@loaded) - - if !loaded? && (!@owner.new_record? || foreign_key_present) + if !loaded? && (!@owner.new_record? || foreign_key_present?) && target_klass @target = find_target end - @loaded = true + loaded @target rescue ActiveRecord::RecordNotFound reset end - # Can be overwritten by associations that might have the foreign key - # available for an association without having the object itself (and - # still being a new record). Currently, only +belongs_to+ presents - # this scenario (both vanilla and polymorphic). - def foreign_key_present + private + + # Forwards any missing method call to the \target. + def method_missing(method, *args, &block) + if load_target + return super unless @target.respond_to?(method) + @target.send(method, *args, &block) + end + rescue NoMethodError => e + raise e, e.message.sub(/ for #<.*$/, " via proxy for #{@target}") + end + + # Should be true if there is a foreign key present on the @owner which + # references the target. This is used to determine whether we can load + # the target if the @owner is currently a new record (and therefore + # without a key). + # + # Currently implemented by belongs_to (vanilla and polymorphic) and + # has_one/has_many :through associations which go through a belongs_to + def foreign_key_present? false end @@ -282,11 +311,6 @@ module ActiveRecord end end - # Returns the ID of the owner, quoted if needed. - def owner_quoted_id - @owner.quoted_id - end - # Can be redefined by subclasses, notably polymorphic belongs_to # The record parameter is necessary to support polymorphic inverses as we must check for # the association in the specific class of the record. @@ -298,6 +322,14 @@ module ActiveRecord def invertible_for?(record) inverse_reflection_for(record) end + + # This should be implemented to return the values of the relevant key(s) on the owner, + # so that when state_state is different from the value stored on the last find_target, + # the target is stale. + # + # This is only relevant to certain associations, which is why it returns nil by default. + def stale_state + end 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 98c1c13524..e80b945dda 100644 --- a/activerecord/lib/active_record/associations/belongs_to_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_association.rb @@ -1,84 +1,50 @@ module ActiveRecord # = Active Record Belongs To Associations module Associations - class BelongsToAssociation < AssociationProxy #:nodoc: - def create(attributes = {}) - replace(@reflection.create_association(attributes)) - end - - def build(attributes = {}) - replace(@reflection.build_association(attributes)) - end - + class BelongsToAssociation < SingularAssociation #:nodoc: def replace(record) - counter_cache_name = @reflection.counter_cache_column - - if record.nil? - if counter_cache_name && @owner.persisted? - @reflection.klass.decrement_counter(counter_cache_name, previous_record_id) if @owner[@reflection.primary_key_name] - end - - @target = @owner[@reflection.primary_key_name] = nil - else - raise_on_type_mismatch(record) - - if counter_cache_name && @owner.persisted? && record.id != @owner[@reflection.primary_key_name] - @reflection.klass.increment_counter(counter_cache_name, record.id) - @reflection.klass.decrement_counter(counter_cache_name, @owner[@reflection.primary_key_name]) if @owner[@reflection.primary_key_name] - end - - @target = (AssociationProxy === record ? record.target : record) - @owner[@reflection.primary_key_name] = record_id(record) if record.persisted? - @updated = true - end + record = check_record(record) + update_counters(record) + replace_keys(record) set_inverse_instance(record) - loaded - record + @updated = true if record + + self.target = record end def updated? @updated end - def stale_target? - if @target && @target.persisted? - target_id = @target.send(@reflection.association_primary_key).to_s - foreign_key = @owner.send(@reflection.primary_key_name).to_s - - target_id != foreign_key - else - false - end - end - private - def find_target - find_method = if @reflection.options[:primary_key] - "find_by_#{@reflection.options[:primary_key]}" - else - "find" - end + def update_counters(record) + counter_cache_name = @reflection.counter_cache_column - options = @reflection.options.dup.slice(:select, :include, :readonly) + if counter_cache_name && @owner.persisted? && different_target?(record) + if record + record.class.increment_counter(counter_cache_name, record.id) + end - the_target = with_scope(:find => @scope[:find]) do - @reflection.klass.send(find_method, - @owner[@reflection.primary_key_name], - options - ) if @owner[@reflection.primary_key_name] + if foreign_key_present? + target_klass.decrement_counter(counter_cache_name, target_id) + end end - set_inverse_instance(the_target) - the_target end - def construct_find_scope - { :conditions => conditions } + # Checks whether record is different to the current target, without loading it + def different_target?(record) + record.nil? && @owner[@reflection.foreign_key] || + record.id != @owner[@reflection.foreign_key] + end + + def replace_keys(record) + @owner[@reflection.foreign_key] = record && record[@reflection.association_primary_key] end - def foreign_key_present - !@owner[@reflection.primary_key_name].nil? + def foreign_key_present? + @owner[@reflection.foreign_key] end # NOTE - for now, we're only supporting inverse setting from belongs_to back onto @@ -88,17 +54,16 @@ module ActiveRecord inverse && inverse.macro == :has_one end - def record_id(record) - record.send(@reflection.options[:primary_key] || :id) + def target_id + if @reflection.options[:primary_key] + @owner.send(@reflection.name).try(:id) + else + @owner[@reflection.foreign_key] + end end - def previous_record_id - @previous_record_id ||= if @reflection.options[:primary_key] - previous_record = @owner.send(@reflection.name) - previous_record.nil? ? nil : previous_record.id - else - @owner[@reflection.primary_key_name] - end + def stale_state + @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 90eff7399c..4f67b02d00 100644 --- a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb @@ -1,80 +1,33 @@ module ActiveRecord # = Active Record Belongs To Polymorphic Association module Associations - class BelongsToPolymorphicAssociation < AssociationProxy #:nodoc: - def replace(record) - if record.nil? - @target = @owner[@reflection.primary_key_name] = @owner[@reflection.options[:foreign_type]] = nil - else - @target = (AssociationProxy === record ? record.target : record) - - @owner[@reflection.primary_key_name] = record_id(record) - @owner[@reflection.options[:foreign_type]] = record.class.base_class.name.to_s + class BelongsToPolymorphicAssociation < BelongsToAssociation #:nodoc: + private - @updated = true + def replace_keys(record) + super + @owner[@reflection.foreign_type] = record && record.class.base_class.name end - set_inverse_instance(record) - loaded - record - end - - def updated? - @updated - end - - def stale_target? - if @target && @target.persisted? - target_id = @target.send(@reflection.association_primary_key).to_s - foreign_key = @owner.send(@reflection.primary_key_name).to_s - target_type = @target.class.base_class.name - foreign_type = @owner.send(@reflection.options[:foreign_type]).to_s - - target_id != foreign_key || target_type != foreign_type - else - false + def different_target?(record) + super || record.class != target_klass end - end - - private def inverse_reflection_for(record) @reflection.polymorphic_inverse_of(record.class) end - def invertible_for?(record) - inverse = inverse_reflection_for(record) - inverse && inverse.macro == :has_one - end - - def construct_find_scope - { :conditions => conditions } - end - - def find_target - return nil if association_class.nil? - - target = association_class.send(:with_scope, :find => @scope[:find]) do - association_class.find( - @owner[@reflection.primary_key_name], - :select => @reflection.options[:select], - :include => @reflection.options[:include] - ) - end - set_inverse_instance(target) - target - end - - def foreign_key_present - !@owner[@reflection.primary_key_name].nil? + def target_klass + type = @owner[@reflection.foreign_type] + type && type.constantize end - def record_id(record) - record.send(@reflection.options[:primary_key] || :id) + def raise_on_type_mismatch(record) + # A polymorphic association cannot have a type mismatch, by definition end - def association_class - @owner[@reflection.options[:foreign_type]] ? @owner[@reflection.options[:foreign_type]].constantize : nil + def stale_state + [super, @owner[@reflection.foreign_type].to_s] end end end diff --git a/activerecord/lib/active_record/associations/class_methods/join_dependency.rb b/activerecord/lib/active_record/associations/class_methods/join_dependency.rb index bb6145656e..cb3edafab1 100644 --- a/activerecord/lib/active_record/associations/class_methods/join_dependency.rb +++ b/activerecord/lib/active_record/associations/class_methods/join_dependency.rb @@ -176,7 +176,7 @@ module ActiveRecord join_part = join_parts.detect { |j| j.reflection.name.to_s == name && - j.parent_table_name == parent.class.table_name } + j.parent_table_name == parent.class.table_name } raise(ConfigurationError, "No such association") unless join_part @@ -201,7 +201,7 @@ module ActiveRecord macro = join_part.reflection.macro if macro == :has_one - return if record.instance_variable_defined?("@#{join_part.reflection.name}") + return 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 @@ -223,7 +223,8 @@ module ActiveRecord end def set_target_and_inverse(join_part, association, record) - association_proxy = record.send("set_#{join_part.reflection.name}_target", association) + association_proxy = record.send(:association_proxy, join_part.reflection.name) + association_proxy.target = association association_proxy.send(:set_inverse_instance, association) end end diff --git a/activerecord/lib/active_record/associations/class_methods/join_dependency/join_association.rb b/activerecord/lib/active_record/associations/class_methods/join_dependency/join_association.rb index 02707dfae1..3fea24ebf8 100644 --- a/activerecord/lib/active_record/associations/class_methods/join_dependency/join_association.rb +++ b/activerecord/lib/active_record/associations/class_methods/join_dependency/join_association.rb @@ -193,11 +193,11 @@ module ActiveRecord first_key = second_key = nil if through_reflection.macro == :belongs_to - jt_primary_key = through_reflection.primary_key_name + jt_primary_key = through_reflection.foreign_key jt_foreign_key = through_reflection.association_primary_key else jt_primary_key = through_reflection.active_record_primary_key - jt_foreign_key = through_reflection.primary_key_name + jt_foreign_key = through_reflection.foreign_key if through_reflection.options[:as] # has_many :through against a polymorphic join jt_conditions << @@ -228,10 +228,10 @@ module ActiveRecord second_key = source_reflection.association_foreign_key jt_conditions << - join_table[reflection.source_reflection.options[:foreign_type]]. + join_table[reflection.source_reflection.foreign_type]. eq(reflection.options[:source_type]) else - second_key = source_reflection.primary_key_name + second_key = source_reflection.foreign_key end end @@ -262,7 +262,7 @@ module ActiveRecord end def join_belongs_to_to(relation) - foreign_key = options[:foreign_key] || reflection.primary_key_name + foreign_key = options[:foreign_key] || reflection.foreign_key primary_key = options[:primary_key] || reflection.klass.primary_key join_target_table( 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 b1d454545f..b28554dce1 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 @@ -2,24 +2,15 @@ module ActiveRecord # = Active Record Has And Belongs To Many Association module Associations class HasAndBelongsToManyAssociation < AssociationCollection #:nodoc: - def columns - @reflection.columns(@reflection.options[:join_table], "#{@reflection.options[:join_table]} Columns") - end - - def reset_column_information - @reflection.reset_column_information - end + attr_reader :join_table - def has_primary_key? - @has_primary_key ||= @owner.connection.supports_primary_key? && @owner.connection.primary_key(@reflection.options[:join_table]) + def initialize(owner, reflection) + @join_table_name = reflection.options[:join_table] + @join_table = Arel::Table.new(@join_table_name) + super end protected - def construct_find_options!(options) - options[:joins] = Arel::SqlLiteral.new(@scope[:find][:joins]) - options[:readonly] = finding_with_ambiguous_select?(options[:select] || @reflection.options[:select]) - options[:select] ||= (@reflection.options[:select] || Arel::SqlLiteral.new('*')) - end def count_records load_target.size @@ -33,26 +24,11 @@ module ActiveRecord if @reflection.options[:insert_sql] @owner.connection.insert(interpolate_sql(@reflection.options[:insert_sql], record)) else - relation = Arel::Table.new(@reflection.options[:join_table]) - timestamps = record_timestamp_columns(record) - timezone = record.send(:current_time_from_proper_timezone) if timestamps.any? - - attributes = columns.map do |column| - name = column.name - value = case name.to_s - when @reflection.primary_key_name.to_s - @owner.id - when @reflection.association_foreign_key.to_s - record.id - when *timestamps - timezone - else - @owner.send(:quote_value, record[name], column) if record.has_attribute?(name) - end - [relation[name], value] unless value.nil? - end + stmt = join_table.compile_insert( + join_table[@reflection.foreign_key] => @owner.id, + join_table[@reflection.association_foreign_key] => record.id + ) - stmt = relation.compile_insert Hash[attributes] @owner.connection.insert stmt.to_sql end @@ -63,8 +39,8 @@ module ActiveRecord if sql = @reflection.options[:delete_sql] records.each { |record| @owner.connection.delete(interpolate_sql(sql, record)) } else - relation = Arel::Table.new(@reflection.options[:join_table]) - stmt = relation.where(relation[@reflection.primary_key_name].eq(@owner.id). + relation = join_table + stmt = relation.where(relation[@reflection.foreign_key].eq(@owner.id). and(relation[@reflection.association_foreign_key].in(records.map { |x| x.id }.compact)) ).compile_delete @owner.connection.delete stmt.to_sql @@ -72,45 +48,28 @@ module ActiveRecord end def construct_joins - "INNER JOIN #{@owner.connection.quote_table_name @reflection.options[:join_table]} ON #{@reflection.quoted_table_name}.#{@reflection.klass.primary_key} = #{@owner.connection.quote_table_name @reflection.options[:join_table]}.#{@reflection.association_foreign_key}" - end + right = join_table + left = @reflection.klass.arel_table - def join_table - Arel::Table.new(@reflection.options[:join_table]) + condition = left[@reflection.klass.primary_key].eq( + right[@reflection.association_foreign_key]) + + right.create_join(right, right.create_on(condition)) end def construct_owner_conditions super(join_table) end - def construct_find_scope - { - :conditions => construct_conditions, - :joins => construct_joins, - :readonly => false, - :order => @reflection.options[:order], - :include => @reflection.options[:include], - :limit => @reflection.options[:limit] - } + def association_scope + super.joins(construct_joins) end - # Join tables with additional columns on top of the two foreign keys must be considered - # ambiguous unless a select clause has been explicitly defined. Otherwise you can get - # broken records back, if, for example, the join column also has an id column. This will - # then overwrite the id column of the records coming back. - def finding_with_ambiguous_select?(select_clause) - !select_clause && columns.size != 2 + def select_value + super || @reflection.klass.arel_table[Arel.star] end private - def record_timestamp_columns(record) - if record.record_timestamps - record.send(:all_timestamp_attributes).map { |x| x.to_s } - else - [] - end - end - def invertible_for?(record) false end diff --git a/activerecord/lib/active_record/associations/has_association.rb b/activerecord/lib/active_record/associations/has_association.rb deleted file mode 100644 index 0ecdb696ea..0000000000 --- a/activerecord/lib/active_record/associations/has_association.rb +++ /dev/null @@ -1,42 +0,0 @@ -module ActiveRecord - module Associations - # Included in all has_* associations (i.e. everything except belongs_to) - module HasAssociation #:nodoc: - protected - # Sets the owner attributes on the given record - def set_owner_attributes(record) - if @owner.persisted? - construct_owner_attributes.each { |key, value| record[key] = value } - end - end - - # Returns a hash linking the owner to the association represented by the reflection - def construct_owner_attributes(reflection = @reflection) - attributes = {} - if reflection.macro == :belongs_to - attributes[reflection.association_primary_key] = @owner.send(reflection.primary_key_name) - else - attributes[reflection.primary_key_name] = @owner.send(reflection.active_record_primary_key) - - if reflection.options[:as] - attributes["#{reflection.options[:as]}_type"] = @owner.class.base_class.name - end - end - attributes - end - - # Builds an array of arel nodes from the owner attributes hash - def construct_owner_conditions(table = aliased_table, reflection = @reflection) - construct_owner_attributes(reflection).map do |attr, value| - table[attr].eq(value) - end - end - - def construct_conditions - conditions = construct_owner_conditions - conditions << Arel.sql(sql_conditions) if sql_conditions - aliased_table.create_and(conditions) - end - end - end -end diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb index c3360463cd..b07441f3c6 100644 --- a/activerecord/lib/active_record/associations/has_many_association.rb +++ b/activerecord/lib/active_record/associations/has_many_association.rb @@ -7,14 +7,6 @@ module ActiveRecord # is provided by its child HasManyThroughAssociation. class HasManyAssociation < AssociationCollection #:nodoc: protected - def owner_quoted_id - if @reflection.options[:primary_key] - @owner.class.quote_value(@owner.send(@reflection.options[:primary_key])) - else - @owner.quoted_id - end - end - # Returns the number of records in this collection. # # If the association has a counter cache it gets that value. Otherwise @@ -34,7 +26,7 @@ module ActiveRecord elsif @reflection.options[:counter_sql] || @reflection.options[:finder_sql] @reflection.klass.count_by_sql(custom_counter_sql) else - @reflection.klass.count(@scope[:find].slice(:conditions, :joins, :include)) + scoped.count end # If there's nothing in the database and @target has no new records @@ -66,12 +58,10 @@ module ActiveRecord when :delete_all @reflection.klass.delete(records.map { |r| r.id }) else - updates = { @reflection.primary_key_name => nil } + updates = { @reflection.foreign_key => nil } conditions = { @reflection.association_primary_key => records.map { |r| r.id } } - with_scope(@scope) do - @reflection.klass.update_all(updates, conditions) - end + scoped.where(conditions).update_all(updates) end if has_cached_counter? && @reflection.options[:dependent] != :destroy @@ -79,19 +69,7 @@ module ActiveRecord end end - def construct_find_scope - { - :conditions => construct_conditions, - :readonly => false, - :order => @reflection.options[:order], - :limit => @reflection.options[:limit], - :include => @reflection.options[:include] - } - end - - def construct_create_scope - construct_owner_attributes - end + alias creation_attributes construct_owner_attributes end end 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 e2b008034e..400db6baf1 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -31,18 +31,13 @@ module ActiveRecord protected def target_reflection_has_associated_record? - if @reflection.through_reflection.macro == :belongs_to && @owner[@reflection.through_reflection.primary_key_name].blank? + if @reflection.through_reflection.macro == :belongs_to && @owner[@reflection.through_reflection.foreign_key].blank? false else true end end - def construct_find_options!(options) - options[:joins] = [construct_joins] + Array.wrap(options[:joins]) - options[:include] = @reflection.source_reflection.options[:include] if options[:include].nil? && @reflection.source_reflection.options[:include] - end - def insert_record(record, force = true, validate = true) if record.new_record? return false unless save_record(record, force, validate) @@ -62,7 +57,6 @@ module ActiveRecord def find_target return [] unless target_reflection_has_associated_record? - update_stale_state scoped.all end diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb index c32aaf986e..6614cbbf18 100644 --- a/activerecord/lib/active_record/associations/has_one_association.rb +++ b/activerecord/lib/active_record/associations/has_one_association.rb @@ -1,122 +1,62 @@ module ActiveRecord # = Active Record Belongs To Has One Association module Associations - class HasOneAssociation < AssociationProxy #:nodoc: - include HasAssociation - - def create(attrs = {}, replace_existing = true) - new_record(replace_existing) do |reflection| - attrs = merge_with_conditions(attrs) - reflection.create_association(attrs) - end - end - - def create!(attrs = {}, replace_existing = true) - new_record(replace_existing) do |reflection| - attrs = merge_with_conditions(attrs) - reflection.create_association!(attrs) - end - end + class HasOneAssociation < SingularAssociation #:nodoc: + def replace(record, save = true) + record = check_record(record) + load_target - def build(attrs = {}, replace_existing = true) - new_record(replace_existing) do |reflection| - attrs = merge_with_conditions(attrs) - reflection.build_association(attrs) - end - end + @reflection.klass.transaction do + if @target && @target != record + remove_target!(@reflection.options[:dependent]) + end - def replace(obj, dont_save = false) - load_target + if record + set_inverse_instance(record) + set_owner_attributes(record) - unless @target.nil? || @target == obj - if @reflection.options[:dependent] && !dont_save - case @reflection.options[:dependent] - when :delete - @target.delete if @target.persisted? - @owner.clear_association_cache - when :destroy - @target.destroy if @target.persisted? - @owner.clear_association_cache - when :nullify - @target[@reflection.primary_key_name] = nil - @target.save if @owner.persisted? && @target.persisted? + if @owner.persisted? && save && !record.save + nullify_owner_attributes(record) + set_owner_attributes(@target) + raise RecordNotSaved, "Failed to save the new associated #{@reflection.name}." end - else - @target[@reflection.primary_key_name] = nil - @target.save if @owner.persisted? && @target.persisted? end end - if obj.nil? - @target = nil - else - raise_on_type_mismatch(obj) - set_owner_attributes(obj) - @target = (AssociationProxy === obj ? obj.target : obj) - end - - set_inverse_instance(obj) - @loaded = true - - unless !@owner.persisted? || obj.nil? || dont_save - return (obj.save ? self : false) - else - return (obj.nil? ? nil : self) - end + self.target = record end - protected - def owner_quoted_id - if @reflection.options[:primary_key] - @owner.class.quote_value(@owner.send(@reflection.options[:primary_key])) - else - @owner.quoted_id - end - end - private - def find_target - options = @reflection.options.dup.slice(:select, :order, :include, :readonly) - - the_target = with_scope(:find => @scope[:find]) do - @reflection.klass.find(:first, options) - end - set_inverse_instance(the_target) - the_target + def association_scope + super.order(@reflection.options[:order]) end - def construct_find_scope - { :conditions => construct_conditions } - end + alias creation_attributes construct_owner_attributes - def construct_create_scope - construct_owner_attributes + # The reason that the save param for replace is false, if for create (not just build), + # is because the setting of the foreign keys is actually handled by the scoping when + # the record is instantiated, and so they are set straight away and do not need to be + # updated within replace. + def set_new_record(record) + replace(record, false) end - def new_record(replace_existing) - # Make sure we load the target first, if we plan on replacing the existing - # instance. Otherwise, if the target has not previously been loaded - # elsewhere, the instance we create will get orphaned. - load_target if replace_existing - record = @reflection.klass.send(:with_scope, :create => @scope[:create]) do - yield @reflection - end - - if replace_existing - replace(record, true) + def remove_target!(method) + if [:delete, :destroy].include?(method) + @target.send(method) else - record[@reflection.primary_key_name] = @owner.id if @owner.persisted? - self.target = record - set_inverse_instance(record) - end + nullify_owner_attributes(@target) - record + if @target.persisted? && @owner.persisted? && !@target.save + set_owner_attributes(@target) + raise RecordNotSaved, "Failed to remove the existing associated #{@reflection.name}. " + + "The record failed to save when after its foreign key was set to nil." + end + end end - def merge_with_conditions(attrs={}) - attrs ||= {} - attrs.update(@reflection.options[:conditions]) if @reflection.options[:conditions].is_a?(Hash) - attrs + def nullify_owner_attributes(record) + record[@reflection.foreign_key] = nil end end end diff --git a/activerecord/lib/active_record/associations/has_one_through_association.rb b/activerecord/lib/active_record/associations/has_one_through_association.rb index c9ae930e93..dcd74e7346 100644 --- a/activerecord/lib/active_record/associations/has_one_through_association.rb +++ b/activerecord/lib/active_record/associations/has_one_through_association.rb @@ -4,37 +4,31 @@ module ActiveRecord class HasOneThroughAssociation < HasOneAssociation include ThroughAssociation - def replace(new_value) - create_through_record(new_value) - @target = new_value + def replace(record) + create_through_record(record) + self.target = record end private - def create_through_record(new_value) - proxy = @owner.send(@reflection.through_reflection.name) || - @owner.send(:association_instance_get, @reflection.through_reflection.name) - record = proxy.target + def create_through_record(record) + through_proxy = @owner.send(:association_proxy, @reflection.through_reflection.name) + through_record = through_proxy.send(:load_target) - if record && !new_value - record.destroy - elsif new_value - attributes = construct_join_attributes(new_value) + if through_record && !record + through_record.destroy + elsif record + attributes = construct_join_attributes(record) - if record - record.update_attributes(attributes) + if through_record + through_record.update_attributes(attributes) elsif @owner.new_record? - proxy.build(attributes) + through_proxy.build(attributes) else - proxy.create(attributes) + through_proxy.create(attributes) end end end - - def find_target - update_stale_state - scoped.first - end end end end diff --git a/activerecord/lib/active_record/associations/singular_association.rb b/activerecord/lib/active_record/associations/singular_association.rb new file mode 100644 index 0000000000..b6f49c6f36 --- /dev/null +++ b/activerecord/lib/active_record/associations/singular_association.rb @@ -0,0 +1,41 @@ +module ActiveRecord + module Associations + class SingularAssociation < AssociationProxy #:nodoc: + def create(attributes = {}) + record = scoped.scoping { @reflection.create_association(attributes) } + set_new_record(record) + record + end + + def create!(attributes = {}) + build(attributes).tap { |record| record.save! } + end + + def build(attributes = {}) + record = scoped.scoping { @reflection.build_association(attributes) } + set_new_record(record) + record + end + + private + def find_target + scoped.first.tap { |record| set_inverse_instance(record) } + end + + # Implemented by subclasses + def replace(record) + raise NotImplementedError + end + + def set_new_record(record) + replace(record) + end + + def check_record(record) + record = record.target if AssociationProxy === record + raise_on_type_mismatch(record) if record + record + end + end + end +end diff --git a/activerecord/lib/active_record/associations/through_association.rb b/activerecord/lib/active_record/associations/through_association.rb index 57718285f8..d2112fb2b6 100644 --- a/activerecord/lib/active_record/associations/through_association.rb +++ b/activerecord/lib/active_record/associations/through_association.rb @@ -3,43 +3,25 @@ module ActiveRecord module Associations module ThroughAssociation - def scoped - with_scope(@scope) do - @reflection.klass.scoped & - @reflection.through_reflection.klass.scoped - end - end - - def stale_target? - if @target && @reflection.through_reflection.macro == :belongs_to && defined?(@through_foreign_key) - previous_key = @through_foreign_key.to_s - current_key = @owner.send(@reflection.through_reflection.primary_key_name).to_s + protected - previous_key != current_key - else - false - end + def target_scope + super & @reflection.through_reflection.klass.scoped end - protected - - def construct_find_scope - { - :conditions => construct_conditions, - :joins => construct_joins, - :include => @reflection.options[:include] || @reflection.source_reflection.options[:include], - :select => construct_select, - :order => @reflection.options[:order], - :limit => @reflection.options[:limit], - :readonly => @reflection.options[:readonly] - } + def association_scope + scope = super.joins(construct_joins).where(conditions) + unless @reflection.options[:include] + scope = scope.includes(@reflection.source_reflection.options[:include]) + end + scope end # This scope affects the creation of the associated records (not the join records). At the # moment we only support creating on a :through association when the source reflection is a # belongs_to. Thus it's not necessary to set a foreign key on the associated record(s), so # this scope has can legitimately be empty. - def construct_create_scope + def creation_attributes { } end @@ -55,11 +37,6 @@ module ActiveRecord super(aliased_through_table, @reflection.through_reflection) end - def construct_select - @reflection.options[:select] || - @reflection.options[:uniq] && "DISTINCT #{@reflection.quoted_table_name}.*" - end - def construct_joins right = aliased_through_table left = @reflection.klass.arel_table @@ -69,14 +46,14 @@ module ActiveRecord if @reflection.source_reflection.macro == :belongs_to reflection_primary_key = @reflection.source_reflection.options[:primary_key] || @reflection.klass.primary_key - source_primary_key = @reflection.source_reflection.primary_key_name + source_primary_key = @reflection.source_reflection.foreign_key if @reflection.options[:source_type] - column = @reflection.source_reflection.options[:foreign_type] + column = @reflection.source_reflection.foreign_type conditions << right[column].eq(@reflection.options[:source_type]) end else - reflection_primary_key = @reflection.source_reflection.primary_key_name + reflection_primary_key = @reflection.source_reflection.foreign_key source_primary_key = @reflection.source_reflection.options[:primary_key] || @reflection.through_reflection.klass.primary_key if @reflection.source_reflection.options[:as] @@ -100,12 +77,12 @@ module ActiveRecord raise ActiveRecord::HasManyThroughCantAssociateThroughHasOneOrManyReflection.new(@owner, @reflection) if [:has_one, :has_many].include?(@reflection.source_reflection.macro) join_attributes = { - @reflection.source_reflection.primary_key_name => + @reflection.source_reflection.foreign_key => associate.send(@reflection.source_reflection.association_primary_key) } if @reflection.options[:source_type] - join_attributes.merge!(@reflection.source_reflection.options[:foreign_type] => associate.class.base_class.name) + join_attributes.merge!(@reflection.source_reflection.foreign_type => associate.class.base_class.name) end if @reflection.through_reflection.options[:conditions].is_a?(Hash) @@ -121,18 +98,13 @@ module ActiveRecord end def build_conditions - association_conditions = @reflection.options[:conditions] through_conditions = build_through_conditions source_conditions = @reflection.source_reflection.options[:conditions] uses_sti = !@reflection.through_reflection.klass.descends_from_active_record? - if association_conditions || through_conditions || source_conditions || uses_sti + if through_conditions || source_conditions || uses_sti all = [] - - [association_conditions, source_conditions].each do |conditions| - all << interpolate_sql(sanitize_sql(conditions)) if conditions - end - + all << interpolate_sql(sanitize_sql(source_conditions)) if source_conditions all << through_conditions if through_conditions all << build_sti_condition if uses_sti @@ -157,13 +129,16 @@ module ActiveRecord alias_method :sql_conditions, :conditions - def update_stale_state - construct_scope if stale_target? - + def stale_state if @reflection.through_reflection.macro == :belongs_to - @through_foreign_key = @owner.send(@reflection.through_reflection.primary_key_name) + @owner[@reflection.through_reflection.foreign_key].to_s end end + + def foreign_key_present? + @reflection.through_reflection.macro == :belongs_to && + !@owner[@reflection.through_reflection.foreign_key].nil? + end end end end diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb index 506f6e878f..660fa9a564 100644 --- a/activerecord/lib/active_record/attribute_methods/read.rb +++ b/activerecord/lib/active_record/attribute_methods/read.rb @@ -20,7 +20,7 @@ module ActiveRecord # be cached. Usually caching only pays off for attributes with expensive conversion # methods, like time related columns (e.g. +created_at+, +updated_at+). def cache_attributes(*attribute_names) - attribute_names.each {|attr| cached_attributes << attr.to_s} + cached_attributes.merge attribute_names.map { |attr| attr.to_s } end # Returns the attributes which are cached. By default time related columns @@ -39,7 +39,7 @@ module ActiveRecord if serialized_attributes.include?(attr_name) define_read_method_for_serialized_attribute(attr_name) else - define_read_method(attr_name.to_sym, attr_name, columns_hash[attr_name]) + define_read_method(attr_name, attr_name, columns_hash[attr_name]) end if attr_name == primary_key && attr_name != "id" @@ -55,7 +55,7 @@ module ActiveRecord # Define read method for serialized attribute. def define_read_method_for_serialized_attribute(attr_name) access_code = "@attributes_cache['#{attr_name}'] ||= unserialize_attribute('#{attr_name}')" - generated_attribute_methods.module_eval("def #{attr_name}; #{access_code}; end", __FILE__, __LINE__) + generated_attribute_methods.module_eval("def _#{attr_name}; #{access_code}; end; alias #{attr_name} _#{attr_name}", __FILE__, __LINE__) end # Define an attribute reader method. Cope with nil column. @@ -64,7 +64,7 @@ module ActiveRecord access_code = cast_code ? "(v=@attributes['#{attr_name}']) && #{cast_code}" : "@attributes['#{attr_name}']" unless attr_name.to_s == self.primary_key.to_s - access_code = access_code.insert(0, "missing_attribute('#{attr_name}', caller) unless @attributes.has_key?('#{attr_name}'); ") + access_code.insert(0, "missing_attribute('#{attr_name}', caller) unless @attributes.has_key?('#{attr_name}'); ") end if cache_attribute?(attr_name) diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb index 4a18719551..70ed16eeaf 100644 --- a/activerecord/lib/active_record/autosave_association.rb +++ b/activerecord/lib/active_record/autosave_association.rb @@ -343,8 +343,8 @@ module ActiveRecord association.destroy else key = reflection.options[:primary_key] ? send(reflection.options[:primary_key]) : id - if autosave != false && (new_record? || association.new_record? || association[reflection.primary_key_name] != key || autosave) - association[reflection.primary_key_name] = key + if autosave != false && (new_record? || association.new_record? || association[reflection.foreign_key] != key || autosave) + association[reflection.foreign_key] = key saved = association.save(:validate => !autosave) raise ActiveRecord::Rollback if !saved && autosave saved @@ -367,7 +367,8 @@ module ActiveRecord if association.updated? association_id = association.send(reflection.options[:primary_key] || :id) - self[reflection.primary_key_name] = association_id + self[reflection.foreign_key] = association_id + association.loaded end saved if autosave diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index 396ab226a9..47dc2d4a3a 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -829,7 +829,7 @@ module ActiveRecord #:nodoc: def arel_engine @arel_engine ||= begin if self == ActiveRecord::Base - Arel::Table.engine + ActiveRecord::Base else connection_handler.connection_pools[name] ? self : superclass.arel_engine end @@ -870,20 +870,36 @@ module ActiveRecord #:nodoc: reset_scoped_methods end + # Specifies how the record is loaded by +Marshal+. + # + # +_load+ sets an instance variable for each key in the hash it takes as input. + # Override this method if you require more complex marshalling. + def _load(data) + record = allocate + record.init_with(Marshal.load(data)) + record + end + + + # Finder methods must instantiate through this method to work with the + # single-table inheritance model that makes it possible to create + # objects of different types from the same table. + def instantiate(record) # :nodoc: + model = find_sti_class(record[inheritance_column]).allocate + model.init_with('attributes' => record) + model + end + private def relation #:nodoc: @relation ||= Relation.new(self, arel_table) - finder_needs_type_condition? ? @relation.where(type_condition) : @relation - end - # Finder methods must instantiate through this method to work with the - # single-table inheritance model that makes it possible to create - # objects of different types from the same table. - def instantiate(record) - model = find_sti_class(record[inheritance_column]).allocate - model.init_with('attributes' => record) - model + if finder_needs_type_condition? + @relation.where(type_condition).create_with(inheritance_column.to_sym => sti_name) + else + @relation + end end def find_sti_class(type_name) @@ -914,10 +930,9 @@ module ActiveRecord #:nodoc: def type_condition sti_column = arel_table[inheritance_column.to_sym] - condition = sti_column.eq(sti_name) - descendants.each { |subclass| condition = condition.or(sti_column.eq(subclass.sti_name)) } + sti_names = ([self] + descendants).map { |model| model.sti_name } - condition + sti_column.in(sti_names) end # Guesses the table name, but does not decorate it with prefix and suffix information. @@ -1364,6 +1379,8 @@ MSG # hence you can't have attributes that aren't part of the table columns. def initialize(attributes = nil) @attributes = attributes_from_column_definition + @association_cache = {} + @aggregation_cache = {} @attributes_cache = {} @new_record = true @readonly = false @@ -1382,6 +1399,22 @@ MSG result end + # Populate +coder+ with attributes about this record that should be + # serialized. The structure of +coder+ defined in this method is + # guaranteed to match the structure of +coder+ passed to the +init_with+ + # method. + # + # Example: + # + # class Post < ActiveRecord::Base + # end + # coder = {} + # Post.new.encode_with(coder) + # coder # => { 'id' => nil, ... } + def encode_with(coder) + coder['attributes'] = attributes + end + # Initialize an empty model object from +coder+. +coder+ must contain # the attributes necessary for initializing an empty model object. For # example: @@ -1395,12 +1428,24 @@ MSG def init_with(coder) @attributes = coder['attributes'] @attributes_cache, @previously_changed, @changed_attributes = {}, {}, {} + @association_cache = {} + @aggregation_cache = {} @readonly = @destroyed = @marked_for_destruction = false @new_record = false _run_find_callbacks _run_initialize_callbacks end + # Specifies how the record is dumped by +Marshal+. + # + # +_dump+ emits a marshalled hash which has been passed to +encode_with+. Override this + # method if you require more complex marshalling. + def _dump(level) + dump = {} + encode_with(dump) + Marshal.dump(dump) + end + # Returns a String, which Action Pack uses for constructing an URL to this # object. The default implementation returns this record's id as a String, # or nil if this record's unsaved. @@ -1533,7 +1578,7 @@ MSG # Returns true if the specified +attribute+ has been set by the user or by a database load and is neither # nil nor empty? (the latter only applies to objects that respond to empty?, most notably Strings). def attribute_present?(attribute) - !read_attribute(attribute).blank? + !_read_attribute(attribute).blank? end # Returns the column object for the named attribute. @@ -1606,9 +1651,9 @@ MSG @changed_attributes[attr] = orig_value if field_changed?(attr, orig_value, @attributes[attr]) end - clear_aggregation_cache - clear_association_cache - @attributes_cache = {} + @aggregation_cache = {} + @association_cache = {} + @attributes_cache = {} @new_record = true ensure_proper_type 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 ee9a0af35c..01e53b46c8 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -1,3 +1,5 @@ +require 'active_support/core_ext/module/deprecation' + module ActiveRecord module ConnectionAdapters # :nodoc: module DatabaseStatements @@ -229,6 +231,8 @@ module ActiveRecord # # This method *modifies* the +sql+ parameter. # + # This method is deprecated!! Stop using it! + # # ===== Examples # add_limit_offset!('SELECT * FROM suppliers', {:limit => 10, :offset => 50}) # generates @@ -243,6 +247,7 @@ module ActiveRecord end sql end + deprecate :add_limit_offset! def default_sequence_name(table, column) nil diff --git a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb index d555308485..1db397f584 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb @@ -60,7 +60,7 @@ module ActiveRecord result = if @query_cache[sql].key?(binds) ActiveSupport::Notifications.instrument("sql.active_record", - :sql => sql, :name => "CACHE", :connection_id => self.object_id) + :sql => sql, :name => "CACHE", :connection_id => object_id) @query_cache[sql][binds] else @query_cache[sql][binds] = yield diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index 0282493219..5ff5813699 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -77,8 +77,8 @@ module ActiveRecord false end - # Does this adapter support savepoints? PostgreSQL and MySQL do, SQLite - # does not. + # Does this adapter support savepoints? PostgreSQL and MySQL do, + # SQLite < 3.6.8 does not. def supports_savepoints? false end diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb index ce2352486b..47acf0b254 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb @@ -364,9 +364,14 @@ module ActiveRecord # statement API. For those queries, we need to use this method. :'( log(sql, name) do result = @connection.query(sql) - cols = result.fetch_fields.map { |field| field.name } - rows = result.to_a - result.free + cols = [] + rows = [] + + if result + cols = result.fetch_fields.map { |field| field.name } + rows = result.to_a + result.free + end ActiveRecord::Result.new(cols, rows) end end @@ -400,7 +405,7 @@ module ActiveRecord def begin_db_transaction #:nodoc: exec_without_stmt "BEGIN" - rescue Exception + rescue Mysql::Error # Transactions aren't supported end @@ -439,6 +444,7 @@ module ActiveRecord end sql end + deprecate :add_limit_offset! # SCHEMA STATEMENTS ======================================== diff --git a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb index d76fc4103e..b04383d5bf 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb @@ -62,6 +62,10 @@ module ActiveRecord sqlite_version >= '2.0.0' end + def supports_savepoints? + sqlite_version >= '3.6.8' + end + # Returns +true+ when the connection adapter supports prepared statement # caching, otherwise returns +false+ def supports_statement_cache? @@ -189,6 +193,18 @@ module ActiveRecord exec_query(sql, name).rows end + def create_savepoint + execute("SAVEPOINT #{current_savepoint_name}") + end + + def rollback_to_savepoint + execute("ROLLBACK TO SAVEPOINT #{current_savepoint_name}") + end + + def release_savepoint + execute("RELEASE SAVEPOINT #{current_savepoint_name}") + end + def begin_db_transaction #:nodoc: @connection.transaction end @@ -314,12 +330,7 @@ module ActiveRecord protected def select(sql, name = nil, binds = []) #:nodoc: - result = exec_query(sql, name, binds) - columns = result.columns.map { |column| - column.sub(/^"?\w+"?\./, '') - } - - result.rows.map { |row| Hash[columns.zip(row)] } + exec_query(sql, name, binds).to_a end def table_structure(table_name) diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index 6fb723f2f5..b6f0511b9a 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -615,14 +615,9 @@ class Fixtures < (RUBY_VERSION < '1.9' ? YAML::Omap : Hash) fk_name = (association.options[:foreign_key] || "#{association.name}_id").to_s if association.name.to_s != fk_name && value = row.delete(association.name.to_s) - if association.options[:polymorphic] - if value.sub!(/\s*\(([^\)]*)\)\s*$/, "") - target_type = $1 - target_type_name = (association.options[:foreign_type] || "#{association.name}_type").to_s - - # support polymorphic belongs_to as "label (Type)" - row[target_type_name] = target_type - end + if association.options[:polymorphic] && value.sub!(/\s*\(([^\)]*)\)\s*$/, "") + # support polymorphic belongs_to as "label (Type)" + row[association.foreign_type] = $1 end row[fk_name] = Fixtures.identify(value) @@ -634,7 +629,7 @@ class Fixtures < (RUBY_VERSION < '1.9' ? YAML::Omap : Hash) targets.each do |target| join_fixtures["#{label}_#{target}"] = Fixture.new( - { association.primary_key_name => row[primary_key_name], + { association.foreign_key => row[primary_key_name], association.association_foreign_key => Fixtures.identify(target) }, nil, @connection) end diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake index a4fc18148e..9924755ddf 100644 --- a/activerecord/lib/active_record/railties/databases.rake +++ b/activerecord/lib/active_record/railties/databases.rake @@ -409,7 +409,7 @@ db_namespace = namespace :db do ENV['PGHOST'] = abcs["test"]["host"] if abcs["test"]["host"] ENV['PGPORT'] = abcs["test"]["port"].to_s if abcs["test"]["port"] ENV['PGPASSWORD'] = abcs["test"]["password"].to_s if abcs["test"]["password"] - `psql -U "#{abcs["test"]["username"]}" -f #{Rails.root}/db/#{Rails.env}_structure.sql #{abcs["test"]["database"]}` + `psql -U "#{abcs["test"]["username"]}" -f #{Rails.root}/db/#{Rails.env}_structure.sql #{abcs["test"]["database"]} #{abcs["test"]["template"]}` when "sqlite", "sqlite3" dbfile = abcs["test"]["database"] || abcs["test"]["dbfile"] `#{abcs["test"]["adapter"]} #{dbfile} < #{Rails.root}/db/#{Rails.env}_structure.sql` diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index 0310e7050d..ceeb0ec39d 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -1,4 +1,5 @@ require 'active_support/core_ext/class/attribute' +require 'active_support/core_ext/module/deprecation' module ActiveRecord # = Active Record Reflection @@ -196,8 +197,17 @@ module ActiveRecord @quoted_table_name ||= klass.quoted_table_name end + def foreign_key + @foreign_key ||= options[:foreign_key] || derive_foreign_key + end + def primary_key_name - @primary_key_name ||= options[:foreign_key] || derive_primary_key_name + foreign_key + end + deprecate :primary_key_name => :foreign_key + + def foreign_type + @foreign_type ||= options[:foreign_type] || "#{name}_type" end def primary_key_column @@ -251,7 +261,7 @@ module ActiveRecord false end - def through_reflection_primary_key_name + def through_reflection_foreign_key end def source_reflection @@ -298,22 +308,36 @@ module ActiveRecord !options[:validate].nil? ? options[:validate] : (options[:autosave] == true || macro == :has_many) end - def dependent_conditions(record, base_class, extra_conditions) - dependent_conditions = [] - dependent_conditions << "#{primary_key_name} = #{record.send(name).send(:owner_quoted_id)}" - dependent_conditions << "#{options[:as]}_type = '#{base_class.name}'" if options[:as] - dependent_conditions << klass.send(:sanitize_sql, options[:conditions]) if options[:conditions] - dependent_conditions << extra_conditions if extra_conditions - dependent_conditions = dependent_conditions.collect {|where| "(#{where})" }.join(" AND ") - dependent_conditions = dependent_conditions.gsub('@', '\@') - dependent_conditions - end - # Returns +true+ if +self+ is a +belongs_to+ reflection. def belongs_to? macro == :belongs_to end + def proxy_class + case macro + when :belongs_to + if options[:polymorphic] + Associations::BelongsToPolymorphicAssociation + else + Associations::BelongsToAssociation + end + when :has_and_belongs_to_many + Associations::HasAndBelongsToManyAssociation + when :has_many + if options[:through] + Associations::HasManyThroughAssociation + else + Associations::HasManyAssociation + end + when :has_one + if options[:through] + Associations::HasOneThroughAssociation + else + Associations::HasOneAssociation + end + end + end + private def derive_class_name class_name = name.to_s.camelize @@ -321,7 +345,7 @@ module ActiveRecord class_name end - def derive_primary_key_name + def derive_foreign_key if belongs_to? "#{name}_id" elsif options[:as] @@ -403,11 +427,11 @@ module ActiveRecord end def through_reflection_primary_key - through_reflection.belongs_to? ? through_reflection.klass.primary_key : through_reflection.primary_key_name + through_reflection.belongs_to? ? through_reflection.klass.primary_key : through_reflection.foreign_key end - def through_reflection_primary_key_name - through_reflection.primary_key_name if through_reflection.belongs_to? + def through_reflection_foreign_key + through_reflection.foreign_key if through_reflection.belongs_to? end private diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index 20e983b5f7..1441e9750e 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -10,7 +10,9 @@ module ActiveRecord include FinderMethods, Calculations, SpawnMethods, QueryMethods, Batches + # These are explicitly delegated to improve performance (avoids method_missing) delegate :to_xml, :to_yaml, :length, :collect, :map, :each, :all?, :include?, :to => :to_a + delegate :table_name, :primary_key, :to => :klass attr_reader :table, :klass, :loaded attr_accessor :extensions @@ -30,13 +32,12 @@ module ActiveRecord def insert(values) im = arel.compile_insert values im.into @table - primary_key_name = @klass.primary_key - primary_key_value = primary_key_name && Hash === values ? values[primary_key] : nil + primary_key_value = primary_key && Hash === values ? values[table[primary_key]] : nil @klass.connection.insert( im.to_sql, 'SQL', - primary_key_name, + primary_key, primary_key_value) end @@ -177,7 +178,7 @@ module ActiveRecord stmt = arel.compile_update(Arel.sql(@klass.send(:sanitize_sql_for_assignment, updates))) stmt.take limit stmt.order(*order) - stmt.key = @klass.arel_table[@klass.primary_key] + stmt.key = table[primary_key] @klass.connection.update stmt.to_sql end end @@ -320,7 +321,7 @@ module ActiveRecord # # Delete multiple rows # Todo.delete([2,3,4]) def delete(id_or_array) - where(@klass.primary_key => id_or_array).delete_all + where(primary_key => id_or_array).delete_all end def reload @@ -336,10 +337,6 @@ module ActiveRecord self end - def primary_key - @primary_key ||= table[@klass.primary_key] - end - def to_sql @to_sql ||= arel.to_sql end @@ -373,10 +370,6 @@ module ActiveRecord to_a.inspect end - def table_name - @klass.table_name - end - protected def method_missing(method, *args, &block) diff --git a/activerecord/lib/active_record/relation/batches.rb b/activerecord/lib/active_record/relation/batches.rb index b41e935ed5..359af9820f 100644 --- a/activerecord/lib/active_record/relation/batches.rb +++ b/activerecord/lib/active_record/relation/batches.rb @@ -65,7 +65,7 @@ module ActiveRecord batch_size = options.delete(:batch_size) || 1000 relation = relation.except(:order).order(batch_order).limit(batch_size) - records = relation.where(primary_key.gteq(start)).all + records = relation.where(table[primary_key].gteq(start)).all while records.any? yield records @@ -73,7 +73,7 @@ module ActiveRecord break if records.size < batch_size if primary_key_offset = records.last.id - records = relation.where(primary_key.gt(primary_key_offset)).to_a + records = relation.where(table[primary_key].gt(primary_key_offset)).to_a else raise "Primary key not included in the custom select clause" end @@ -83,7 +83,7 @@ module ActiveRecord private def batch_order - "#{@klass.table_name}.#{@klass.primary_key} ASC" + "#{table_name}.#{primary_key} ASC" end end end diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb index fd45bb24dd..b75a65e3ca 100644 --- a/activerecord/lib/active_record/relation/calculations.rb +++ b/activerecord/lib/active_record/relation/calculations.rb @@ -168,7 +168,7 @@ module ActiveRecord unless arel.ast.grep(Arel::Nodes::OuterJoin).empty? distinct = true - column_name = @klass.primary_key if column_name == :all + column_name = primary_key if column_name == :all end distinct = nil if column_name =~ /\s*DISTINCT\s+/i @@ -211,7 +211,7 @@ module ActiveRecord group_attr = @group_values association = @klass.reflect_on_association(group_attr.first.to_sym) associated = group_attr.size == 1 && association && association.macro == :belongs_to # only count belongs_to associations - group_fields = Array(associated ? association.primary_key_name : group_attr) + group_fields = Array(associated ? association.foreign_key : group_attr) group_aliases = group_fields.map { |field| column_alias_for(field) } group_columns = group_aliases.zip(group_fields).map { |aliaz,field| [aliaz, column_for(field)] @@ -282,15 +282,11 @@ module ActiveRecord end def type_cast_calculated_value(value, column, operation = nil) - if value.is_a?(String) || value.nil? - case operation - when 'count' then value.to_i - when 'sum' then type_cast_using_column(value || '0', column) - when 'average' then value.try(:to_d) - else type_cast_using_column(value, column) - end - else - type_cast_using_column(value, column) + case operation + when 'count' then value.to_i + when 'sum' then type_cast_using_column(value || '0', column) + when 'average' then value.try(:to_d) + else type_cast_using_column(value, column) end end diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index 8bc28c06cb..e447de92a4 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -171,13 +171,13 @@ module ActiveRecord def exists?(id = nil) id = id.id if ActiveRecord::Base === id - relation = select(primary_key).limit(1) + relation = select("1").limit(1) case id when Array, Hash relation = relation.where(id) else - relation = relation.where(primary_key.eq(id)) if id + relation = relation.where(table[primary_key].eq(id)) if id end relation.first ? true : false @@ -188,7 +188,8 @@ module ActiveRecord def find_with_associations including = (@eager_load_values + @includes_values).uniq join_dependency = ActiveRecord::Associations::ClassMethods::JoinDependency.new(@klass, including, []) - rows = construct_relation_for_association_find(join_dependency).to_a + relation = construct_relation_for_association_find(join_dependency) + rows = connection.select_all(relation.to_sql, 'SQL', relation.bind_values) join_dependency.instantiate(rows) rescue ThrowResult [] @@ -225,10 +226,10 @@ module ActiveRecord def construct_limited_ids_condition(relation) orders = relation.order_values - values = @klass.connection.distinct("#{@klass.connection.quote_table_name @klass.table_name}.#{@klass.primary_key}", orders) + values = @klass.connection.distinct("#{@klass.connection.quote_table_name table_name}.#{primary_key}", orders) - ids_array = relation.select(values).collect {|row| row[@klass.primary_key]} - ids_array.empty? ? raise(ThrowResult) : primary_key.in(ids_array) + ids_array = relation.select(values).collect {|row| row[primary_key]} + ids_array.empty? ? raise(ThrowResult) : table[primary_key].in(ids_array) end def find_by_attributes(match, attributes, *args) @@ -290,24 +291,24 @@ module ActiveRecord def find_one(id) id = id.id if ActiveRecord::Base === id - column = columns_hash[primary_key.name.to_s] + column = columns_hash[primary_key] substitute = connection.substitute_for(column, @bind_values) - relation = where(primary_key.eq(substitute)) + relation = where(table[primary_key].eq(substitute)) relation.bind_values = [[column, id]] record = relation.first unless record conditions = arel.where_sql conditions = " [#{conditions}]" if conditions - raise RecordNotFound, "Couldn't find #{@klass.name} with #{@klass.primary_key}=#{id}#{conditions}" + raise RecordNotFound, "Couldn't find #{@klass.name} with #{primary_key}=#{id}#{conditions}" end record end def find_some(ids) - result = where(primary_key.in(ids)).all + result = where(table[primary_key].in(ids)).all expected_size = if @limit_value && ids.size > @limit_value diff --git a/activerecord/lib/active_record/relation/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb index b3f1b9e36a..61d9974570 100644 --- a/activerecord/lib/active_record/relation/predicate_builder.rb +++ b/activerecord/lib/active_record/relation/predicate_builder.rb @@ -20,7 +20,7 @@ module ActiveRecord case value when Array, ActiveRecord::Associations::AssociationCollection, ActiveRecord::Relation values = value.to_a.map { |x| - x.respond_to?(:quoted_id) ? x.quoted_id : x + x.is_a?(ActiveRecord::Base) ? x.id : x } attribute.in(values) when Range, Arel::Relation diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index 0ab55ae864..2cbb103eb9 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -128,7 +128,7 @@ module ActiveRecord def create_with(value) relation = clone - relation.create_with_value = value + relation.create_with_value = value && (@create_with_value || {}).merge(value) relation end @@ -152,7 +152,7 @@ module ActiveRecord order_clause = arel.order_clauses order = order_clause.empty? ? - "#{@klass.table_name}.#{@klass.primary_key} DESC" : + "#{table_name}.#{primary_key} DESC" : reverse_sql_order(order_clause).join(', ') except(:order).order(Arel.sql(order)) diff --git a/activerecord/lib/active_record/relation/spawn_methods.rb b/activerecord/lib/active_record/relation/spawn_methods.rb index ae777208db..69a7642ec5 100644 --- a/activerecord/lib/active_record/relation/spawn_methods.rb +++ b/activerecord/lib/active_record/relation/spawn_methods.rb @@ -46,13 +46,15 @@ module ActiveRecord merged_relation.where_values = merged_wheres - (Relation::SINGLE_VALUE_METHODS - [:lock]).each do |method| + (Relation::SINGLE_VALUE_METHODS - [:lock, :create_with]).each do |method| value = r.send(:"#{method}_value") merged_relation.send(:"#{method}_value=", value) unless value.nil? end merged_relation.lock_value = r.lock_value unless merged_relation.lock_value + merged_relation = merged_relation.create_with(r.create_with_value) if r.create_with_value + # Apply scope extension modules merged_relation.send :apply_modules, r.extensions @@ -112,7 +114,7 @@ module ActiveRecord options.assert_valid_keys(VALID_FIND_OPTIONS) finders = options.dup - finders.delete_if { |key, value| value.nil? } + finders.delete_if { |key, value| value.nil? && key != :limit } ([:joins, :select, :group, :order, :having, :limit, :offset, :from, :lock, :readonly] & finders.keys).each do |finder| relation = relation.send(finder, finders[finder]) diff --git a/activerecord/lib/active_record/result.rb b/activerecord/lib/active_record/result.rb index 8deff1478f..47a5ac2700 100644 --- a/activerecord/lib/active_record/result.rb +++ b/activerecord/lib/active_record/result.rb @@ -23,7 +23,7 @@ module ActiveRecord private def hash_rows @hash_rows ||= @rows.map { |row| - ActiveSupport::OrderedHash[@columns.zip(row)] + Hash[@columns.zip(row)] } end end diff --git a/activerecord/lib/active_record/schema_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb index e30b481fe1..a893c0ad85 100644 --- a/activerecord/lib/active_record/schema_dumper.rb +++ b/activerecord/lib/active_record/schema_dumper.rb @@ -83,7 +83,7 @@ HEADER # first dump primary key column if @connection.respond_to?(:pk_and_sequence_for) - pk, pk_seq = @connection.pk_and_sequence_for(table) + pk, _ = @connection.pk_and_sequence_for(table) elsif @connection.respond_to?(:primary_key) pk = @connection.primary_key(table) end diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb index 868f761a33..48d2f7c9a9 100644 --- a/activerecord/lib/active_record/transactions.rb +++ b/activerecord/lib/active_record/transactions.rb @@ -325,16 +325,14 @@ module ActiveRecord @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) - 1 if @_start_transaction_state[:level] < 1 restore_state = remove_instance_variable(:@_start_transaction_state) - if restore_state - @attributes = @attributes.dup if @attributes.frozen? - @new_record = restore_state[:new_record] - @destroyed = restore_state[:destroyed] - if restore_state.has_key?(:id) - self.id = restore_state[:id] - else - @attributes.delete(self.class.primary_key) - @attributes_cache.delete(self.class.primary_key) - end + @attributes = @attributes.dup if @attributes.frozen? + @new_record = restore_state[:new_record] + @destroyed = restore_state[:destroyed] + if restore_state.has_key?(:id) + self.id = restore_state[:id] + else + @attributes.delete(self.class.primary_key) + @attributes_cache.delete(self.class.primary_key) end end end |