aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--activerecord/CHANGELOG33
-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
-rwxr-xr-xactiverecord/test/associations_test.rb232
-rw-r--r--activerecord/test/callbacks_test.rb16
-rwxr-xr-xactiverecord/test/deprecated_associations_test.rb38
-rwxr-xr-xactiverecord/test/fixtures/company.rb2
15 files changed, 800 insertions, 307 deletions
diff --git a/activerecord/CHANGELOG b/activerecord/CHANGELOG
index 16fb14b9a5..d43e6a0537 100644
--- a/activerecord/CHANGELOG
+++ b/activerecord/CHANGELOG
@@ -1,5 +1,38 @@
*SVN*
+* Added support for associating unsaved objects #402 [Tim Bates]. Rules that govern this addition:
+
+ == 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.
+
+* Added replace to associations, so you can do project.manager.replace(new_manager) or project.milestones.replace(new_milestones) #402 [Tim Bates]
+
+* Added build and create methods to has_one and belongs_to associations, so you can now do project.manager.build(attributes) #402 [Tim Bates]
+
+* Added that 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. #402 [Tim Bates]
+
+* Fixed that Base#== wouldn't work for multiple references to the same unsaved object #402 [Tim Bates]
+
* Fixed binary support for PostgreSQL #444 [alex@byzantine.no]
* Added a differenciation between AssociationCollection#size and -length. Now AssociationCollection#size returns the size of the
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
diff --git a/activerecord/test/associations_test.rb b/activerecord/test/associations_test.rb
index 26640d0048..0001eebe9f 100755
--- a/activerecord/test/associations_test.rb
+++ b/activerecord/test/associations_test.rb
@@ -24,14 +24,14 @@ class AssociationsTest < Test::Unit::TestCase
end
def test_force_reload
- firm = Firm.new
+ firm = Firm.new("name" => "A New Firm, Inc")
firm.save
firm.clients.each {|c|} # forcing to load all clients
assert firm.clients.empty?, "New firm shouldn't have client objects"
assert !firm.has_clients?, "New firm shouldn't have clients"
assert_equal 0, firm.clients.size, "New firm should have 0 clients"
- client = Client.new("firm_id" => firm.id)
+ client = Client.new("name" => "TheClient.com", "firm_id" => firm.id)
client.save
assert firm.clients.empty?, "New firm should have cached no client objects"
@@ -72,12 +72,6 @@ class HasOneAssociationsTest < Test::Unit::TestCase
def test_has_one
assert_equal @signals37.account, Account.find(1)
assert_equal Account.find(1).credit_limit, @signals37.account.credit_limit
- assert @signals37.has_account?, "37signals should have an account"
- assert Account.find(1).firm?(@signals37), "37signals account should be able to backtrack"
- assert Account.find(1).has_firm?, "37signals account should be able to backtrack"
-
- assert !Account.find(2).has_firm?, "Unknown isn't linked"
- assert !Account.find(2).firm?(@signals37), "Unknown isn't linked"
end
def test_type_mismatch
@@ -100,43 +94,105 @@ class HasOneAssociationsTest < Test::Unit::TestCase
assert_nil Account.find(old_account_id).firm_id
end
+ def test_dependence
+ firm = Firm.find(1)
+ assert !firm.account.nil?
+ firm.destroy
+ assert_equal 1, Account.find_all.length
+ end
+
def test_build
firm = Firm.new("name" => "GlobalMegaCorp")
firm.save
-
- account = firm.build_account("credit_limit" => 1000)
+
+ account = firm.account.build("credit_limit" => 1000)
+ assert_equal account, firm.account
assert account.save
assert_equal account, firm.account
end
+ def test_build_before_child_saved
+ firm = Firm.find(1)
+
+ account = firm.account.build("credit_limit" => 1000)
+ assert_equal account, firm.account
+ assert account.new_record?
+ assert firm.save
+ assert_equal account, firm.account
+ assert !account.new_record?
+ end
+
+ def test_build_before_either_saved
+ firm = Firm.new("name" => "GlobalMegaCorp")
+
+ account = firm.account.build("credit_limit" => 1000)
+ assert_equal account, firm.account
+ assert account.new_record?
+ assert firm.save
+ assert_equal account, firm.account
+ assert !account.new_record?
+ end
+
def test_failing_build_association
firm = Firm.new("name" => "GlobalMegaCorp")
firm.save
- account = firm.build_account
+ account = firm.account.build
+ assert_equal account, firm.account
assert !account.save
+ assert_equal account, firm.account
assert_equal "can't be empty", account.errors.on("credit_limit")
end
def test_create
firm = Firm.new("name" => "GlobalMegaCorp")
firm.save
- assert_equal firm.create_account("credit_limit" => 1000), firm.account
+ assert_equal firm.account.create("credit_limit" => 1000), firm.account
end
-
- def test_dependence
- firm = Firm.find(1)
- assert !firm.account.nil?
- firm.destroy
- assert_equal 1, Account.find_all.length
+
+ def test_create_before_save
+ firm = Firm.new("name" => "GlobalMegaCorp")
+ assert_equal firm.account.create("credit_limit" => 1000), firm.account
end
def test_dependence_with_missing_association
Account.destroy_all
- firm = Firm.find(1)
- assert !firm.has_account?
+ firm = Firm.find(1)
+ assert firm.account.nil?
firm.destroy
end
+
+ def test_assignment_before_parent_saved
+ firm = Firm.new("name" => "GlobalMegaCorp")
+ firm.account = a = Account.find(1)
+ assert firm.new_record?
+ assert_equal a, firm.account
+ assert firm.save
+ assert_equal a, firm.account
+ assert_equal a, firm.account(true)
+ end
+
+ def test_assignment_before_child_saved
+ firm = Firm.find(1)
+ firm.account = a = Account.new("credit_limit" => 1000)
+ assert !a.new_record?
+ assert_equal a, firm.account
+ assert_equal a, firm.account
+ assert_equal a, firm.account(true)
+ end
+
+ def test_assignment_before_either_saved
+ firm = Firm.new("name" => "GlobalMegaCorp")
+ firm.account = a = Account.new("credit_limit" => 1000)
+ assert firm.new_record?
+ assert a.new_record?
+ assert_equal a, firm.account
+ assert firm.save
+ assert !firm.new_record?
+ assert !a.new_record?
+ assert_equal a, firm.account
+ assert_equal a, firm.account(true)
+ end
end
@@ -257,16 +313,72 @@ class HasManyAssociationsTest < Test::Unit::TestCase
assert_equal 3, @signals37.clients_of_firm(true).size
end
+ def test_adding_before_save
+ no_of_firms = Firm.count
+ no_of_clients = Client.count
+ new_firm = Firm.new("name" => "A New Firm, Inc")
+ new_firm.clients_of_firm.push Client.new("name" => "Natural Company")
+ new_firm.clients_of_firm << (c = Client.new("name" => "Apple"))
+ assert new_firm.new_record?
+ assert c.new_record?
+ assert_equal 2, new_firm.clients_of_firm.size
+ assert_equal no_of_firms, Firm.count # Firm was not saved to database.
+ assert_equal no_of_clients, Client.count # Clients were not saved to database.
+ assert new_firm.save
+ assert !new_firm.new_record?
+ assert !c.new_record?
+ assert_equal new_firm, c.firm
+ assert_equal no_of_firms+1, Firm.count # Firm was saved to database.
+ assert_equal no_of_clients+2, Client.count # Clients were saved to database.
+ assert_equal 2, new_firm.clients_of_firm.size
+ assert_equal 2, new_firm.clients_of_firm(true).size
+ end
+
+ def test_invalid_adding
+ firm = Firm.find(1)
+ assert !(firm.clients_of_firm << c = Client.new)
+ assert c.new_record?
+ assert !firm.save
+ assert c.new_record?
+ end
+
+ def test_invalid_adding_before_save
+ no_of_firms = Firm.count
+ no_of_clients = Client.count
+ new_firm = Firm.new("name" => "A New Firm, Inc")
+ new_firm.clients_of_firm.concat([c = Client.new, Client.new("name" => "Apple")])
+ assert c.new_record?
+ assert !c.valid?
+ assert new_firm.valid?
+ assert !new_firm.save
+ assert c.new_record?
+ assert new_firm.new_record?
+ end
+
def test_build
new_client = @signals37.clients_of_firm.build("name" => "Another Client")
assert_equal "Another Client", new_client.name
- assert new_client.save
+ assert new_client.new_record?
+ assert_equal new_client, @signals37.clients_of_firm.last
+ assert @signals37.save
+ assert !new_client.new_record?
assert_equal 2, @signals37.clients_of_firm(true).size
end
+
+ def test_invalid_build
+ new_client = @signals37.clients_of_firm.build
+ assert new_client.new_record?
+ assert !new_client.valid?
+ assert_equal new_client, @signals37.clients_of_firm.last
+ assert !@signals37.save
+ assert new_client.new_record?
+ assert_equal 1, @signals37.clients_of_firm(true).size
+ end
def test_create
force_signal37_to_load_all_clients_of_firm
new_client = @signals37.clients_of_firm.create("name" => "Another Client")
+ assert !new_client.new_record?
assert_equal new_client, @signals37.clients_of_firm.last
assert_equal new_client, @signals37.clients_of_firm(true).last
end
@@ -278,6 +390,14 @@ class HasManyAssociationsTest < Test::Unit::TestCase
assert_equal 0, @signals37.clients_of_firm(true).size
end
+ def test_deleting_before_save
+ new_firm = Firm.new("name" => "A New Firm, Inc.")
+ new_client = new_firm.clients_of_firm.build("name" => "Another Client")
+ assert_equal 1, new_firm.clients_of_firm.size
+ new_firm.clients_of_firm.delete(new_client)
+ assert_equal 0, new_firm.clients_of_firm.size
+ end
+
def test_deleting_a_collection
force_signal37_to_load_all_clients_of_firm
@signals37.clients_of_firm.create("name" => "Another Client")
@@ -419,6 +539,44 @@ class BelongsToAssociationsTest < Test::Unit::TestCase
assert_equal 0, Topic.find(debate.id).send(:read_attribute, "replies_count"), "First reply deleted"
end
+ def test_assignment_before_parent_saved
+ client = Client.find_first
+ apple = Firm.new("name" => "Apple")
+ client.firm = apple
+ assert_equal apple, client.firm
+ assert apple.new_record?
+ assert client.save
+ assert apple.save
+ assert !apple.new_record?
+ assert_equal apple, client.firm
+ assert_equal apple, client.firm(true)
+ end
+
+ def test_assignment_before_child_saved
+ final_cut = Client.new("name" => "Final Cut")
+ firm = Firm.find(1)
+ final_cut.firm = firm
+ assert final_cut.new_record?
+ assert final_cut.save
+ assert !final_cut.new_record?
+ assert !firm.new_record?
+ assert_equal firm, final_cut.firm
+ assert_equal firm, final_cut.firm(true)
+ end
+
+ def test_assignment_before_either_saved
+ final_cut = Client.new("name" => "Final Cut")
+ apple = Firm.new("name" => "Apple")
+ final_cut.firm = apple
+ assert final_cut.new_record?
+ assert apple.new_record?
+ assert final_cut.save
+ assert !final_cut.new_record?
+ assert !apple.new_record?
+ assert_equal apple, final_cut.firm
+ assert_equal apple, final_cut.firm(true)
+ end
+
def test_field_name_same_as_foreign_key
computer = Computer.find 1
assert_not_nil computer.developer, ":foreign key == attribute didn't lock up"
@@ -514,6 +672,38 @@ class HasAndBelongsToManyAssociationsTest < Test::Unit::TestCase
assert_equal 2, aridridel.projects.size
assert_equal 2, aridridel.projects(true).size
end
+
+ def test_habtm_adding_before_save
+ no_of_devels = Developer.count
+ no_of_projects = Project.count
+ aridridel = Developer.new("name" => "Aridridel")
+ aridridel.projects.concat([Project.find(1), p = Project.new("name" => "Projekt")])
+ assert aridridel.new_record?
+ assert p.new_record?
+ assert aridridel.save
+ assert !aridridel.new_record?
+ assert_equal no_of_devels+1, Developer.count
+ assert_equal no_of_projects+1, Project.count
+ assert_equal 2, aridridel.projects.size
+ assert_equal 2, aridridel.projects(true).size
+ end
+
+ def test_build
+ devel = Developer.find(1)
+ proj = devel.projects.build("name" => "Projekt")
+ assert_equal devel.projects.last, proj
+ assert proj.new_record?
+ devel.save
+ assert !proj.new_record?
+ assert_equal devel.projects.last, proj
+ end
+
+ def test_create
+ devel = Developer.find(1)
+ proj = devel.projects.create("name" => "Projekt")
+ assert_equal devel.projects.last, proj
+ assert !proj.new_record?
+ end
def test_uniq_after_the_fact
@developers["jamis"].find.projects << @projects["active_record"].find
diff --git a/activerecord/test/callbacks_test.rb b/activerecord/test/callbacks_test.rb
index 3992c0a6a0..b92cbe7798 100644
--- a/activerecord/test/callbacks_test.rb
+++ b/activerecord/test/callbacks_test.rb
@@ -227,4 +227,20 @@ class CallbacksTest < Test::Unit::TestCase
[ :after_initialize, :method ]
], david.history
end
+
+ def test_zzz_callback_returning_false # must be run last since we modify CallbackDeveloper
+ david = CallbackDeveloper.find(1)
+ CallbackDeveloper.before_validation proc { |model| model.history << [:before_validation, :returning_false]; return false }
+ CallbackDeveloper.before_validation proc { |model| model.history << [:before_validation, :should_never_get_here] }
+ david.save
+ assert_equal [
+ [ :after_find, :method ],
+ [ :after_initialize, :method ],
+ [ :before_validation, :string ],
+ [ :before_validation, :proc ],
+ [ :before_validation, :object ],
+ [ :before_validation, :block ],
+ [ :before_validation, :returning_false ]
+ ], david.history
+ end
end
diff --git a/activerecord/test/deprecated_associations_test.rb b/activerecord/test/deprecated_associations_test.rb
index ed1d3c4055..f746275215 100755
--- a/activerecord/test/deprecated_associations_test.rb
+++ b/activerecord/test/deprecated_associations_test.rb
@@ -138,14 +138,14 @@ class DeprecatedAssociationsTest < Test::Unit::TestCase
end
def test_force_reload
- firm = Firm.new
+ firm = Firm.new("name" => "A New Firm, Inc")
firm.save
firm.clients.each {|c|} # forcing to load all clients
assert firm.clients.empty?, "New firm shouldn't have client objects"
assert !firm.has_clients?, "New firm shouldn't have clients"
assert_equal 0, firm.clients_count, "New firm should have 0 clients"
- client = Client.new("firm_id" => firm.id)
+ client = Client.new("name" => "TheClient.com", "firm_id" => firm.id)
client.save
assert firm.clients.empty?, "New firm should have cached no client objects"
@@ -340,4 +340,38 @@ class DeprecatedAssociationsTest < Test::Unit::TestCase
assert_equal 2, Firm.find_first.find_all_in_clients("type = 'Client'").length
assert_equal 1, Firm.find_first.find_all_in_clients("name = 'Summit'").length
end
+
+ def test_has_one
+ assert @signals37.account?(Account.find(1))
+ assert @signals37.has_account?, "37signals should have an account"
+ assert Account.find(1).firm?(@signals37), "37signals account should be able to backtrack"
+ assert Account.find(1).has_firm?, "37signals account should be able to backtrack"
+
+ assert !Account.find(2).has_firm?, "Unknown isn't linked"
+ assert !Account.find(2).firm?(@signals37), "Unknown isn't linked"
+ end
+
+ def test_has_one_build
+ firm = Firm.new("name" => "GlobalMegaCorp")
+ assert firm.save
+
+ account = firm.build_account("credit_limit" => 1000)
+ assert account.save
+ assert_equal account, firm.account
+ end
+
+ def test_has_one_failing_build_association
+ firm = Firm.new("name" => "GlobalMegaCorp")
+ firm.save
+
+ account = firm.build_account
+ assert !account.save
+ assert_equal "can't be empty", account.errors.on("credit_limit")
+ end
+
+ def test_has_one_create
+ firm = Firm.new("name" => "GlobalMegaCorp")
+ firm.save
+ assert_equal firm.create_account("credit_limit" => 1000), firm.account
+ end
end
diff --git a/activerecord/test/fixtures/company.rb b/activerecord/test/fixtures/company.rb
index 7d9a6053d4..579590e909 100755
--- a/activerecord/test/fixtures/company.rb
+++ b/activerecord/test/fixtures/company.rb
@@ -1,5 +1,7 @@
class Company < ActiveRecord::Base
attr_protected :rating
+
+ validates_presence_of :name
end