aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/lib/active_record
diff options
context:
space:
mode:
Diffstat (limited to 'activerecord/lib/active_record')
-rwxr-xr-xactiverecord/lib/active_record/associations.rb328
-rw-r--r--activerecord/lib/active_record/associations/association_collection.rb87
-rw-r--r--activerecord/lib/active_record/associations/association_proxy.rb49
-rw-r--r--activerecord/lib/active_record/associations/belongs_to_association.rb70
-rw-r--r--activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb55
-rw-r--r--activerecord/lib/active_record/associations/has_many_association.rb64
-rw-r--r--activerecord/lib/active_record/associations/has_one_association.rb48
-rwxr-xr-xactiverecord/lib/active_record/base.rb6
-rwxr-xr-xactiverecord/lib/active_record/callbacks.rb25
-rw-r--r--activerecord/lib/active_record/deprecated_associations.rb54
10 files changed, 502 insertions, 284 deletions
diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb
index e7e5a7d71d..882baffbe4 100755
--- a/activerecord/lib/active_record/associations.rb
+++ b/activerecord/lib/active_record/associations.rb
@@ -1,4 +1,7 @@
+require 'active_record/associations/association_proxy'
require 'active_record/associations/association_collection'
+require 'active_record/associations/belongs_to_association'
+require 'active_record/associations/has_one_association'
require 'active_record/associations/has_many_association'
require 'active_record/associations/has_and_belongs_to_many_association'
require 'active_record/deprecated_associations'
@@ -30,9 +33,9 @@ module ActiveRecord
# end
#
# The project class now has the following methods (and more) to ease the traversal and manipulation of its relationships:
- # * <tt>Project#portfolio, Project#portfolio=(portfolio), Project#portfolio.nil?, Project#portfolio?(portfolio)</tt>
+ # * <tt>Project#portfolio, Project#portfolio=(portfolio), Project#portfolio.nil?</tt>
# * <tt>Project#project_manager, Project#project_manager=(project_manager), Project#project_manager.nil?,</tt>
- # <tt>Project#project_manager?(project_manager), Project#build_project_manager, Project#create_project_manager</tt>
+ # <tt>Project#project_manager.build, Project#project_manager.create</tt>
# * <tt>Project#milestones.empty?, Project#milestones.size, Project#milestones, Project#milestones<<(milestone),</tt>
# <tt>Project#milestones.delete(milestone), Project#milestones.find(milestone_id), Project#milestones.find_all(conditions),</tt>
# <tt>Project#milestones.build, Project#milestones.create</tt>
@@ -71,6 +74,29 @@ module ActiveRecord
# PRIMARY KEY (id)
# )
#
+ # == Unsaved objects and associations
+ #
+ # You can manipulate objects and associations before they are saved to the database, but there is some special behaviour you should be
+ # aware of, mostly involving the saving of associated objects.
+ #
+ # === 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 (new_record? == true).
+ # * If either of these saves fail (due to one of the objects being invalid) the assignment statement returns false and the assignment
+ # is cancelled.
+ # * If you wish to assign an object to a has_one association without saving it, use the #association.build method (documented below).
+ # * 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.
+ #
+ # === 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.
+ # * If saving any of the objects being added to a collection (via #push or similar) fails, then #push returns false.
+ # * You can add an object to a collection without automatically saving it by using the #collection.build method (documented below).
+ # * All unsaved (new_record? == true) members of the collection are automatically saved when the parent is saved.
+ #
# == Caching
#
# All of the methods are built on a simple caching principle that will keep the result of the last query around unless specifically
@@ -124,7 +150,7 @@ module ActiveRecord
module ClassMethods
# Adds the following methods for retrieval and query of collections of associated objects.
# +collection+ is replaced with the symbol passed as the first argument, so
- # <tt>has_many :clients</tt> would add among others <tt>has_clients?</tt>.
+ # <tt>has_many :clients</tt> would add among others <tt>clients.empty?</tt>.
# * <tt>collection(force_reload = false)</tt> - returns an array of all the associated objects.
# An empty array is returned if none are found.
# * <tt>collection<<(object, ...)</tt> - adds one or more objects to the collection by setting their foreign keys to the collection's primary key.
@@ -200,18 +226,9 @@ module ActiveRecord
module_eval "before_destroy { |record| #{association_class_name}.delete_all(%(#{association_class_primary_key_name} = \#{record.quoted_id})) }"
end
- define_method(association_name) do |*params|
- force_reload = params.first unless params.empty?
- association = instance_variable_get("@#{association_name}")
- if association.nil?
- association = HasManyAssociation.new(self,
- association_name, association_class_name,
- association_class_primary_key_name, options)
- instance_variable_set("@#{association_name}", association)
- end
- association.reload if force_reload
- association
- end
+ add_multiple_associated_save_callbacks(association_name)
+
+ association_accessor_methods(association_name, association_class_name, association_class_primary_key_name, options, HasManyAssociation)
# deprecated api
deprecated_collection_count_method(association_name)
@@ -220,31 +237,28 @@ module ActiveRecord
deprecated_has_collection_method(association_name)
deprecated_find_in_collection_method(association_name)
deprecated_find_all_in_collection_method(association_name)
- deprecated_create_method(association_name)
- deprecated_build_method(association_name)
+ deprecated_collection_create_method(association_name)
+ deprecated_collection_build_method(association_name)
end
# Adds the following methods for retrieval and query of a single associated object.
# +association+ is replaced with the symbol passed as the first argument, so
- # <tt>has_one :manager</tt> would add among others <tt>has_manager?</tt>.
+ # <tt>has_one :manager</tt> would add among others <tt>manager.nil?</tt>.
# * <tt>association(force_reload = false)</tt> - returns the associated object. Nil is returned if none is found.
# * <tt>association=(associate)</tt> - assigns the associate object, extracts the primary key, sets it as the foreign key,
# and saves the associate object.
- # * <tt>association?(object, force_reload = false)</tt> - returns true if the +object+ is of the same type and has the
- # same id as the associated object.
# * <tt>association.nil?</tt> - returns true if there is no associated object.
- # * <tt>build_association(attributes = {})</tt> - returns a new object of the associated type that has been instantiated
+ # * <tt>association.build(attributes = {})</tt> - 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.
- # * <tt>create_association(attributes = {})</tt> - returns a new object of the associated type that has been instantiated
+ # * <tt>association.create(attributes = {})</tt> - returns a new object of the associated type that has been instantiated
# with +attributes+ and linked to this object through a foreign key and that has already been saved (if it passed the validation).
#
# Example: An Account class declares <tt>has_one :beneficiary</tt>, which will add:
# * <tt>Account#beneficiary</tt> (similar to <tt>Beneficiary.find_first "account_id = #{id}"</tt>)
# * <tt>Account#beneficiary=(beneficiary)</tt> (similar to <tt>beneficiary.account_id = account.id; beneficiary.save</tt>)
- # * <tt>Account#beneficiary?</tt> (similar to <tt>account.beneficiary == some_beneficiary</tt>)
# * <tt>Account#beneficiary.nil?</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#beneficiary.build</tt> (similar to <tt>Beneficiary.new("account_id" => id)</tt>)
+ # * <tt>Account#beneficiary.create</tt> (similar to <tt>b = Beneficiary.new("account_id" => id); b.save; b</tt>)
# The declaration can also include an options hash to specialize the behavior of the association.
#
# Options are:
@@ -265,35 +279,53 @@ module ActiveRecord
# has_one :last_comment, :class_name => "Comment", :order => "posted_on"
# has_one :project_manager, :class_name => "Person", :conditions => "role = 'project_manager'"
def has_one(association_id, options = {})
- options.merge!({ :remote => true })
- belongs_to(association_id, options)
+ validate_options([ :class_name, :foreign_key, :remote, :conditions, :order, :dependent, :counter_cache ], options.keys)
- association_name, association_class_name, class_primary_key_name =
+ association_name, association_class_name, association_class_primary_key_name =
associate_identification(association_id, options[:class_name], options[:foreign_key], false)
require_association_class(association_class_name)
- has_one_writer_method(association_name, association_class_name, class_primary_key_name)
- build_method("build_", association_name, association_class_name, class_primary_key_name)
- create_method("create_", association_name, association_class_name, class_primary_key_name)
+ module_eval do
+ after_save <<-EOF
+ association = instance_variable_get("@#{association_name}")
+ if (true or @new_record_before_save) and association.respond_to?(:loaded?) and not association.nil?
+ association["#{association_class_primary_key_name}"] = id
+ association.save(true)
+ association.send(:construct_sql)
+ end
+ EOF
+ end
+
+ association_accessor_methods(association_name, association_class_name, association_class_primary_key_name, options, HasOneAssociation)
- module_eval "before_destroy '#{association_name}.destroy if has_#{association_name}?'" if options[:dependent]
+ module_eval "before_destroy '#{association_name}.destroy unless #{association_name}.nil?'" if options[:dependent]
+
+ # deprecated api
+ deprecated_has_association_method(association_name)
+ deprecated_build_method("build_", association_name, association_class_name, association_class_primary_key_name)
+ deprecated_create_method("create_", association_name, association_class_name, association_class_primary_key_name)
+ deprecated_association_comparison_method(association_name, association_class_name)
end
# Adds the following methods for retrieval and query for a single associated object that this object holds an id to.
# +association+ is replaced with the symbol passed as the first argument, so
- # <tt>belongs_to :author</tt> would add among others <tt>has_author?</tt>.
+ # <tt>belongs_to :author</tt> would add among others <tt>author.nil?</tt>.
# * <tt>association(force_reload = false)</tt> - returns the associated object. Nil is returned if none is found.
# * <tt>association=(associate)</tt> - assigns the associate object, extracts the primary key, and sets it as the foreign key.
- # * <tt>association?(object, force_reload = false)</tt> - returns true if the +object+ is of the same type and has the
- # same id as the associated object.
# * <tt>association.nil?</tt> - returns true if there is no associated object.
+ # * <tt>association.build(attributes = {})</tt> - 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.
+ # * <tt>association.create(attributes = {})</tt> - returns a new object of the associated type that has been instantiated
+ # with +attributes+ and linked to this object through a foreign key and that has already been saved (if it passed the validation).
#
# Example: A Post class declares <tt>has_one :author</tt>, which will add:
# * <tt>Post#author</tt> (similar to <tt>Author.find(author_id)</tt>)
# * <tt>Post#author=(author)</tt> (similar to <tt>post.author_id = author.id</tt>)
# * <tt>Post#author?</tt> (similar to <tt>post.author == some_author</tt>)
# * <tt>Post#author.nil?</tt>
+ # * <tt>Post#author.build</tt> (similar to <tt>Author.new("post_id" => id)</tt>)
+ # * <tt>Post#author.create</tt> (similar to <tt>b = Author.new("post_id" => id); b.save; b</tt>)
# The declaration can also include an options hash to specialize the behavior of the association.
#
# Options are:
@@ -317,47 +349,46 @@ module ActiveRecord
# belongs_to :author, :class_name => "Person", :foreign_key => "author_id"
# belongs_to :valid_coupon, :class_name => "Coupon", :foreign_key => "coupon_id",
# :conditions => 'discounts > #{payments_count}'
- def belongs_to(association_id, options = {})
- validate_options([ :class_name, :foreign_key, :remote, :conditions, :order, :dependent, :counter_cache ], options.keys)
+ def belongs_to(association_id, options = {})
+ validate_options([ :class_name, :foreign_key, :remote, :conditions, :order, :dependent, :counter_cache ], options.keys)
- association_name, association_class_name, class_primary_key_name =
- associate_identification(association_id, options[:class_name], options[:foreign_key], false)
+ association_name, association_class_name, class_primary_key_name =
+ associate_identification(association_id, options[:class_name], options[:foreign_key], false)
- require_association_class(association_class_name)
+ require_association_class(association_class_name)
- association_class_primary_key_name = options[:foreign_key] || Inflector.underscore(Inflector.demodulize(association_class_name)) + "_id"
+ association_class_primary_key_name = options[:foreign_key] || Inflector.underscore(Inflector.demodulize(association_class_name)) + "_id"
- if options[:remote]
- association_finder = <<-"end_eval"
- #{association_class_name}.find_first(
- "#{class_primary_key_name} = \#{quoted_id}#{options[:conditions] ? " AND " + options[:conditions] : ""}",
- #{options[:order] ? "\"" + options[:order] + "\"" : "nil" }
- )
- end_eval
- else
- association_finder = options[:conditions] ?
- "#{association_class_name}.find_on_conditions(read_attribute(\"#{association_class_primary_key_name}\"), \"#{options[:conditions]}\")" :
- "#{association_class_name}.find(read_attribute(\"#{association_class_primary_key_name}\"))"
- end
-
- has_association_method(association_name)
- association_reader_method(association_name, association_finder)
- belongs_to_writer_method(association_name, association_class_name, association_class_primary_key_name)
- association_comparison_method(association_name, association_class_name)
+ association_accessor_methods(association_name, association_class_name, association_class_primary_key_name, options, BelongsToAssociation)
- if options[:counter_cache]
- module_eval(
- "after_create '#{association_class_name}.increment_counter(\"#{Inflector.pluralize(self.to_s.downcase). + "_count"}\", #{association_class_primary_key_name})" +
- " if has_#{association_name}?'"
- )
+ module_eval do
+ before_save <<-EOF
+ association = instance_variable_get("@#{association_name}")
+ if association.respond_to?(:loaded?) and not association.nil? and association.new_record?
+ association.save(true)
+ self["#{association_class_primary_key_name}"] = association.id
+ association.send(:construct_sql)
+ end
+ EOF
+ end
+
+ if options[:counter_cache]
+ module_eval(
+ "after_create '#{association_class_name}.increment_counter(\"#{Inflector.pluralize(self.to_s.downcase). + "_count"}\", #{association_class_primary_key_name})" +
+ " unless #{association_name}.nil?'"
+ )
- module_eval(
- "before_destroy '#{association_class_name}.decrement_counter(\"#{Inflector.pluralize(self.to_s.downcase) + "_count"}\", #{association_class_primary_key_name})" +
- " if has_#{association_name}?'"
- )
- end
+ module_eval(
+ "before_destroy '#{association_class_name}.decrement_counter(\"#{Inflector.pluralize(self.to_s.downcase) + "_count"}\", #{association_class_primary_key_name})" +
+ " unless #{association_name}.nil?'"
+ )
end
+ # deprecated api
+ deprecated_has_association_method(association_name)
+ deprecated_association_comparison_method(association_name, association_class_name)
+ end
+
# Associates two classes via an intermediate join table. Unless the join table is explicitly specified as
# an option, it is guessed using the lexical order of the class names. So a join between Developer and Project
# will give the default join table name of "developers_projects" because "D" outranks "P".
@@ -371,7 +402,7 @@ module ActiveRecord
#
# Adds the following methods for retrieval and query.
# +collection+ is replaced with the symbol passed as the first argument, so
- # <tt>has_and_belongs_to_many :categories</tt> would add among others +add_categories+.
+ # <tt>has_and_belongs_to_many :categories</tt> would add among others +categories.empty?+.
# * <tt>collection(force_reload = false)</tt> - returns an array of all the associated objects.
# An empty array is returned if none is found.
# * <tt>collection<<(object, ...)</tt> - adds one or more objects to the collection by creating associations in the join table
@@ -385,10 +416,13 @@ module ActiveRecord
# * <tt>collection.clear</tt> - removes every object from the collection. This does not destroy the objects.
# * <tt>collection.empty?</tt> - returns true if there are no associated objects.
# * <tt>collection.size</tt> - returns the number of associated objects.
+ # * <tt>collection.find(id)</tt> - finds an associated object responding to the +id+ and that
+ # meets the condition that it has to be associated with this object.
#
# Example: An Developer class declares <tt>has_and_belongs_to_many :projects</tt>, which will add:
# * <tt>Developer#projects</tt>
# * <tt>Developer#projects<<</tt>
+ # * <tt>Developer#projects.push_with_attributes</tt>
# * <tt>Developer#projects.delete</tt>
# * <tt>Developer#projects.clear</tt>
# * <tt>Developer#projects.empty?</tt>
@@ -431,23 +465,13 @@ module ActiveRecord
require_association_class(association_class_name)
- join_table = options[:join_table] ||
- join_table_name(undecorated_table_name(self.to_s), undecorated_table_name(association_class_name))
-
- define_method(association_name) do |*params|
- force_reload = params.first unless params.empty?
- association = instance_variable_get("@#{association_name}")
- if association.nil?
- association = HasAndBelongsToManyAssociation.new(self,
- association_name, association_class_name,
- association_class_primary_key_name, join_table, options)
- instance_variable_set("@#{association_name}", association)
- end
- association.reload if force_reload
- association
- end
+ options[:join_table] ||= join_table_name(undecorated_table_name(self.to_s), undecorated_table_name(association_class_name))
+
+ add_multiple_associated_save_callbacks(association_name)
+
+ association_accessor_methods(association_name, association_class_name, association_class_primary_key_name, options, HasAndBelongsToManyAssociation)
- before_destroy_sql = "DELETE FROM #{join_table} WHERE #{association_class_primary_key_name} = \\\#{self.quoted_id}"
+ before_destroy_sql = "DELETE FROM #{options[:join_table]} WHERE #{association_class_primary_key_name} = \\\#{self.quoted_id}"
module_eval(%{before_destroy "self.connection.delete(%{#{before_destroy_sql}})"}) # "
# deprecated api
@@ -487,94 +511,70 @@ module ActiveRecord
return association_id.id2name, association_class_name, primary_key_name
end
- def association_comparison_method(association_name, association_class_name)
- module_eval <<-"end_eval", __FILE__, __LINE__
- def #{association_name}?(comparison_object, force_reload = false)
- if comparison_object.kind_of?(#{association_class_name})
- #{association_name}(force_reload) == comparison_object
- else
- raise "Comparison object is a #{association_class_name}, should have been \#{comparison_object.class.name}"
- end
+ def association_accessor_methods(association_name, association_class_name, association_class_primary_key_name, options, association_proxy_class)
+ define_method(association_name) do |*params|
+ force_reload = params.first unless params.empty?
+ association = instance_variable_get("@#{association_name}")
+ unless association.respond_to?(:loaded?)
+ association = association_proxy_class.new(self,
+ association_name, association_class_name,
+ association_class_primary_key_name, options)
+ instance_variable_set("@#{association_name}", association)
end
- end_eval
- end
+ association.reload if force_reload
+ association
+ end
- def association_reader_method(association_name, association_finder)
- module_eval <<-"end_eval", __FILE__, __LINE__
- def #{association_name}(force_reload = false)
- if @#{association_name}.nil? || force_reload
- begin
- @#{association_name} = #{association_finder}
- rescue ActiveRecord::StatementInvalid, ActiveRecord::RecordNotFound
- nil
- end
- end
-
- return @#{association_name}
+ define_method("#{association_name}=") do |new_value|
+ association = instance_variable_get("@#{association_name}")
+ unless association.respond_to?(:loaded?)
+ association = association_proxy_class.new(self,
+ association_name, association_class_name,
+ association_class_primary_key_name, options)
+ instance_variable_set("@#{association_name}", association)
end
- end_eval
+ association.replace(new_value)
+ association
+ end
end
- def has_one_writer_method(association_name, association_class_name, class_primary_key_name)
- module_eval <<-"end_eval", __FILE__, __LINE__
- def #{association_name}=(association)
- if association.nil?
- @#{association_name}.#{class_primary_key_name} = nil
- @#{association_name}.save(false)
- @#{association_name} = nil
- else
- raise ActiveRecord::AssociationTypeMismatch unless #{association_class_name} === association
- association.#{class_primary_key_name} = id
- association.save(false)
- @#{association_name} = association
- end
- end
- end_eval
+ def require_association_class(class_name)
+ require_association(Inflector.underscore(class_name)) if class_name
end
- def belongs_to_writer_method(association_name, association_class_name, association_class_primary_key_name)
- module_eval <<-"end_eval", __FILE__, __LINE__
- def #{association_name}=(association)
- if association.nil?
- @#{association_name} = self.#{association_class_primary_key_name} = nil
- else
- raise ActiveRecord::AssociationTypeMismatch unless #{association_class_name} === association
- @#{association_name} = association
- self.#{association_class_primary_key_name} = association.id
+ def add_multiple_associated_save_callbacks(association_name)
+ module_eval do
+ before_save <<-end_eval
+ @new_record_before_save = new_record?
+ association = instance_variable_get("@#{association_name}")
+ if association.respond_to?(:loaded?)
+ if new_record?
+ records_to_save = association
+ else
+ records_to_save = association.select{ |record| record.new_record? }
+ end
+ records_to_save.inject(true) do |result,record|
+ result &&= record.valid?
+ end
end
- end
- end_eval
- end
-
- def has_association_method(association_name)
- module_eval <<-"end_eval", __FILE__, __LINE__
- def has_#{association_name}?(force_reload = false)
- !#{association_name}(force_reload).nil?
- end
- end_eval
- end
-
- def build_method(method_prefix, collection_name, collection_class_name, class_primary_key_name)
- module_eval <<-"end_eval", __FILE__, __LINE__
- def #{method_prefix + collection_name}(attributes = {})
- association = #{collection_class_name}.new
- association.attributes = attributes.merge({ "#{class_primary_key_name}" => id})
- association
- end
- end_eval
- end
-
- def create_method(method_prefix, collection_name, collection_class_name, class_primary_key_name)
- module_eval <<-"end_eval", __FILE__, __LINE__
- def #{method_prefix + collection_name}(attributes = nil)
- #{collection_class_name}.create((attributes || {}).merge({ "#{class_primary_key_name}" => id}))
- end
- end_eval
- end
+ end_eval
+ end
- def require_association_class(class_name)
- require_association(Inflector.underscore(class_name)) if class_name
+ module_eval do
+ after_save <<-end_eval
+ association = instance_variable_get("@#{association_name}")
+ if association.respond_to?(:loaded?)
+ if @new_record_before_save
+ records_to_save = association
+ else
+ records_to_save = association.select{ |record| record.new_record? }
+ end
+ records_to_save.each{ |record| association.send(:insert_record, record) }
+ association.send(:construct_sql) # reconstruct the SQL queries now that we know the owner's id
+ end
+ end_eval
+ end
end
end
end
-end \ No newline at end of file
+end
diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb
index ca87fae5ff..334cdccc3d 100644
--- a/activerecord/lib/active_record/associations/association_collection.rb
+++ b/activerecord/lib/active_record/associations/association_collection.rb
@@ -1,51 +1,34 @@
module ActiveRecord
module Associations
- class AssociationCollection #:nodoc:
- alias_method :proxy_respond_to?, :respond_to?
- instance_methods.each { |m| undef_method m unless m =~ /(^__|^nil\?|^proxy_respond_to\?)/ }
-
- def initialize(owner, association_name, association_class_name, association_class_primary_key_name, options)
- @owner = owner
- @options = options
- @association_name = association_name
- @association_class = eval(association_class_name)
- @association_class_primary_key_name = association_class_primary_key_name
- end
-
- def method_missing(symbol, *args, &block)
- load_collection
- @collection.send(symbol, *args, &block)
- end
-
+ class AssociationCollection < AssociationProxy #:nodoc:
def to_ary
- load_collection
- @collection.to_ary
+ load_target
+ @target.to_ary
end
- def respond_to?(symbol, include_priv = false)
- proxy_respond_to?(symbol, include_priv) || [].respond_to?(symbol, include_priv)
+ def reset
+ @target = []
+ @loaded = false
end
- def loaded?
- !@collection.nil?
- end
-
def reload
- @collection = nil
+ reset
end
# Add +records+ to this association. Returns +self+ so method calls may be chained.
# Since << flattens its argument list and inserts each record, +push+ and +concat+ behave identically.
def <<(*records)
+ result = true
+ load_target
@owner.transaction do
flatten_deeper(records).each do |record|
raise_on_type_mismatch(record)
- insert_record(record)
- @collection << record if loaded?
+ result &&= insert_record(record) unless @owner.new_record?
+ @target << record
end
end
- self
+ result and self
end
alias_method :push, :<<
@@ -54,11 +37,13 @@ module ActiveRecord
# Remove +records+ from this association. Does not destroy +records+.
def delete(*records)
records = flatten_deeper(records)
+ records.each { |record| raise_on_type_mismatch(record) }
+ records.reject! { |record| @target.delete(record) if record.new_record? }
+ return if records.empty?
@owner.transaction do
- records.each { |record| raise_on_type_mismatch(record) }
delete_records(records)
- records.each { |record| @collection.delete(record) } if loaded?
+ records.each { |record| @target.delete(record) }
end
end
@@ -67,20 +52,27 @@ module ActiveRecord
each { |record| record.destroy }
end
- @collection = []
+ @target = []
end
+ def create(attributes = {})
+ # Can't use Base.create since the foreign key may be a protected attribute.
+ record = build(attributes)
+ record.save unless @owner.new_record?
+ record
+ end
+
# Returns the size of the collection by executing a SELECT COUNT(*) query if the collection hasn't been loaded and
# calling collection.size if it has. If it's more likely than not that the collection does have a size larger than zero
# and you need to fetch that collection afterwards, it'll take one less SELECT query if you use length.
def size
- if loaded? then @collection.size else count_records end
+ if loaded? then @target.size else count_records end
end
# Returns the size of the collection by loading it and calling size on the array. If you want to use this method to check
# whether the collection is empty, use collection.length.zero? instead of collection.empty?
def length
- load_collection.size
+ load_target.size
end
def empty?
@@ -91,11 +83,14 @@ module ActiveRecord
collection.inject([]) { |uniq_records, record| uniq_records << record unless uniq_records.include?(record); uniq_records }
end
- protected
- def loaded?
- not @collection.nil?
- end
+ def replace(other_array)
+ other_array.each{ |val| raise_on_type_mismatch(val) }
+
+ @target = other_array
+ @loaded = true
+ end
+ protected
def quoted_record_ids(records)
records.map { |record| record.quoted_id }.join(',')
end
@@ -117,22 +112,14 @@ module ActiveRecord
end
private
- def load_collection
- if loaded?
- @collection
- else
- begin
- @collection = find_all_records
- rescue ActiveRecord::RecordNotFound
- @collection = []
- end
- end
- end
-
def raise_on_type_mismatch(record)
raise ActiveRecord::AssociationTypeMismatch, "#{@association_class} expected, got #{record.class}" unless record.is_a?(@association_class)
end
+ def target_obsolete?
+ false
+ end
+
# Array#flatten has problems with rescursive arrays. Going one level deeper solves the majority of the problems.
def flatten_deeper(array)
array.collect { |element| element.respond_to?(:flatten) ? element.flatten : element }.flatten
diff --git a/activerecord/lib/active_record/associations/association_proxy.rb b/activerecord/lib/active_record/associations/association_proxy.rb
new file mode 100644
index 0000000000..dcba207e20
--- /dev/null
+++ b/activerecord/lib/active_record/associations/association_proxy.rb
@@ -0,0 +1,49 @@
+module ActiveRecord
+ module Associations
+ class AssociationProxy #:nodoc:
+ alias_method :proxy_respond_to?, :respond_to?
+ instance_methods.each { |m| undef_method m unless m =~ /(^__|^nil\?|^proxy_respond_to\?|^send)/ }
+
+ def initialize(owner, association_name, association_class_name, association_class_primary_key_name, options)
+ @owner = owner
+ @options = options
+ @association_name = association_name
+ @association_class = eval(association_class_name)
+ @association_class_primary_key_name = association_class_primary_key_name
+
+ reset
+ end
+
+ def method_missing(symbol, *args, &block)
+ load_target
+ @target.send(symbol, *args, &block)
+ end
+
+ def respond_to?(symbol, include_priv = false)
+ load_target
+ proxy_respond_to?(symbol, include_priv) || @target.respond_to?(symbol, include_priv)
+ end
+
+ def loaded?
+ @loaded
+ end
+
+ private
+ def load_target
+ unless @owner.new_record?
+ begin
+ @target = find_target if not loaded?
+ rescue ActiveRecord::RecordNotFound
+ reset
+ end
+ end
+ @loaded = true
+ @target
+ end
+
+ def raise_on_type_mismatch(record)
+ raise ActiveRecord::AssociationTypeMismatch, "#{@association_class} expected, got #{record.class}" unless record.is_a?(@association_class)
+ 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
new file mode 100644
index 0000000000..aa627f7495
--- /dev/null
+++ b/activerecord/lib/active_record/associations/belongs_to_association.rb
@@ -0,0 +1,70 @@
+module ActiveRecord
+ module Associations
+ class BelongsToAssociation < AssociationProxy #:nodoc:
+
+ def reset
+ @target = nil
+ @loaded = false
+ end
+
+ def reload
+ reset
+ load_target
+ end
+
+ def create(attributes = {})
+ record = build(attributes)
+ record.save
+ record
+ end
+
+ def build(attributes = {})
+ record = @association_class.new(attributes)
+ replace(record, true)
+ record
+ end
+
+ def replace(obj, dont_save = false)
+ if obj.nil?
+ @target = @owner[@association_class_primary_key_name] = nil
+ else
+ raise_on_type_mismatch(obj) unless obj.nil?
+
+ @target = obj
+ @owner[@association_class_primary_key_name] = obj.id unless obj.new_record?
+ end
+ @loaded = true
+ end
+
+ # Ugly workaround - .nil? is done in C and the method_missing trick doesn't work when we pretend to be nil
+ def nil?
+ load_target
+ @target.nil?
+ end
+
+ private
+ def find_target
+ if @options[:conditions]
+ @association_class.find_on_conditions(@owner[@association_class_primary_key_name], @options[:conditions])
+ else
+ @association_class.find(@owner[@association_class_primary_key_name])
+ end
+ end
+
+ def target_obsolete?
+ @owner[@association_class_primary_key_name] != @target.id
+ end
+
+ def construct_sql
+ # no sql to construct
+ end
+ end
+ end
+end
+
+class NilClass #:nodoc:
+ # Ugly workaround - nil comparison is usually done in C and so a proxy object pretending to be nil doesn't work.
+ def ==(other)
+ other.nil?
+ end
+end
diff --git a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb
index 1152846df2..83b87547ee 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
@@ -1,26 +1,27 @@
module ActiveRecord
module Associations
class HasAndBelongsToManyAssociation < AssociationCollection #:nodoc:
- def initialize(owner, association_name, association_class_name, association_class_primary_key_name, join_table, options)
- super(owner, association_name, association_class_name, association_class_primary_key_name, options)
+ def initialize(owner, association_name, association_class_name, association_class_primary_key_name, options)
+ super
@association_foreign_key = options[:association_foreign_key] || Inflector.underscore(Inflector.demodulize(association_class_name)) + "_id"
- association_table_name = options[:table_name] || @association_class.table_name
- @join_table = join_table
+ @association_table_name = options[:table_name] || @association_class.table_name
+ @join_table = options[:join_table]
@order = options[:order] || "t.#{@association_class.primary_key}"
- interpolate_sql_options!(options, :finder_sql, :delete_sql)
- @finder_sql = options[:finder_sql] ||
- "SELECT t.*, j.* FROM #{association_table_name} t, #{@join_table} j " +
- "WHERE t.#{@association_class.primary_key} = j.#{@association_foreign_key} AND " +
- "j.#{association_class_primary_key_name} = #{@owner.quoted_id} " +
- (options[:conditions] ? " AND " + interpolate_sql(options[:conditions]) : "") + " " +
- "ORDER BY #{@order}"
+ construct_sql
end
+ def build(attributes = {})
+ load_target
+ record = @association_class.new(attributes)
+ @target << record
+ record
+ end
+
# Removes all records from this association. Returns +self+ so method calls may be chained.
def clear
- return self if size == 0 # forces load_collection if hasn't happened already
+ return self if size == 0 # forces load_target if hasn't happened already
if sql = @options[:delete_sql]
each { |record| @owner.connection.execute(sql) }
@@ -34,12 +35,12 @@ module ActiveRecord
@owner.connection.execute(sql)
end
- @collection = []
+ @target = []
self
end
def find_first
- load_collection.first
+ load_target.first
end
def find(*args)
@@ -56,16 +57,16 @@ module ActiveRecord
elsif @options[:finder_sql]
if ids.size == 1
id = ids.first
- record = load_collection.detect { |record| id == record.id }
+ record = load_target.detect { |record| id == record.id }
expects_array? ? [record] : record
else
- load_collection.select { |record| ids.include?(record.id) }
+ load_target.select { |record| ids.include?(record.id) }
end
# Otherwise, construct a query.
else
ids_list = ids.map { |id| @owner.send(:quote, id) }.join(',')
- records = find_all_records(@finder_sql.sub(/ORDER BY/, "AND j.#{@association_foreign_key} IN (#{ids_list}) ORDER BY"))
+ records = find_target(@finder_sql.sub(/ORDER BY/, "AND j.#{@association_foreign_key} IN (#{ids_list}) ORDER BY"))
if records.size == ids.size
if ids.size == 1 and !expects_array
records.first
@@ -82,7 +83,7 @@ module ActiveRecord
raise_on_type_mismatch(record)
insert_record_with_join_attributes(record, join_attributes)
join_attributes.each { |key, value| record.send(:write_attribute, key, value) }
- @collection << record if loaded?
+ @target << record
self
end
@@ -93,16 +94,17 @@ module ActiveRecord
end
protected
- def find_all_records(sql = @finder_sql)
+ def find_target(sql = @finder_sql)
records = @association_class.find_by_sql(sql)
@options[:uniq] ? uniq(records) : records
end
def count_records
- load_collection.size
+ load_target.size
end
def insert_record(record)
+ return false unless record.save
if @options[:insert_sql]
@owner.connection.execute(interpolate_sql(@options[:insert_sql], record))
else
@@ -110,6 +112,7 @@ module ActiveRecord
"VALUES (#{@owner.quoted_id},#{record.quoted_id})"
@owner.connection.execute(sql)
end
+ true
end
def insert_record_with_join_attributes(record, join_attributes)
@@ -129,6 +132,16 @@ module ActiveRecord
@owner.connection.execute(sql)
end
end
- end
+
+ def construct_sql
+ interpolate_sql_options!(@options, :finder_sql, :delete_sql)
+ @finder_sql = @options[:finder_sql] ||
+ "SELECT t.*, j.* FROM #{@association_table_name} t, #{@join_table} j " +
+ "WHERE t.#{@association_class.primary_key} = j.#{@association_foreign_key} AND " +
+ "j.#{@association_class_primary_key_name} = #{@owner.quoted_id} " +
+ (@options[:conditions] ? " AND " + interpolate_sql(@options[:conditions]) : "") + " " +
+ "ORDER BY #{@order}"
+ 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 f2652f55cc..92f6f4c262 100644
--- a/activerecord/lib/active_record/associations/has_many_association.rb
+++ b/activerecord/lib/active_record/associations/has_many_association.rb
@@ -2,37 +2,17 @@ module ActiveRecord
module Associations
class HasManyAssociation < AssociationCollection #:nodoc:
def initialize(owner, association_name, association_class_name, association_class_primary_key_name, options)
- super(owner, association_name, association_class_name, association_class_primary_key_name, options)
+ super
@conditions = sanitize_sql(options[:conditions])
- if options[:finder_sql]
- @finder_sql = interpolate_sql(options[:finder_sql])
- else
- @finder_sql = "#{@association_class_primary_key_name} = #{@owner.quoted_id}"
- @finder_sql << " AND #{interpolate_sql(@conditions)}" if @conditions
- end
-
- if options[:counter_sql]
- @counter_sql = interpolate_sql(options[:counter_sql])
- elsif options[:finder_sql]
- options[:counter_sql] = options[:finder_sql].gsub(/SELECT (.*) FROM/i, "SELECT COUNT(*) FROM")
- @counter_sql = interpolate_sql(options[:counter_sql])
- else
- @counter_sql = "#{@association_class_primary_key_name} = #{@owner.quoted_id}#{@conditions ? " AND " + interpolate_sql(@conditions) : ""}"
- end
- end
-
- def create(attributes = {})
- # Can't use Base.create since the foreign key may be a protected attribute.
- record = build(attributes)
- record.save
- @collection << record if loaded?
- record
+ construct_sql
end
def build(attributes = {})
+ load_target
record = @association_class.new(attributes)
- record[@association_class_primary_key_name] = @owner.id
+ record[@association_class_primary_key_name] = @owner.id unless @owner.new_record?
+ @target << record
record
end
@@ -77,10 +57,10 @@ module ActiveRecord
elsif @options[:finder_sql]
if ids.size == 1
id = ids.first
- record = load_collection.detect { |record| id == record.id }
+ record = load_target.detect { |record| id == record.id }
expects_array? ? [record] : record
else
- load_collection.select { |record| ids.include?(record.id) }
+ load_target.select { |record| ids.include?(record.id) }
end
# Otherwise, delegate to association class with conditions.
@@ -94,12 +74,12 @@ module ActiveRecord
# method calls may be chained.
def clear
@association_class.update_all("#{@association_class_primary_key_name} = NULL", "#{@association_class_primary_key_name} = #{@owner.quoted_id}")
- @collection = []
+ @target = []
self
end
protected
- def find_all_records
+ def find_target
find_all
end
@@ -122,7 +102,8 @@ module ActiveRecord
end
def insert_record(record)
- record.update_attribute(@association_class_primary_key_name, @owner.id)
+ record[@association_class_primary_key_name] = @owner.id
+ record.save
end
def delete_records(records)
@@ -132,6 +113,29 @@ module ActiveRecord
"#{@association_class_primary_key_name} = #{@owner.quoted_id} AND #{@association_class.primary_key} IN (#{ids})"
)
end
+
+ def target_obsolete?
+ false
+ end
+
+ def construct_sql
+ if @options[:finder_sql]
+ @finder_sql = interpolate_sql(@options[:finder_sql])
+ else
+ @finder_sql = "#{@association_class_primary_key_name} = #{@owner.quoted_id}"
+ @finder_sql << " AND #{interpolate_sql(@conditions)}" if @conditions
+ end
+
+ if @options[:counter_sql]
+ @counter_sql = interpolate_sql(@options[:counter_sql])
+ elsif @options[:finder_sql]
+ @options[:counter_sql] = @options[:finder_sql].gsub(/SELECT (.*) FROM/i, "SELECT COUNT(*) FROM")
+ @counter_sql = interpolate_sql(@options[:counter_sql])
+ else
+ @counter_sql = "#{@association_class_primary_key_name} = #{@owner.quoted_id}"
+ @counter_sql << " AND #{interpolate_sql(@conditions)}" if @conditions
+ end
+ end
end
end
end
diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb
new file mode 100644
index 0000000000..74e82f146a
--- /dev/null
+++ b/activerecord/lib/active_record/associations/has_one_association.rb
@@ -0,0 +1,48 @@
+module ActiveRecord
+ module Associations
+ class HasOneAssociation < BelongsToAssociation #:nodoc:
+ def initialize(owner, association_name, association_class_name, association_class_primary_key_name, options)
+ super
+
+ construct_sql
+ end
+
+ def replace(obj, dont_save = false)
+ load_target
+ unless @target.nil?
+ @target[@association_class_primary_key_name] = nil
+ @target.save unless @owner.new_record?
+ end
+
+ if obj.nil?
+ @target = nil
+ else
+ raise_on_type_mismatch(obj)
+
+ obj[@association_class_primary_key_name] = @owner.id unless @owner.new_record?
+ @target = obj
+ end
+
+ @loaded = true
+ unless @owner.new_record? or obj.nil? or dont_save
+ return (obj.save ? obj : false)
+ else
+ return obj
+ end
+ end
+
+ private
+ def find_target
+ @association_class.find_first(@finder_sql, @options[:order])
+ end
+
+ def target_obsolete?
+ false
+ end
+
+ def construct_sql
+ @finder_sql = "#{@association_class_primary_key_name} = #{@owner.quoted_id}#{@options[:conditions] ? " AND " + @options[:conditions] : ""}"
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb
index 8b022ef6d4..ae743b086a 100755
--- a/activerecord/lib/active_record/base.rb
+++ b/activerecord/lib/active_record/base.rb
@@ -789,7 +789,6 @@ module ActiveRecord #:nodoc:
# * A record does exist: Updates the record with values matching those of the object attributes.
def save
create_or_update
- return true
end
# Deletes the record in the database and freezes this instance to reflect that no changes should
@@ -928,9 +927,9 @@ module ActiveRecord #:nodoc:
self.class.columns_hash[name.to_s]
end
- # Returns true if the +comparison_object+ is of the same type and has the same id.
+ # Returns true if the +comparison_object+ is the same object, or is of the same type and has the same id.
def ==(comparison_object)
- comparison_object.instance_of?(self.class) && comparison_object.id == id
+ comparison_object.equal?(self) or (comparison_object.instance_of?(self.class) and comparison_object.id == id)
end
# Delegates to ==
@@ -956,6 +955,7 @@ module ActiveRecord #:nodoc:
private
def create_or_update
if new_record? then create else update end
+ return true
end
# Updates the associated record with values matching those of the instant attributes.
diff --git a/activerecord/lib/active_record/callbacks.rb b/activerecord/lib/active_record/callbacks.rb
index 715efe723f..b70d27f413 100755
--- a/activerecord/lib/active_record/callbacks.rb
+++ b/activerecord/lib/active_record/callbacks.rb
@@ -153,6 +153,12 @@ module ActiveRecord
# to implement a simple performance constraint (50% more speed on a simple test case). Unlike all the other callbacks, after_find and
# after_initialize can only be declared using an explicit implementation. So using the inheritable callback queue for after_find and
# after_initialize won't work.
+ #
+ # == Cancelling callbacks
+ #
+ # If a before_* callback returns false, all the later callbacks and the associated action are cancelled. If an after_* callback returns
+ # false, all the later callbacks are cancelled. Callbacks are generally run in the order they are defined, with the exception of callbacks
+ # defined as methods on the model, which are called last.
module Callbacks
CALLBACKS = %w(
after_find after_initialize before_save after_save before_create after_create before_update after_update before_validation
@@ -227,7 +233,7 @@ module ActiveRecord
# Is called _after_ Base.save (regardless of whether it's a create or update save).
def after_save() end
def create_or_update_with_callbacks #:nodoc:
- callback(:before_save)
+ return false if callback(:before_save) == false
result = create_or_update_without_callbacks
callback(:after_save)
result
@@ -239,7 +245,7 @@ module ActiveRecord
# Is called _after_ Base.save on new objects that haven't been saved yet (no record exists).
def after_create() end
def create_with_callbacks #:nodoc:
- callback(:before_create)
+ return false if callback(:before_create) == false
result = create_without_callbacks
callback(:after_create)
result
@@ -252,7 +258,7 @@ module ActiveRecord
def after_update() end
def update_with_callbacks #:nodoc:
- callback(:before_update)
+ return false if callback(:before_update) == false
result = update_without_callbacks
callback(:after_update)
result
@@ -281,8 +287,9 @@ module ActiveRecord
def after_validation_on_update() end
def valid_with_callbacks #:nodoc:
- callback(:before_validation)
- if new_record? then callback(:before_validation_on_create) else callback(:before_validation_on_update) end
+ return false if callback(:before_validation) == false
+ if new_record? then result = callback(:before_validation_on_create) else result = callback(:before_validation_on_update) end
+ return false if result == false
result = valid_without_callbacks
@@ -298,7 +305,7 @@ module ActiveRecord
# Is called _after_ Base.destroy (and all the attributes have been frozen).
def after_destroy() end
def destroy_with_callbacks #:nodoc:
- callback(:before_destroy)
+ return false if callback(:before_destroy) == false
result = destroy_without_callbacks
callback(:after_destroy)
result
@@ -307,7 +314,7 @@ module ActiveRecord
private
def callback(method)
callbacks_for(method).each do |callback|
- case callback
+ result = case callback
when Symbol
self.send(callback)
when String
@@ -321,9 +328,11 @@ module ActiveRecord
raise ActiveRecordError, "Callbacks must be a symbol denoting the method to call, a string to be evaluated, a block to be invoked, or an object responding to the callback method."
end
end
+ return false if result == false
end
invoke_and_notify(method)
+ true
end
def callbacks_for(method)
@@ -340,4 +349,4 @@ module ActiveRecord
self.class.notify_observers(method, self)
end
end
-end \ No newline at end of file
+end
diff --git a/activerecord/lib/active_record/deprecated_associations.rb b/activerecord/lib/active_record/deprecated_associations.rb
index 481b66bf0a..7a595eb216 100644
--- a/activerecord/lib/active_record/deprecated_associations.rb
+++ b/activerecord/lib/active_record/deprecated_associations.rb
@@ -18,7 +18,7 @@ module ActiveRecord
end_eval
end
- def deprecated_remove_association_relation(association_name)# :nodoc:
+ def deprecated_remove_association_relation(association_name)# :nodoc:
module_eval <<-"end_eval", __FILE__, __LINE__
def remove_#{association_name}(*items)
#{association_name}.delete(items)
@@ -50,7 +50,7 @@ module ActiveRecord
end_eval
end
- def deprecated_create_method(collection_name)# :nodoc:
+ def deprecated_collection_create_method(collection_name)# :nodoc:
module_eval <<-"end_eval", __FILE__, __LINE__
def create_in_#{collection_name}(attributes = {})
#{collection_name}.create(attributes)
@@ -58,13 +58,51 @@ module ActiveRecord
end_eval
end
- def deprecated_build_method(collection_name)# :nodoc:
- module_eval <<-"end_eval", __FILE__, __LINE__
- def build_to_#{collection_name}(attributes = {})
- #{collection_name}.build(attributes)
+ def deprecated_collection_build_method(collection_name)# :nodoc:
+ module_eval <<-"end_eval", __FILE__, __LINE__
+ def build_to_#{collection_name}(attributes = {})
+ #{collection_name}.build(attributes)
+ end
+ end_eval
+ end
+
+ def deprecated_association_comparison_method(association_name, association_class_name)
+ module_eval <<-"end_eval", __FILE__, __LINE__
+ def #{association_name}?(comparison_object, force_reload = false)
+ if comparison_object.kind_of?(#{association_class_name})
+ #{association_name}(force_reload) == comparison_object
+ else
+ raise "Comparison object is a #{association_class_name}, should have been \#{comparison_object.class.name}"
end
- end_eval
- end
+ end
+ end_eval
+ end
+
+ def deprecated_has_association_method(association_name)
+ module_eval <<-"end_eval", __FILE__, __LINE__
+ def has_#{association_name}?(force_reload = false)
+ !#{association_name}(force_reload).nil?
+ end
+ end_eval
+ end
+
+ def deprecated_build_method(method_prefix, collection_name, collection_class_name, class_primary_key_name)
+ module_eval <<-"end_eval", __FILE__, __LINE__
+ def #{method_prefix + collection_name}(attributes = {})
+ association = #{collection_class_name}.new
+ association.attributes = attributes.merge({ "#{class_primary_key_name}" => id})
+ association
+ end
+ end_eval
+ end
+
+ def deprecated_create_method(method_prefix, collection_name, collection_class_name, class_primary_key_name)
+ module_eval <<-"end_eval", __FILE__, __LINE__
+ def #{method_prefix + collection_name}(attributes = nil)
+ #{collection_class_name}.create((attributes || {}).merge({ "#{class_primary_key_name}" => id}))
+ end
+ end_eval
+ end
end
end
end