aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/lib/active_record/associations.rb
diff options
context:
space:
mode:
authorJon Leighton <j@jonathanleighton.com>2011-03-02 21:24:56 +0000
committerJon Leighton <j@jonathanleighton.com>2011-03-04 09:30:27 +0000
commit735844db712c511dd8abf36a5279318fbc0ff9d0 (patch)
tree5fbd5d224ef85d8c878bf221db98b422c9345466 /activerecord/lib/active_record/associations.rb
parent9a98c766e045aebc2ef6d5b716936b73407f095d (diff)
parentb171b9e73dcc6a89b1da652da61c5127fe605b51 (diff)
downloadrails-735844db712c511dd8abf36a5279318fbc0ff9d0.tar.gz
rails-735844db712c511dd8abf36a5279318fbc0ff9d0.tar.bz2
rails-735844db712c511dd8abf36a5279318fbc0ff9d0.zip
Merge branch 'master' into nested_has_many_through
Conflicts: activerecord/CHANGELOG activerecord/lib/active_record/association_preload.rb activerecord/lib/active_record/associations.rb activerecord/lib/active_record/associations/class_methods/join_dependency.rb activerecord/lib/active_record/associations/class_methods/join_dependency/join_association.rb activerecord/lib/active_record/associations/has_many_association.rb activerecord/lib/active_record/associations/has_many_through_association.rb activerecord/lib/active_record/associations/has_one_association.rb activerecord/lib/active_record/associations/has_one_through_association.rb activerecord/lib/active_record/associations/through_association_scope.rb activerecord/lib/active_record/reflection.rb activerecord/test/cases/associations/has_many_through_associations_test.rb activerecord/test/cases/associations/has_one_through_associations_test.rb activerecord/test/cases/reflection_test.rb activerecord/test/cases/relations_test.rb activerecord/test/fixtures/memberships.yml activerecord/test/models/categorization.rb activerecord/test/models/category.rb activerecord/test/models/member.rb activerecord/test/models/reference.rb activerecord/test/models/tagging.rb
Diffstat (limited to 'activerecord/lib/active_record/associations.rb')
-rw-r--r--activerecord/lib/active_record/associations.rb761
1 files changed, 207 insertions, 554 deletions
diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb
index 9544fdcb39..ec5b41a3e7 100644
--- a/activerecord/lib/active_record/associations.rb
+++ b/activerecord/lib/active_record/associations.rb
@@ -5,7 +5,6 @@ require 'active_support/core_ext/object/blank'
require 'active_support/core_ext/string/conversions'
require 'active_support/core_ext/module/remove_method'
require 'active_support/core_ext/class/attribute'
-require 'active_record/associations/class_methods/join_dependency'
module ActiveRecord
class InverseOfAssociationNotFoundError < ActiveRecordError #:nodoc:
@@ -20,18 +19,30 @@ module ActiveRecord
end
end
- class HasManyThroughAssociationPolymorphicError < ActiveRecordError #:nodoc:
+ class HasManyThroughAssociationPolymorphicSourceError < ActiveRecordError #:nodoc:
def initialize(owner_class_name, reflection, source_reflection)
super("Cannot have a has_many :through association '#{owner_class_name}##{reflection.name}' on the polymorphic object '#{source_reflection.class_name}##{source_reflection.name}'.")
end
end
+ class HasManyThroughAssociationPolymorphicThroughError < ActiveRecordError #:nodoc:
+ def initialize(owner_class_name, reflection)
+ super("Cannot have a has_many :through association '#{owner_class_name}##{reflection.name}' which goes through the polymorphic association '#{owner_class_name}##{reflection.through_reflection.name}'.")
+ end
+ end
+
class HasManyThroughAssociationPointlessSourceTypeError < ActiveRecordError #:nodoc:
def initialize(owner_class_name, reflection, source_reflection)
super("Cannot have a has_many :through association '#{owner_class_name}##{reflection.name}' with a :source_type option if the '#{reflection.through_reflection.class_name}##{source_reflection.name}' is not polymorphic. Try removing :source_type on your association.")
end
end
+ class HasOneThroughCantAssociateThroughCollection < ActiveRecordError #:nodoc:
+ def initialize(owner_class_name, reflection, through_reflection)
+ super("Cannot have a has_one :through association '#{owner_class_name}##{reflection.name}' where the :through association '#{owner_class_name}##{through_reflection.name}' is a collection. Specify a has_one or belongs_to association in the :through option instead.")
+ end
+ end
+
class HasManyThroughSourceAssociationNotFoundError < ActiveRecordError #:nodoc:
def initialize(reflection)
through_reflection = reflection.through_reflection
@@ -93,8 +104,8 @@ module ActiveRecord
# (has_many, has_one) when there is at least 1 child associated instance.
# ex: if @project.tasks.size > 0, DeleteRestrictionError will be raised when trying to destroy @project
class DeleteRestrictionError < ActiveRecordError #:nodoc:
- def initialize(reflection)
- super("Cannot delete record because of dependent #{reflection.name}")
+ def initialize(name)
+ super("Cannot delete record because of dependent #{name}")
end
end
@@ -104,37 +115,65 @@ module ActiveRecord
# These classes will be loaded when associations are created.
# So there is no need to eager load them.
- autoload :AssociationCollection, 'active_record/associations/association_collection'
- autoload :AssociationProxy, 'active_record/associations/association_proxy'
- autoload :BelongsToAssociation, 'active_record/associations/belongs_to_association'
+ autoload :Association, 'active_record/associations/association'
+ autoload :SingularAssociation, 'active_record/associations/singular_association'
+ autoload :CollectionAssociation, 'active_record/associations/collection_association'
+ autoload :CollectionProxy, 'active_record/associations/collection_proxy'
+
+ autoload :BelongsToAssociation, 'active_record/associations/belongs_to_association'
autoload :BelongsToPolymorphicAssociation, 'active_record/associations/belongs_to_polymorphic_association'
- autoload :HasAndBelongsToManyAssociation, 'active_record/associations/has_and_belongs_to_many_association'
- autoload :HasManyAssociation, 'active_record/associations/has_many_association'
- autoload :HasManyThroughAssociation, 'active_record/associations/has_many_through_association'
- autoload :HasOneAssociation, 'active_record/associations/has_one_association'
- autoload :HasOneThroughAssociation, 'active_record/associations/has_one_through_association'
- autoload :AliasTracker, 'active_record/associations/alias_tracker'
+ autoload :HasAndBelongsToManyAssociation, 'active_record/associations/has_and_belongs_to_many_association'
+ autoload :HasManyAssociation, 'active_record/associations/has_many_association'
+ autoload :HasManyThroughAssociation, 'active_record/associations/has_many_through_association'
+ autoload :HasOneAssociation, 'active_record/associations/has_one_association'
+ autoload :HasOneThroughAssociation, 'active_record/associations/has_one_through_association'
+ autoload :ThroughAssociation, 'active_record/associations/through_association'
+
+ module Builder #:nodoc:
+ autoload :Association, 'active_record/associations/builder/association'
+ autoload :SingularAssociation, 'active_record/associations/builder/singular_association'
+ autoload :CollectionAssociation, 'active_record/associations/builder/collection_association'
+
+ autoload :BelongsTo, 'active_record/associations/builder/belongs_to'
+ autoload :HasOne, 'active_record/associations/builder/has_one'
+ autoload :HasMany, 'active_record/associations/builder/has_many'
+ autoload :HasAndBelongsToMany, 'active_record/associations/builder/has_and_belongs_to_many'
+ end
+
+ autoload :Preloader, 'active_record/associations/preloader'
+ autoload :JoinDependency, 'active_record/associations/join_dependency'
+ autoload :AliasTracker, 'active_record/associations/alias_tracker'
# Clears out the association cache.
def clear_association_cache #:nodoc:
- self.class.reflect_on_all_associations.to_a.each do |assoc|
- instance_variable_set "@#{assoc.name}", nil
- end if self.persisted?
+ @association_cache.clear if persisted?
+ end
+
+ # :nodoc:
+ attr_reader :association_cache
+
+ # Returns the association instance for the given name, instantiating it if it doesn't already exist
+ def association(name) #:nodoc:
+ association = association_instance_get(name)
+
+ if association.nil?
+ reflection = self.class.reflect_on_association(name)
+ association = reflection.association_class.new(self, reflection)
+ association_instance_set(name, association)
+ end
+
+ association
end
private
# Returns the specified association instance if it responds to :loaded?, nil otherwise.
def association_instance_get(name)
- ivar = "@#{name}"
- if instance_variable_defined?(ivar)
- association = instance_variable_get(ivar)
- association if association.respond_to?(:loaded?)
- end
+ @association_cache[name.to_sym]
end
# Set the specified association instance.
def association_instance_set(name, association)
- instance_variable_set("@#{name}", association)
+ @association_cache[name] = association
end
# Associations are a set of macro-like class methods for tying objects together through
@@ -178,7 +217,7 @@ module ActiveRecord
# other=(other) | X | X | X
# build_other(attributes={}) | X | | X
# create_other(attributes={}) | X | | X
- # other.create!(attributes={}) | | | X
+ # create_other!(attributes={}) | X | | X
#
# ===Collection associations (one-to-many / many-to-many)
# | | | has_many
@@ -201,10 +240,9 @@ module ActiveRecord
# others.empty? | X | X | X
# others.clear | X | X | X
# others.delete(other,other,...) | X | X | X
- # others.delete_all | X | X |
+ # others.delete_all | X | X | X
# others.destroy_all | X | X | X
# others.find(*args) | X | X | X
- # others.find_first | X | |
# others.exists? | X | X | X
# others.uniq | X | X | X
# others.reset | X | X | X
@@ -318,26 +356,31 @@ module ActiveRecord
# === One-to-one associations
#
# * Assigning an object to a +has_one+ association automatically saves that object and
- # the object being replaced (if there is one), in order to update their primary
- # keys - except if the parent object is unsaved (<tt>new_record? == true</tt>).
- # * If either of these saves fail (due to one of the objects being invalid) the assignment
- # statement returns +false+ and the assignment is cancelled.
+ # the object being replaced (if there is one), in order to update their foreign
+ # keys - except if the parent object is unsaved (<tt>new_record? == true</tt>).
+ # * If either of these saves fail (due to one of the objects being invalid), an
+ # <tt>ActiveRecord::RecordNotSaved</tt> exception is raised and the assignment is
+ # cancelled.
# * If you wish to assign an object to a +has_one+ association without saving it,
- # use the <tt>association.build</tt> method (documented below).
+ # use the <tt>build_association</tt> method (documented below). The object being
+ # replaced will still be saved to update its foreign key.
# * Assigning an object to a +belongs_to+ association does not save the object, since
- # the foreign key field belongs on the parent. It does not save the parent either.
+ # the foreign key field belongs on the parent. It does not save the parent either.
#
# === Collections
#
# * Adding an object to a collection (+has_many+ or +has_and_belongs_to_many+) automatically
- # saves that object, except if the parent object (the owner of the collection) is not yet
- # stored in the database.
+ # saves that object, except if the parent object (the owner of the collection) is not yet
+ # stored in the database.
# * If saving any of the objects being added to a collection (via <tt>push</tt> or similar)
- # fails, then <tt>push</tt> returns +false+.
+ # fails, then <tt>push</tt> returns +false+.
+ # * If saving fails while replacing the collection (via <tt>association=</tt>), an
+ # <tt>ActiveRecord::RecordNotSaved</tt> exception is raised and the assignment is
+ # cancelled.
# * You can add an object to a collection without automatically saving it by using the
- # <tt>collection.build</tt> method (documented below).
+ # <tt>collection.build</tt> method (documented below).
# * All unsaved (<tt>new_record? == true</tt>) members of the collection are automatically
- # saved when the parent is saved.
+ # saved when the parent is saved.
#
# === Association callbacks
#
@@ -488,6 +531,22 @@ module ActiveRecord
# @group.avatars << Avatar.new # this would work if User belonged_to Avatar rather than the other way around
# @group.avatars.delete(@group.avatars.last) # so would this
#
+ # If you are using a +belongs_to+ on the join model, it is a good idea to set the
+ # <tt>:inverse_of</tt> option on the +belongs_to+, which will mean that the following example
+ # works correctly (where <tt>tags</tt> is a +has_many+ <tt>:through</tt> association):
+ #
+ # @post = Post.first
+ # @tag = @post.tags.build :name => "ruby"
+ # @tag.save
+ #
+ # The last line ought to save the through record (a <tt>Taggable</tt>). This will only work if the
+ # <tt>:inverse_of</tt> is set:
+ #
+ # class Taggable < ActiveRecord::Base
+ # belongs_to :post
+ # belongs_to :tag, :inverse_of => :taggings
+ # end
+ #
# === Nested Associations
#
# You can actually specify *any* association with the <tt>:through</tt> option, including an
@@ -800,7 +859,7 @@ module ActiveRecord
# belongs_to :dungeon
# end
#
- # The +traps+ association on +Dungeon+ and the the +dungeon+ association on +Trap+ are
+ # The +traps+ association on +Dungeon+ and the +dungeon+ association on +Trap+ are
# the inverse of each other and the inverse of the +dungeon+ association on +EvilWizard+
# is the +evil_wizard+ association on +Dungeon+ (and vice-versa). By default,
# Active Record doesn't know anything about these inverse relationships and so no object
@@ -840,6 +899,73 @@ module ActiveRecord
# * does not work with <tt>:polymorphic</tt> associations.
# * for +belongs_to+ associations +has_many+ inverse associations are ignored.
#
+ # == Deleting from associations
+ #
+ # === Dependent associations
+ #
+ # +has_many+, +has_one+ and +belongs_to+ associations support the <tt>:dependent</tt> option.
+ # This allows you to specify that associated records should be deleted when the owner is
+ # deleted.
+ #
+ # For example:
+ #
+ # class Author
+ # has_many :posts, :dependent => :destroy
+ # end
+ # Author.find(1).destroy # => Will destroy all of the author's posts, too
+ #
+ # The <tt>:dependent</tt> option can have different values which specify how the deletion
+ # is done. For more information, see the documentation for this option on the different
+ # specific association types.
+ #
+ # === Delete or destroy?
+ #
+ # +has_many+ and +has_and_belongs_to_many+ associations have the methods <tt>destroy</tt>,
+ # <tt>delete</tt>, <tt>destroy_all</tt> and <tt>delete_all</tt>.
+ #
+ # For +has_and_belongs_to_many+, <tt>delete</tt> and <tt>destroy</tt> are the same: they
+ # cause the records in the join table to be removed.
+ #
+ # For +has_many+, <tt>destroy</tt> will always call the <tt>destroy</tt> method of the
+ # record(s) being removed so that callbacks are run. However <tt>delete</tt> will either
+ # do the deletion according to the strategy specified by the <tt>:dependent</tt> option, or
+ # if no <tt>:dependent</tt> option is given, then it will follow the default strategy.
+ # The default strategy is <tt>:nullify</tt> (set the foreign keys to <tt>nil</tt>), except for
+ # +has_many+ <tt>:through</tt>, where the default strategy is <tt>delete_all</tt> (delete
+ # the join records, without running their callbacks).
+ #
+ # There is also a <tt>clear</tt> method which is the same as <tt>delete_all</tt>, except that
+ # it returns the association rather than the records which have been deleted.
+ #
+ # === What gets deleted?
+ #
+ # There is a potential pitfall here: +has_and_belongs_to_many+ and +has_many+ <tt>:through</tt>
+ # associations have records in join tables, as well as the associated records. So when we
+ # call one of these deletion methods, what exactly should be deleted?
+ #
+ # The answer is that it is assumed that deletion on an association is about removing the
+ # <i>link</i> between the owner and the associated object(s), rather than necessarily the
+ # associated objects themselves. So with +has_and_belongs_to_many+ and +has_many+
+ # <tt>:through</tt>, the join records will be deleted, but the associated records won't.
+ #
+ # This makes sense if you think about it: if you were to call <tt>post.tags.delete(Tag.find_by_name('food'))</tt>
+ # you would want the 'food' tag to be unlinked from the post, rather than for the tag itself
+ # to be removed from the database.
+ #
+ # However, there are examples where this strategy doesn't make sense. For example, suppose
+ # a person has many projects, and each project has many tasks. If we deleted one of a person's
+ # tasks, we would probably not want the project to be deleted. In this scenario, the delete method
+ # won't actually work: it can only be used if the association on the join model is a
+ # +belongs_to+. In other situations you are expected to perform operations directly on
+ # either the associated records or the <tt>:through</tt> association.
+ #
+ # With a regular +has_many+ there is no distinction between the "associated records"
+ # and the "link", so there is only one choice for what gets deleted.
+ #
+ # With +has_and_belongs_to_many+ and +has_many+ <tt>:through</tt>, if you want to delete the
+ # associated records themselves, you can always do something along the lines of
+ # <tt>person.tasks.each(&:destroy)</tt>.
+ #
# == Type safety with <tt>ActiveRecord::AssociationTypeMismatch</tt>
#
# If you attempt to assign an object to an association that doesn't match the inferred
@@ -864,6 +990,10 @@ module ActiveRecord
# Removes one or more objects from the collection by setting their foreign keys to +NULL+.
# Objects will be in addition destroyed if they're associated with <tt>:dependent => :destroy</tt>,
# and deleted if they're associated with <tt>:dependent => :delete_all</tt>.
+ #
+ # If the <tt>:through</tt> option is used, then the join records are deleted (rather than
+ # nullified) by default, but you can specify <tt>:dependent => :destroy</tt> or
+ # <tt>:dependent => :nullify</tt> to override this.
# [collection=objects]
# Replaces the collections content by deleting and adding objects as appropriate. If the <tt>:through</tt>
# option is true callbacks in the join models are triggered except destroy callbacks, since deletion is
@@ -919,7 +1049,7 @@ module ActiveRecord
# * <tt>Firm#clients.create</tt> (similar to <tt>c = Client.new("firm_id" => id); c.save; c</tt>)
# The declaration can also include an options hash to specialize the behavior of the association.
#
- # === Supported options
+ # === Options
# [:class_name]
# Specify the class name of the association. Use it only if that name can't be inferred
# from the association name. So <tt>has_many :products</tt> will by default be linked
@@ -947,7 +1077,9 @@ module ActiveRecord
# objects' foreign keys are set to +NULL+ *without* calling their +save+ callbacks. If set to
# <tt>:restrict</tt> this object cannot be deleted if it has any associated object.
#
- # *Warning:* This option is ignored when used with <tt>:through</tt> option.
+ # If using with the <tt>:through</tt> option, the association on the join model must be
+ # a +belongs_to+, and the records which get deleted are the join records, rather than
+ # the associated records.
#
# [:finder_sql]
# Specify a complete SQL statement to fetch the association. This is a good way to go for complex
@@ -978,14 +1110,21 @@ module ActiveRecord
# [:as]
# Specifies a polymorphic interface (See <tt>belongs_to</tt>).
# [:through]
- # Specifies a join model through which to perform the query. Options for <tt>:class_name</tt>,
+ # Specifies an association through which to perform the query. This can be any other type
+ # of association, including other <tt>:through</tt> associations. Options for <tt>:class_name</tt>,
# <tt>:primary_key</tt> and <tt>:foreign_key</tt> are ignored, as the association uses the
- # source reflection. You can use a <tt>:through</tt> association through any other,
- # association, but if other <tt>:through</tt> associations are involved then the resulting
- # association will be read-only. Otherwise, the collection of join models
- # can be managed via the collection API. For example, new join models are created for
- # newly associated objects, and if some are gone their rows are deleted (directly,
- # no destroy callbacks are triggered).
+ # source reflection.
+ #
+ # If the association on the join model is a +belongs_to+, the collection can be modified
+ # and the records on the <tt>:through</tt> model will be automatically created and removed
+ # as appropriate. Otherwise, the collection is read-only, so you should manipulate the
+ # <tt>:through</tt> association directly.
+ #
+ # If you are going to modify the association (rather than just read from it), then it is
+ # a good idea to set the <tt>:inverse_of</tt> option on the source association on the
+ # join model. This allows associated records to be built which will automatically create
+ # the appropriate join model records when they are saved. (See the 'Association Join Models'
+ # section above.)
# [:source]
# Specifies the source association name used by <tt>has_many :through</tt> queries.
# Only use it if the name cannot be inferred from the association.
@@ -1024,16 +1163,8 @@ module ActiveRecord
# 'FROM people p, post_subscriptions ps ' +
# 'WHERE ps.post_id = #{id} AND ps.person_id = p.id ' +
# 'ORDER BY p.first_name'
- def has_many(association_id, options = {}, &extension)
- reflection = create_has_many_reflection(association_id, options, &extension)
- configure_dependency_for_has_many(reflection)
- add_association_callbacks(reflection.name, reflection.options)
-
- if options[:through]
- collection_accessor_methods(reflection, HasManyThroughAssociation)
- else
- collection_accessor_methods(reflection, HasManyAssociation)
- end
+ def has_many(name, options = {}, &extension)
+ Builder::HasMany.build(self, name, options, &extension)
end
# Specifies a one-to-one association with another class. This method should only be used
@@ -1051,12 +1182,14 @@ module ActiveRecord
# [build_association(attributes = {})]
# Returns a new object of the associated type that has been instantiated
# with +attributes+ and linked to this object through a foreign key, but has not
- # yet been saved. <b>Note:</b> This ONLY works if an association already exists.
- # It will NOT work if the association is +nil+.
+ # yet been saved.
# [create_association(attributes = {})]
# Returns a new object of the associated type that has been instantiated
# with +attributes+, linked to this object through a foreign key, and that
# has already been saved (if it passed the validation).
+ # [create_association!(attributes = {})]
+ # Does the same as <tt>create_association</tt>, but raises <tt>ActiveRecord::RecordInvalid</tt>
+ # if the record is invalid.
#
# (+association+ is replaced with the symbol passed as the first argument, so
# <tt>has_one :manager</tt> would add among others <tt>manager.nil?</tt>.)
@@ -1068,6 +1201,7 @@ module ActiveRecord
# * <tt>Account#beneficiary=(beneficiary)</tt> (similar to <tt>beneficiary.account_id = account.id; beneficiary.save</tt>)
# * <tt>Account#build_beneficiary</tt> (similar to <tt>Beneficiary.new("account_id" => id)</tt>)
# * <tt>Account#create_beneficiary</tt> (similar to <tt>b = Beneficiary.new("account_id" => id); b.save; b</tt>)
+ # * <tt>Account#create_beneficiary!</tt> (similar to <tt>b = Beneficiary.new("account_id" => id); b.save!; b</tt>)
#
# === Options
#
@@ -1142,17 +1276,8 @@ module ActiveRecord
# has_one :boss, :readonly => :true
# has_one :club, :through => :membership
# has_one :primary_address, :through => :addressables, :conditions => ["addressable.primary = ?", true], :source => :addressable
- def has_one(association_id, options = {})
- if options[:through]
- reflection = create_has_one_through_reflection(association_id, options)
- association_accessor_methods(reflection, ActiveRecord::Associations::HasOneThroughAssociation)
- else
- reflection = create_has_one_reflection(association_id, options)
- association_accessor_methods(reflection, HasOneAssociation)
- association_constructor_method(:build, reflection, HasOneAssociation)
- association_constructor_method(:create, reflection, HasOneAssociation)
- configure_dependency_for_has_one(reflection)
- end
+ def has_one(name, options = {})
+ Builder::HasOne.build(self, name, options)
end
# Specifies a one-to-one association with another class. This method should only be used
@@ -1174,6 +1299,9 @@ module ActiveRecord
# Returns a new object of the associated type that has been instantiated
# with +attributes+, linked to this object through a foreign key, and that
# has already been saved (if it passed the validation).
+ # [create_association!(attributes = {})]
+ # Does the same as <tt>create_association</tt>, but raises <tt>ActiveRecord::RecordInvalid</tt>
+ # if the record is invalid.
#
# (+association+ is replaced with the symbol passed as the first argument, so
# <tt>belongs_to :author</tt> would add among others <tt>author.nil?</tt>.)
@@ -1185,6 +1313,7 @@ module ActiveRecord
# * <tt>Post#author=(author)</tt> (similar to <tt>post.author_id = author.id</tt>)
# * <tt>Post#build_author</tt> (similar to <tt>post.author = Author.new</tt>)
# * <tt>Post#create_author</tt> (similar to <tt>post.author = Author.new; post.author.save; post.author</tt>)
+ # * <tt>Post#create_author!</tt> (similar to <tt>post.author = Author.new; post.author.save!; post.author</tt>)
# The declaration can also include an options hash to specialize the behavior of the association.
#
# === Options
@@ -1206,6 +1335,11 @@ module ActiveRecord
# association will use "person_id" as the default <tt>:foreign_key</tt>. Similarly,
# <tt>belongs_to :favorite_person, :class_name => "Person"</tt> will use a foreign key
# of "favorite_person_id".
+ # [:foreign_type]
+ # Specify the column used to store the associated object's type, if this is a polymorphic
+ # association. By default this is guessed to be the name of the association with a "_type"
+ # suffix. So a class that defines a <tt>belongs_to :taggable, :polymorphic => true</tt>
+ # association will use "taggable_type" as the default <tt>:foreign_type</tt>.
# [:primary_key]
# Specify the method that returns the primary key of associated object used for the association.
# By default this is id.
@@ -1261,23 +1395,8 @@ module ActiveRecord
# belongs_to :post, :counter_cache => true
# belongs_to :company, :touch => true
# belongs_to :company, :touch => :employees_last_updated_at
- def belongs_to(association_id, options = {})
- reflection = create_belongs_to_reflection(association_id, options)
-
- if reflection.options[:polymorphic]
- association_accessor_methods(reflection, BelongsToPolymorphicAssociation)
- association_foreign_type_setter_method(reflection)
- else
- association_accessor_methods(reflection, BelongsToAssociation)
- association_constructor_method(:build, reflection, BelongsToAssociation)
- association_constructor_method(:create, reflection, BelongsToAssociation)
- end
-
- association_foreign_key_setter_method(reflection)
- add_counter_cache_callbacks(reflection) if options[:counter_cache]
- add_touch_callbacks(reflection, options[:touch]) if options[:touch]
-
- configure_dependency_for_belongs_to(reflection)
+ def belongs_to(name, options = {})
+ Builder::BelongsTo.build(self, name, options)
end
# Specifies a many-to-many relationship with another class. This associates two classes via an
@@ -1308,12 +1427,6 @@ module ActiveRecord
# end
# end
#
- # Deprecated: Any additional fields added to the join table will be placed as attributes when
- # pulling records out through +has_and_belongs_to_many+ associations. Records returned from join
- # tables with additional attributes will be marked as readonly (because we can't save changes
- # to the additional attributes). It's strongly recommended that you upgrade any
- # associations with attributes to a real join model (see introduction).
- #
# Adds the following methods for retrieval and query:
#
# [collection(force_reload = false)]
@@ -1454,469 +1567,9 @@ module ActiveRecord
# has_and_belongs_to_many :categories, :readonly => true
# has_and_belongs_to_many :active_projects, :join_table => 'developers_projects', :delete_sql =>
# 'DELETE FROM developers_projects WHERE active=1 AND developer_id = #{id} AND project_id = #{record.id}'
- def has_and_belongs_to_many(association_id, options = {}, &extension)
- reflection = create_has_and_belongs_to_many_reflection(association_id, options, &extension)
- collection_accessor_methods(reflection, HasAndBelongsToManyAssociation)
-
- # Don't use a before_destroy callback since users' before_destroy
- # callbacks will be executed after the association is wiped out.
- include Module.new {
- class_eval <<-RUBY, __FILE__, __LINE__ + 1
- def destroy # def destroy
- #{reflection.name}.clear # posts.clear
- super # super
- end # end
- RUBY
- }
-
- add_association_callbacks(reflection.name, options)
+ def has_and_belongs_to_many(name, options = {}, &extension)
+ Builder::HasAndBelongsToMany.build(self, name, options, &extension)
end
-
- private
- # Generates a join table name from two provided table names.
- # The names in the join table names end up in lexicographic order.
- #
- # join_table_name("members", "clubs") # => "clubs_members"
- # join_table_name("members", "special_clubs") # => "members_special_clubs"
- def join_table_name(first_table_name, second_table_name)
- if first_table_name < second_table_name
- join_table = "#{first_table_name}_#{second_table_name}"
- else
- join_table = "#{second_table_name}_#{first_table_name}"
- end
-
- table_name_prefix + join_table + table_name_suffix
- end
-
- def association_accessor_methods(reflection, association_proxy_class)
- redefine_method(reflection.name) do |*params|
- force_reload = params.first unless params.empty?
- association = association_instance_get(reflection.name)
-
- if association.nil? || force_reload
- association = association_proxy_class.new(self, reflection)
- retval = force_reload ? reflection.klass.uncached { association.reload } : association.reload
- if retval.nil? and association_proxy_class == BelongsToAssociation
- association_instance_set(reflection.name, nil)
- return nil
- end
- association_instance_set(reflection.name, association)
- end
-
- association.target.nil? ? nil : association
- end
-
- redefine_method("loaded_#{reflection.name}?") do
- association = association_instance_get(reflection.name)
- association && association.loaded?
- end
-
- redefine_method("#{reflection.name}=") do |new_value|
- association = association_instance_get(reflection.name)
-
- if association.nil? || association.target != new_value
- association = association_proxy_class.new(self, reflection)
- end
-
- association.replace(new_value)
- association_instance_set(reflection.name, new_value.nil? ? nil : association)
- end
-
- redefine_method("set_#{reflection.name}_target") do |target|
- return if target.nil? and association_proxy_class == BelongsToAssociation
- association = association_proxy_class.new(self, reflection)
- association.target = target
- association_instance_set(reflection.name, association)
- end
- end
-
- def collection_reader_method(reflection, association_proxy_class)
- redefine_method(reflection.name) do |*params|
- force_reload = params.first unless params.empty?
- association = association_instance_get(reflection.name)
-
- unless association
- association = association_proxy_class.new(self, reflection)
- association_instance_set(reflection.name, association)
- end
-
- reflection.klass.uncached { association.reload } if force_reload
-
- association
- end
-
- redefine_method("#{reflection.name.to_s.singularize}_ids") do
- if send(reflection.name).loaded? || reflection.options[:finder_sql]
- send(reflection.name).map { |r| r.id }
- else
- if reflection.through_reflection && reflection.source_reflection.belongs_to?
- through = reflection.through_reflection
- primary_key = reflection.source_reflection.primary_key_name
- send(through.name).select("DISTINCT #{through.quoted_table_name}.#{primary_key}").map! { |r| r.send(primary_key) }
- else
- send(reflection.name).select("#{reflection.quoted_table_name}.#{reflection.klass.primary_key}").except(:includes).map! { |r| r.id }
- end
- end
- end
-
- end
-
- def collection_accessor_methods(reflection, association_proxy_class, writer = true)
- collection_reader_method(reflection, association_proxy_class)
-
- if writer
- redefine_method("#{reflection.name}=") do |new_value|
- # Loads proxy class instance (defined in collection_reader_method) if not already loaded
- association = send(reflection.name)
- association.replace(new_value)
- association
- end
-
- redefine_method("#{reflection.name.to_s.singularize}_ids=") do |new_value|
- pk_column = reflection.primary_key_column
- ids = (new_value || []).reject { |nid| nid.blank? }
- ids.map!{ |i| pk_column.type_cast(i) }
- send("#{reflection.name}=", reflection.klass.find(ids).index_by{ |r| r.id }.values_at(*ids))
- end
- end
- end
-
- def association_constructor_method(constructor, reflection, association_proxy_class)
- redefine_method("#{constructor}_#{reflection.name}") do |*params|
- attributees = params.first unless params.empty?
- replace_existing = params[1].nil? ? true : params[1]
- association = association_instance_get(reflection.name)
-
- unless association
- association = association_proxy_class.new(self, reflection)
- association_instance_set(reflection.name, association)
- end
-
- if association_proxy_class == HasOneAssociation
- association.send(constructor, attributees, replace_existing)
- else
- association.send(constructor, attributees)
- end
- end
- end
-
- def association_foreign_key_setter_method(reflection)
- setters = reflect_on_all_associations(:belongs_to).map do |belongs_to_reflection|
- if belongs_to_reflection.primary_key_name == reflection.primary_key_name
- "association_instance_set(:#{belongs_to_reflection.name}, nil);"
- end
- end.compact.join
-
- if method_defined?(:"#{reflection.primary_key_name}=")
- undef_method :"#{reflection.primary_key_name}="
- end
-
- class_eval <<-FILE, __FILE__, __LINE__ + 1
- def #{reflection.primary_key_name}=(new_id)
- write_attribute :#{reflection.primary_key_name}, new_id
- if #{reflection.primary_key_name}_changed?
- #{ setters }
- end
- end
- FILE
- end
-
- def association_foreign_type_setter_method(reflection)
- setters = reflect_on_all_associations(:belongs_to).map do |belongs_to_reflection|
- if belongs_to_reflection.options[:foreign_type] == reflection.options[:foreign_type]
- "association_instance_set(:#{belongs_to_reflection.name}, nil);"
- end
- end.compact.join
-
- field = reflection.options[:foreign_type]
- class_eval <<-FILE, __FILE__, __LINE__ + 1
- def #{field}=(new_id)
- write_attribute :#{field}, new_id
- if #{field}_changed?
- #{ setters }
- end
- end
- FILE
- end
-
- def add_counter_cache_callbacks(reflection)
- cache_column = reflection.counter_cache_column
-
- method_name = "belongs_to_counter_cache_after_create_for_#{reflection.name}".to_sym
- define_method(method_name) do
- association = send(reflection.name)
- association.class.increment_counter(cache_column, association.id) unless association.nil?
- end
- after_create(method_name)
-
- method_name = "belongs_to_counter_cache_before_destroy_for_#{reflection.name}".to_sym
- define_method(method_name) do
- association = send(reflection.name)
- association.class.decrement_counter(cache_column, association.id) unless association.nil?
- end
- before_destroy(method_name)
-
- module_eval(
- "#{reflection.class_name}.send(:attr_readonly,\"#{cache_column}\".intern) if defined?(#{reflection.class_name}) && #{reflection.class_name}.respond_to?(:attr_readonly)", __FILE__, __LINE__
- )
- end
-
- def add_touch_callbacks(reflection, touch_attribute)
- method_name = :"belongs_to_touch_after_save_or_destroy_for_#{reflection.name}"
- redefine_method(method_name) do
- association = send(reflection.name)
-
- if touch_attribute == true
- association.touch unless association.nil?
- else
- association.touch(touch_attribute) unless association.nil?
- end
- end
- after_save(method_name)
- after_touch(method_name)
- after_destroy(method_name)
- end
-
- # Creates before_destroy callback methods that nullify, delete or destroy
- # has_many associated objects, according to the defined :dependent rule.
- #
- # See HasManyAssociation#delete_records for more information. In general
- # - delete children if the option is set to :destroy or :delete_all
- # - set the foreign key to NULL if the option is set to :nullify
- # - do not delete the parent record if there is any child record if the
- # option is set to :restrict
- #
- # The +extra_conditions+ parameter, which is not used within the main
- # Active Record codebase, is meant to allow plugins to define extra
- # finder conditions.
- def configure_dependency_for_has_many(reflection, extra_conditions = nil)
- if reflection.options.include?(:dependent)
- case reflection.options[:dependent]
- when :destroy
- method_name = "has_many_dependent_destroy_for_#{reflection.name}".to_sym
- define_method(method_name) do
- send(reflection.name).each do |o|
- # No point in executing the counter update since we're going to destroy the parent anyway
- counter_method = ('belongs_to_counter_cache_before_destroy_for_' + self.class.name.downcase).to_sym
- if(o.respond_to? counter_method) then
- class << o
- self
- end.send(:define_method, counter_method, Proc.new {})
- end
- o.destroy
- end
- end
- before_destroy method_name
- when :delete_all
- before_destroy do |record|
- self.class.send(:delete_all_has_many_dependencies,
- record,
- reflection.name,
- reflection.klass,
- reflection.dependent_conditions(record, self.class, extra_conditions))
- end
- when :nullify
- before_destroy do |record|
- self.class.send(:nullify_has_many_dependencies,
- record,
- reflection.name,
- reflection.klass,
- reflection.primary_key_name,
- reflection.dependent_conditions(record, self.class, extra_conditions))
- end
- when :restrict
- method_name = "has_many_dependent_restrict_for_#{reflection.name}".to_sym
- define_method(method_name) do
- unless send(reflection.name).empty?
- raise DeleteRestrictionError.new(reflection)
- end
- end
- before_destroy method_name
- else
- raise ArgumentError, "The :dependent option expects either :destroy, :delete_all, :nullify or :restrict (#{reflection.options[:dependent].inspect})"
- end
- end
- end
-
- # Creates before_destroy callback methods that nullify, delete or destroy
- # has_one associated objects, according to the defined :dependent rule.
- # If the association is marked as :dependent => :restrict, create a callback
- # that prevents deleting entirely.
- def configure_dependency_for_has_one(reflection)
- if reflection.options.include?(:dependent)
- name = reflection.options[:dependent]
- method_name = :"has_one_dependent_#{name}_for_#{reflection.name}"
-
- case name
- when :destroy, :delete
- class_eval <<-eoruby, __FILE__, __LINE__ + 1
- def #{method_name}
- association = #{reflection.name}
- association.#{name} if association
- end
- eoruby
- when :nullify
- class_eval <<-eoruby, __FILE__, __LINE__ + 1
- def #{method_name}
- association = #{reflection.name}
- association.update_attribute(#{reflection.primary_key_name.inspect}, nil) if association
- end
- eoruby
- when :restrict
- method_name = "has_one_dependent_restrict_for_#{reflection.name}".to_sym
- define_method(method_name) do
- unless send(reflection.name).nil?
- raise DeleteRestrictionError.new(reflection)
- end
- end
- before_destroy method_name
- else
- raise ArgumentError, "The :dependent option expects either :destroy, :delete, :nullify or :restrict (#{reflection.options[:dependent].inspect})"
- end
-
- before_destroy method_name
- end
- end
-
- def configure_dependency_for_belongs_to(reflection)
- if reflection.options.include?(:dependent)
- name = reflection.options[:dependent]
-
- unless [:destroy, :delete].include?(name)
- raise ArgumentError, "The :dependent option expects either :destroy or :delete (#{reflection.options[:dependent].inspect})"
- end
-
- method_name = :"belongs_to_dependent_#{name}_for_#{reflection.name}"
- class_eval <<-eoruby, __FILE__, __LINE__ + 1
- def #{method_name}
- association = #{reflection.name}
- association.#{name} if association
- end
- eoruby
- after_destroy method_name
- end
- end
-
- def delete_all_has_many_dependencies(record, reflection_name, association_class, dependent_conditions)
- association_class.delete_all(dependent_conditions)
- end
-
- def nullify_has_many_dependencies(record, reflection_name, association_class, primary_key_name, dependent_conditions)
- association_class.update_all("#{primary_key_name} = NULL", dependent_conditions)
- end
-
- mattr_accessor :valid_keys_for_has_many_association
- @@valid_keys_for_has_many_association = [
- :class_name, :table_name, :foreign_key, :primary_key,
- :dependent,
- :select, :conditions, :include, :order, :group, :having, :limit, :offset,
- :as, :through, :source, :source_type,
- :uniq,
- :finder_sql, :counter_sql,
- :before_add, :after_add, :before_remove, :after_remove,
- :extend, :readonly,
- :validate, :inverse_of
- ]
-
- def create_has_many_reflection(association_id, options, &extension)
- options.assert_valid_keys(valid_keys_for_has_many_association)
- options[:extend] = create_extension_modules(association_id, extension, options[:extend])
-
- create_reflection(:has_many, association_id, options, self)
- end
-
- mattr_accessor :valid_keys_for_has_one_association
- @@valid_keys_for_has_one_association = [
- :class_name, :foreign_key, :remote, :select, :conditions, :order,
- :include, :dependent, :counter_cache, :extend, :as, :readonly,
- :validate, :primary_key, :inverse_of
- ]
-
- def create_has_one_reflection(association_id, options)
- options.assert_valid_keys(valid_keys_for_has_one_association)
- create_reflection(:has_one, association_id, options, self)
- end
-
- def create_has_one_through_reflection(association_id, options)
- options.assert_valid_keys(
- :class_name, :foreign_key, :remote, :select, :conditions, :order, :include, :dependent, :counter_cache, :extend, :as, :through, :source, :source_type, :validate
- )
- create_reflection(:has_one, association_id, options, self)
- end
-
- mattr_accessor :valid_keys_for_belongs_to_association
- @@valid_keys_for_belongs_to_association = [
- :class_name, :primary_key, :foreign_key, :foreign_type, :remote, :select, :conditions,
- :include, :dependent, :counter_cache, :extend, :polymorphic, :readonly,
- :validate, :touch, :inverse_of
- ]
-
- def create_belongs_to_reflection(association_id, options)
- options.assert_valid_keys(valid_keys_for_belongs_to_association)
- reflection = create_reflection(:belongs_to, association_id, options, self)
-
- if options[:polymorphic]
- reflection.options[:foreign_type] ||= reflection.class_name.underscore + "_type"
- end
-
- reflection
- end
-
- mattr_accessor :valid_keys_for_has_and_belongs_to_many_association
- @@valid_keys_for_has_and_belongs_to_many_association = [
- :class_name, :table_name, :join_table, :foreign_key, :association_foreign_key,
- :select, :conditions, :include, :order, :group, :having, :limit, :offset,
- :uniq,
- :finder_sql, :counter_sql, :delete_sql, :insert_sql,
- :before_add, :after_add, :before_remove, :after_remove,
- :extend, :readonly,
- :validate
- ]
-
- def create_has_and_belongs_to_many_reflection(association_id, options, &extension)
- options.assert_valid_keys(valid_keys_for_has_and_belongs_to_many_association)
- options[:extend] = create_extension_modules(association_id, extension, options[:extend])
-
- reflection = create_reflection(:has_and_belongs_to_many, association_id, options, self)
-
- if reflection.association_foreign_key == reflection.primary_key_name
- raise HasAndBelongsToManyAssociationForeignKeyNeeded.new(reflection)
- end
-
- reflection.options[:join_table] ||= join_table_name(undecorated_table_name(self.to_s), undecorated_table_name(reflection.class_name))
- if connection.supports_primary_key? && (connection.primary_key(reflection.options[:join_table]) rescue false)
- raise HasAndBelongsToManyAssociationWithPrimaryKeyError.new(reflection)
- end
-
- reflection
- end
-
- def add_association_callbacks(association_name, options)
- callbacks = %w(before_add after_add before_remove after_remove)
- callbacks.each do |callback_name|
- full_callback_name = "#{callback_name}_for_#{association_name}"
- defined_callbacks = options[callback_name.to_sym]
-
- full_callback_value = options.has_key?(callback_name.to_sym) ? [defined_callbacks].flatten : []
-
- # TODO : why do i need method_defined? I think its because of the inheritance chain
- class_attribute full_callback_name.to_sym unless method_defined?(full_callback_name)
- self.send("#{full_callback_name}=", full_callback_value)
- end
- end
-
- def create_extension_modules(association_id, block_extension, extensions)
- if block_extension
- extension_module_name = "#{self.to_s.demodulize}#{association_id.to_s.camelize}AssociationExtension"
-
- silence_warnings do
- self.parent.const_set(extension_module_name, Module.new(&block_extension))
- end
- Array.wrap(extensions).push("#{self.parent}::#{extension_module_name}".constantize)
- else
- Array.wrap(extensions)
- end
- end
end
end
end