require 'active_record/associations/association_proxy'
require 'active_record/associations/association_collection'
require 'active_record/associations/belongs_to_association'
require 'active_record/associations/belongs_to_polymorphic_association'
require 'active_record/associations/has_one_association'
require 'active_record/associations/has_many_association'
require 'active_record/associations/has_many_through_association'
require 'active_record/associations/has_and_belongs_to_many_association'
require 'active_record/deprecated_associations'
module ActiveRecord
module Associations # :nodoc:
def self.append_features(base)
super
base.extend(ClassMethods)
end
# 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 unless self.new_record?
end
# Associations are a set of macro-like class methods for tying objects together through foreign keys. They express relationships like
# "Project has one Project Manager" or "Project belongs to a Portfolio". Each macro adds a number of methods to the class which are
# specialized according to the collection or association symbol and the options hash. It works much the same way as Ruby's own attr*
# methods. Example:
#
# class Project < ActiveRecord::Base
# belongs_to :portfolio
# has_one :project_manager
# has_many :milestones
# has_and_belongs_to_many :categories
# end
#
# The project class now has the following methods (and more) to ease the traversal and manipulation of its relationships:
# * Project#portfolio, Project#portfolio=(portfolio), Project#portfolio.nil?
# * Project#project_manager, Project#project_manager=(project_manager), Project#project_manager.nil?,
# * Project#milestones.empty?, Project#milestones.size, Project#milestones, Project#milestones<<(milestone),
# Project#milestones.delete(milestone), Project#milestones.find(milestone_id), Project#milestones.find_all(conditions),
# Project#milestones.build, Project#milestones.create
# * Project#categories.empty?, Project#categories.size, Project#categories, Project#categories<<(category1),
# Project#categories.delete(category1)
#
# == Example
#
# link:files/examples/associations.png
#
# == Is it belongs_to or has_one?
#
# Both express a 1-1 relationship, the difference is mostly where to place the foreign key, which goes on the table for the class
# saying belongs_to. Example:
#
# class Post < ActiveRecord::Base
# has_one :author
# end
#
# class Author < ActiveRecord::Base
# belongs_to :post
# end
#
# The tables for these classes could look something like:
#
# CREATE TABLE posts (
# id int(11) NOT NULL auto_increment,
# title varchar default NULL,
# PRIMARY KEY (id)
# )
#
# CREATE TABLE authors (
# id int(11) NOT NULL auto_increment,
# post_id int(11) default NULL,
# name varchar default NULL,
# 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.
#
# === Association callbacks
#
# Similiar to the normal callbacks that hook into the lifecycle of an Active Record object, you can also define callbacks that get
# trigged when you add an object to or removing an object from a association collection. Example:
#
# class Project
# has_and_belongs_to_many :developers, :after_add => :evaluate_velocity
#
# def evaluate_velocity(developer)
# ...
# end
# end
#
# It's possible to stack callbacks by passing them as an array. Example:
#
# class Project
# has_and_belongs_to_many :developers, :after_add => [:evaluate_velocity, Proc.new { |p, d| p.shipping_date = Time.now}]
# end
#
# Possible callbacks are: before_add, after_add, before_remove and after_remove.
#
# Should any of the before_add callbacks throw an exception, the object does not get added to the collection. Same with
# the before_remove callbacks, if an exception is thrown the object doesn't get removed.
#
# === Association extensions
#
# The proxy objects that controls the access to associations can be extended through anonymous modules. This is especially
# beneficial for adding new finders, creators, and other factory-type methods that are only used as part of this association.
# Example:
#
# class Account < ActiveRecord::Base
# has_many :people do
# def find_or_create_by_name(name)
# first_name, last_name = name.split(" ", 2)
# find_or_create_by_first_name_and_last_name(first_name, last_name)
# end
# end
# end
#
# person = Account.find(:first).people.find_or_create_by_name("David Heinemeier Hansson")
# person.first_name # => "David"
# person.last_name # => "Heinemeier Hansson"
#
# If you need to share the same extensions between many associations, you can use a named extension module. Example:
#
# module FindOrCreateByNameExtension
# def find_or_create_by_name(name)
# first_name, last_name = name.split(" ", 2)
# find_or_create_by_first_name_and_last_name(first_name, last_name)
# end
# end
#
# class Account < ActiveRecord::Base
# has_many :people, :extend => FindOrCreateByNameExtension
# end
#
# class Company < ActiveRecord::Base
# has_many :people, :extend => FindOrCreateByNameExtension
# end
#
# == Caching
#
# All of the methods are built on a simple caching principle that will keep the result of the last query around unless specifically
# instructed not to. The cache is even shared across methods to make it even cheaper to use the macro-added methods without
# worrying too much about performance at the first go. Example:
#
# project.milestones # fetches milestones from the database
# project.milestones.size # uses the milestone cache
# project.milestones.empty? # uses the milestone cache
# project.milestones(true).size # fetches milestones from the database
# project.milestones # uses the milestone cache
#
# == Eager loading of associations
#
# Eager loading is a way to find objects of a certain class and a number of named associations along with it in a single SQL call. This is
# one of the easiest ways of to prevent the dreaded 1+N problem in which fetching 100 posts that each needs to display their author
# triggers 101 database queries. Through the use of eager loading, the 101 queries can be reduced to 1. Example:
#
# class Post < ActiveRecord::Base
# belongs_to :author
# has_many :comments
# end
#
# Consider the following loop using the class above:
#
# for post in Post.find(:all)
# puts "Post: " + post.title
# puts "Written by: " + post.author.name
# puts "Last comment on: " + post.comments.first.created_on
# end
#
# To iterate over these one hundred posts, we'll generate 201 database queries. Let's first just optimize it for retrieving the author:
#
# for post in Post.find(:all, :include => :author)
#
# This references the name of the belongs_to association that also used the :author symbol, so the find will now weave in a join something
# like this: LEFT OUTER JOIN authors ON authors.id = posts.author_id. Doing so will cut down the number of queries from 201 to 101.
#
# We can improve upon the situation further by referencing both associations in the finder with:
#
# for post in Post.find(:all, :include => [ :author, :comments ])
#
# That'll add another join along the lines of: LEFT OUTER JOIN comments ON comments.post_id = posts.id. And we'll be down to 1 query.
# But that shouldn't fool you to think that you can pull out huge amounts of data with no performance penalty just because you've reduced
# the number of queries. The database still needs to send all the data to Active Record and it still needs to be processed. So it's no
# catch-all for performance problems, but it's a great way to cut down on the number of queries in a situation as the one described above.
#
# Please note that limited eager loading with has_many and has_and_belongs_to_many associations is not compatible with describing conditions
# on these eager tables. This will work:
#
# Post.find(:all, :include => :comments, :conditions => "posts.title = 'magic forest'", :limit => 2)
#
# ...but this will not (and an ArgumentError will be raised):
#
# Post.find(:all, :include => :comments, :conditions => "comments.body like 'Normal%'", :limit => 2)
#
# Also have in mind that since the eager loading is pulling from multiple tables, you'll have to disambiguate any column references
# in both conditions and orders. So :order => "posts.id DESC" will work while :order => "id DESC" will not. This may require that
# you alter the :order and :conditions on the association definitions themselves.
#
# It's currently not possible to use eager loading on multiple associations from the same table. Eager loading will also not pull
# additional attributes on join tables, so "rich associations" with has_and_belongs_to_many is not a good fit for eager loading.
#
# == Modules
#
# By default, associations will look for objects within the current module scope. Consider:
#
# module MyApplication
# module Business
# class Firm < ActiveRecord::Base
# has_many :clients
# end
#
# class Company < ActiveRecord::Base; end
# end
# end
#
# When Firm#clients is called, it'll in turn call MyApplication::Business::Company.find(firm.id). If you want to associate
# with a class in another module scope this can be done by specifying the complete class name, such as:
#
# module MyApplication
# module Business
# class Firm < ActiveRecord::Base; end
# end
#
# module Billing
# class Account < ActiveRecord::Base
# belongs_to :firm, :class_name => "MyApplication::Business::Firm"
# end
# end
# end
#
# == Type safety with ActiveRecord::AssociationTypeMismatch
#
# If you attempt to assign an object to an association that doesn't match the inferred or specified :class_name, you'll
# get a ActiveRecord::AssociationTypeMismatch.
#
# == Options
#
# All of the association macros can be specialized through options which makes more complex cases than the simple and guessable ones
# possible.
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
# has_many :clients would add among others clients.empty?.
# * collection(force_reload = false) - returns an array of all the associated objects.
# An empty array is returned if none are found.
# * collection<<(object, ...) - adds one or more objects to the collection by setting their foreign keys to the collection's primary key.
# * collection.delete(object, ...) - removes one or more objects from the collection by setting their foreign keys to NULL.
# This will also destroy the objects if they're declared as belongs_to and dependent on this model.
# * collection=objects - replaces the collections content by deleting and adding objects as appropriate.
# * collection_singular_ids=ids - replace the collection by the objects identified by the primary keys in +ids+
# * collection.clear - removes every object from the collection. This destroys the associated objects if they
# are :dependent, deletes them directly from the database if they are :exclusively_dependent,
# and sets their foreign keys to NULL otherwise.
# * collection.empty? - returns true if there are no associated objects.
# * collection.size - returns the number of associated objects.
# * collection.find - finds an associated object according to the same rules as Base.find.
# * collection.build(attributes = {}) - returns a new object of the collection type that has been instantiated
# with +attributes+ and linked to this object through a foreign key but has not yet been saved. *Note:* This only works if an
# associated object already exists, not if it's nil!
# * collection.create(attributes = {}) - returns a new object of the collection 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).
# *Note:* This only works if an associated object already exists, not if it's nil!
#
# Example: A Firm class declares has_many :clients, which will add:
# * Firm#clients (similar to Clients.find :all, :conditions => "firm_id = #{id}")
# * Firm#clients<<
# * Firm#clients.delete
# * Firm#clients=
# * Firm#client_ids=
# * Firm#clients.clear
# * Firm#clients.empty? (similar to firm.clients.size == 0)
# * Firm#clients.size (similar to Client.count "firm_id = #{id}")
# * Firm#clients.find (similar to Client.find(id, :conditions => "firm_id = #{id}"))
# * Firm#clients.build (similar to Client.new("firm_id" => id))
# * Firm#clients.create (similar to c = Client.new("firm_id" => id); c.save; c)
# The declaration can also include an options hash to specialize the behavior of the association.
#
# Options are:
# * :class_name - specify the class name of the association. Use it only if that name can't be inferred
# from the association name. So has_many :products will by default be linked to the +Product+ class, but
# if the real class name is +SpecialProduct+, you'll have to specify it with this option.
# * :conditions - specify the conditions that the associated objects must meet in order to be included as a "WHERE"
# sql fragment, such as "price > 5 AND name LIKE 'B%'".
# * :order - specify the order in which the associated objects are returned as a "ORDER BY" sql fragment,
# such as "last_name, first_name DESC"
# * :group - specify the attribute by which the associated objects are returned as a "GROUP BY" sql fragment,
# such as "category"
# * :foreign_key - specify the foreign key used for the association. By default this is guessed to be the name
# of this class in lower-case and "_id" suffixed. So a +Person+ class that makes a has_many association will use "person_id"
# as the default foreign_key.
# * :dependent - if set to :destroy all the associated objects are destroyed
# alongside this object by calling their destroy method. If set to :delete_all all associated
# objects are deleted *without* calling their destroy method. If set to :nullify all associated
# objects' foreign keys are set to NULL *without* calling their save callbacks.
# NOTE: :dependent => true is deprecated and has been replaced with :dependent => :destroy.
# May not be set if :exclusively_dependent is also set.
# * :exclusively_dependent - Deprecated; equivalent to :dependent => :delete_all. If set to true all
# the associated object are deleted in one SQL statement without having their
# before_destroy callback run. This should only be used on associations that depend solely on this class and don't need to do any
# clean-up in before_destroy. The upside is that it's much faster, especially if there's a counter_cache involved.
# May not be set if :dependent is also set.
# * :finder_sql - specify a complete SQL statement to fetch the association. This is a good way to go for complex
# associations that depend on multiple tables. Note: When this option is used, +find_in_collection+ is _not_ added.
# * :counter_sql - specify a complete SQL statement to fetch the size of the association. If +:finder_sql+ is
# specified but +:counter_sql+, +:counter_sql+ will be generated by replacing SELECT ... FROM with SELECT COUNT(*) FROM.
# * :extend - specify a named module for extending the proxy, see "Association extensions".
# * :include - specify second-order associations that should be eager loaded when the collection is loaded.
# * :group: An attribute name by which the result should be grouped. Uses the GROUP BY SQL-clause.
# * :limit: An integer determining the limit on the number of rows that should be returned.
# * :offset: An integer determining the offset from where the rows should be fetched. So at 5, it would skip the first 4 rows.
# * :select: By default, this is * as in SELECT * FROM, but can be changed if you for example want to do a join, but not
# include the joined columns.
#
# Option examples:
# has_many :comments, :order => "posted_on"
# has_many :comments, :include => :author
# has_many :people, :class_name => "Person", :conditions => "deleted = 0", :order => "name"
# has_many :tracks, :order => "position", :dependent => :destroy
# has_many :comments, :dependent => :nullify
# has_many :subscribers, :class_name => "Person", :finder_sql =>
# 'SELECT DISTINCT people.* ' +
# '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)
if options[:through]
collection_reader_method(reflection, HasManyThroughAssociation)
else
add_multiple_associated_save_callbacks(reflection.name)
add_association_callbacks(reflection.name, reflection.options)
collection_accessor_methods(reflection, HasManyAssociation)
end
add_deprecated_api_for_has_many(reflection.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
# has_one :manager would add among others manager.nil?.
# * association(force_reload = false) - returns the associated object. Nil is returned if none is found.
# * association=(associate) - assigns the associate object, extracts the primary key, sets it as the foreign key,
# and saves the associate object.
# * association.nil? - returns true if there is no associated object.
# * 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. Note: This ONLY works if
# an association already exists. It will NOT work if the association is nil.
# * create_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 and that has already been saved (if it passed the validation).
#
# Example: An Account class declares has_one :beneficiary, which will add:
# * Account#beneficiary (similar to Beneficiary.find(:first, :conditions => "account_id = #{id}"))
# * Account#beneficiary=(beneficiary) (similar to beneficiary.account_id = account.id; beneficiary.save)
# * Account#beneficiary.nil?
# * Account#build_beneficiary (similar to Beneficiary.new("account_id" => id))
# * Account#create_beneficiary (similar to b = Beneficiary.new("account_id" => id); b.save; b)
#
# The declaration can also include an options hash to specialize the behavior of the association.
#
# Options are:
# * :class_name - specify the class name of the association. Use it only if that name can't be inferred
# from the association name. So has_one :manager will by default be linked to the +Manager+ class, but
# if the real class name is +Person+, you'll have to specify it with this option.
# * :conditions - specify the conditions that the associated object must meet in order to be included as a "WHERE"
# sql fragment, such as "rank = 5".
# * :order - specify the order from which the associated object will be picked at the top. Specified as
# an "ORDER BY" sql fragment, such as "last_name, first_name DESC"
# * :dependent - if set to :destroy (or true) all the associated objects are destroyed when this object is. Also,
# association is assigned.
# * :foreign_key - specify the foreign key used for the association. By default this is guessed to be the name
# of this class in lower-case and "_id" suffixed. So a +Person+ class that makes a has_one association will use "person_id"
# as the default foreign_key.
# * :include - specify second-order associations that should be eager loaded when this object is loaded.
#
# Option examples:
# has_one :credit_card, :dependent => :destroy # destroys the associated credit card
# has_one :credit_card, :dependent => :nullify # updates the associated records foriegn key value to null rather than destroying it
# 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 = {})
reflection = create_has_one_reflection(association_id, options)
module_eval do
after_save <<-EOF
association = instance_variable_get("@#{reflection.name}")
unless association.nil?
association["#{reflection.primary_key_name}"] = id
association.save(true)
end
EOF
end
association_accessor_methods(reflection, HasOneAssociation)
association_constructor_method(:build, reflection, HasOneAssociation)
association_constructor_method(:create, reflection, HasOneAssociation)
configure_dependency_for_has_one(reflection)
# deprecated api
deprecated_has_association_method(reflection.name)
deprecated_association_comparison_method(reflection.name, reflection.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
# belongs_to :author would add among others author.nil?.
# * association(force_reload = false) - returns the associated object. Nil is returned if none is found.
# * association=(associate) - assigns the associate object, extracts the primary key, and sets it as the foreign key.
# * association.nil? - returns true if there is no associated object.
# * 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.
# * create_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 and that has already been saved (if it passed the validation).
#
# Example: A Post class declares belongs_to :author, which will add:
# * Post#author (similar to Author.find(author_id))
# * Post#author=(author) (similar to post.author_id = author.id)
# * Post#author? (similar to post.author == some_author)
# * Post#author.nil?
# * Post#build_author (similar to post.author = Author.new)
# * Post#create_author (similar to post.author = Author.new; post.author.save; post.author)
# The declaration can also include an options hash to specialize the behavior of the association.
#
# Options are:
# * :class_name - specify the class name of the association. Use it only if that name can't be inferred
# from the association name. So has_one :author will by default be linked to the +Author+ class, but
# if the real class name is +Person+, you'll have to specify it with this option.
# * :conditions - specify the conditions that the associated object must meet in order to be included as a "WHERE"
# sql fragment, such as "authorized = 1".
# * :order - specify the order from which the associated object will be picked at the top. Specified as
# an "ORDER BY" sql fragment, such as "last_name, first_name DESC"
# * :foreign_key - specify the foreign key used for the association. By default this is guessed to be the name
# of the associated class in lower-case and "_id" suffixed. So a +Person+ class that makes a belongs_to association to a
# +Boss+ class will use "boss_id" as the default foreign_key.
# * :counter_cache - caches the number of belonging objects on the associate class through use of increment_counter
# and decrement_counter. The counter cache is incremented when an object of this class is created and decremented when it's
# destroyed. This requires that a column named "#{table_name}_count" (such as comments_count for a belonging Comment class)
# is used on the associate class (such as a Post class). You can also specify a custom counter cache column by given that
# name instead of a true/false value to this option (e.g., :counter_cache => :my_custom_counter.)
# * :include - specify second-order associations that should be eager loaded when this object is loaded.
#
# Option examples:
# belongs_to :firm, :foreign_key => "client_of"
# 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 = {})
reflection = create_belongs_to_reflection(association_id, options)
if reflection.options[:polymorphic]
association_accessor_methods(reflection, BelongsToPolymorphicAssociation)
module_eval do
before_save <<-EOF
association = instance_variable_get("@#{reflection.name}")
if !association.nil?
if association.new_record?
association.save(true)
end
if association.updated?
self["#{reflection.primary_key_name}"] = association.id
self["#{reflection.options[:foreign_type]}"] = ActiveRecord::Base.send(:class_name_of_active_record_descendant, association.class).to_s
end
end
EOF
end
else
association_accessor_methods(reflection, BelongsToAssociation)
association_constructor_method(:build, reflection, BelongsToAssociation)
association_constructor_method(:create, reflection, BelongsToAssociation)
module_eval do
before_save <<-EOF
association = instance_variable_get("@#{reflection.name}")
if !association.nil?
if association.new_record?
association.save(true)
end
if association.updated?
self["#{reflection.primary_key_name}"] = association.id
end
end
EOF
end
# deprecated api
deprecated_has_association_method(reflection.name)
deprecated_association_comparison_method(reflection.name, reflection.class_name)
end
if options[:counter_cache]
cache_column = options[:counter_cache] == true ?
"#{self.to_s.underscore.pluralize}_count" :
options[:counter_cache]
module_eval(
"after_create '#{reflection.name}.class.increment_counter(\"#{cache_column}\", #{reflection.primary_key_name})" +
" unless #{reflection.name}.nil?'"
)
module_eval(
"before_destroy '#{reflection.name}.class.decrement_counter(\"#{cache_column}\", #{reflection.primary_key_name})" +
" unless #{reflection.name}.nil?'"
)
end
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".
#
# Any additional fields added to the join table will be placed as attributes when pulling records out through
# has_and_belongs_to_many associations. This is helpful when have information about the association itself
# that you want available on retrieval. Note that any fields in the join table will override matching field names
# in the two joined tables. As a consequence, having an "id" field in the join table usually has the undesirable
# result of clobbering the "id" fields in either of the other two tables.
#
# Adds the following methods for retrieval and query.
# +collection+ is replaced with the symbol passed as the first argument, so
# has_and_belongs_to_many :categories would add among others categories.empty?.
# * collection(force_reload = false) - returns an array of all the associated objects.
# An empty array is returned if none is found.
# * collection<<(object, ...) - adds one or more objects to the collection by creating associations in the join table
# (collection.push and collection.concat are aliases to this method).
# * collection.push_with_attributes(object, join_attributes) - adds one to the collection by creating an association in the join table that
# also holds the attributes from join_attributes (should be a hash with the column names as keys). This can be used to have additional
# attributes on the join, which will be injected into the associated objects when they are retrieved through the collection.
# (collection.concat_with_attributes is an alias to this method).
# * collection.delete(object, ...) - removes one or more objects from the collection by removing their associations from the join table.
# This does not destroy the objects.
# * collection=objects - replaces the collections content by deleting and adding objects as appropriate.
# * collection_singular_ids=ids - replace the collection by the objects identified by the primary keys in +ids+
# * collection.clear - removes every object from the collection. This does not destroy the objects.
# * collection.empty? - returns true if there are no associated objects.
# * collection.size - returns the number of associated objects.
# * collection.find(id) - 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 has_and_belongs_to_many :projects, which will add:
# * Developer#projects
# * Developer#projects<<
# * Developer#projects.push_with_attributes
# * Developer#projects.delete
# * Developer#projects=
# * Developer#project_ids=
# * Developer#projects.clear
# * Developer#projects.empty?
# * Developer#projects.size
# * Developer#projects.find(id)
# The declaration may include an options hash to specialize the behavior of the association.
#
# Options are:
# * :class_name - specify the class name of the association. Use it only if that name can't be inferred
# from the association name. So has_and_belongs_to_many :projects will by default be linked to the
# +Project+ class, but if the real class name is +SuperProject+, you'll have to specify it with this option.
# * :join_table - specify the name of the join table if the default based on lexical order isn't what you want.
# WARNING: If you're overwriting the table name of either class, the table_name method MUST be declared underneath any
# has_and_belongs_to_many declaration in order to work.
# * :foreign_key - specify the foreign key used for the association. By default this is guessed to be the name
# of this class in lower-case and "_id" suffixed. So a +Person+ class that makes a has_and_belongs_to_many association
# will use "person_id" as the default foreign_key.
# * :association_foreign_key - specify the association foreign key used for the association. By default this is
# guessed to be the name of the associated class in lower-case and "_id" suffixed. So if the associated class is +Project+,
# the has_and_belongs_to_many association will use "project_id" as the default association foreign_key.
# * :conditions - specify the conditions that the associated object must meet in order to be included as a "WHERE"
# sql fragment, such as "authorized = 1".
# * :order - specify the order in which the associated objects are returned as a "ORDER BY" sql fragment, such as "last_name, first_name DESC"
# * :uniq - if set to true, duplicate associated objects will be ignored by accessors and query methods
# * :finder_sql - overwrite the default generated SQL used to fetch the association with a manual one
# * :delete_sql - overwrite the default generated SQL used to remove links between the associated
# classes with a manual one
# * :insert_sql - overwrite the default generated SQL used to add links between the associated classes
# with a manual one
# * :extend - anonymous module for extending the proxy, see "Association extensions".
# * :include - specify second-order associations that should be eager loaded when the collection is loaded.
# * :group: An attribute name by which the result should be grouped. Uses the GROUP BY SQL-clause.
# * :limit: An integer determining the limit on the number of rows that should be returned.
# * :offset: An integer determining the offset from where the rows should be fetched. So at 5, it would skip the first 4 rows.
# * :select: By default, this is * as in SELECT * FROM, but can be changed if you for example want to do a join, but not
# include the joined columns.
#
# Option examples:
# has_and_belongs_to_many :projects
# has_and_belongs_to_many :projects, :include => [ :milestones, :manager ]
# has_and_belongs_to_many :nations, :class_name => "Country"
# has_and_belongs_to_many :categories, :join_table => "prods_cats"
# 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)
add_multiple_associated_save_callbacks(reflection.name)
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.
old_method = "destroy_without_habtm_shim_for_#{reflection.name}"
class_eval <<-end_eval
alias_method :#{old_method}, :destroy_without_callbacks
def destroy_without_callbacks
#{reflection.name}.clear
#{old_method}
end
end_eval
add_association_callbacks(reflection.name, options)
# deprecated api
deprecated_collection_count_method(reflection.name)
deprecated_add_association_relation(reflection.name)
deprecated_remove_association_relation(reflection.name)
deprecated_has_collection_method(reflection.name)
end
private
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)
define_method(reflection.name) do |*params|
force_reload = params.first unless params.empty?
association = instance_variable_get("@#{reflection.name}")
if association.nil? || force_reload
association = association_proxy_class.new(self, reflection)
retval = association.reload
unless retval.nil?
instance_variable_set("@#{reflection.name}", association)
else
instance_variable_set("@#{reflection.name}", nil)
return nil
end
end
association
end
define_method("#{reflection.name}=") do |new_value|
association = instance_variable_get("@#{reflection.name}")
if association.nil?
association = association_proxy_class.new(self, reflection)
end
association.replace(new_value)
unless new_value.nil?
instance_variable_set("@#{reflection.name}", association)
else
instance_variable_set("@#{reflection.name}", nil)
return nil
end
association
end
define_method("set_#{reflection.name}_target") do |target|
return if target.nil?
association = association_proxy_class.new(self, reflection)
association.target = target
instance_variable_set("@#{reflection.name}", association)
end
end
def collection_reader_method(reflection, association_proxy_class)
define_method(reflection.name) do |*params|
force_reload = params.first unless params.empty?
association = instance_variable_get("@#{reflection.name}")
unless association.respond_to?(:loaded?)
association = association_proxy_class.new(self, reflection)
instance_variable_set("@#{reflection.name}", association)
end
association.reload if force_reload
association
end
end
def collection_accessor_methods(reflection, association_proxy_class)
collection_reader_method(reflection, association_proxy_class)
define_method("#{reflection.name}=") do |new_value|
association = instance_variable_get("@#{reflection.name}")
unless association.respond_to?(:loaded?)
association = association_proxy_class.new(self, reflection)
instance_variable_set("@#{reflection.name}", association)
end
association.replace(new_value)
association
end
define_method("#{reflection.name.to_s.singularize}_ids=") do |new_value|
send("#{reflection.name}=", reflection.class_name.constantize.find(new_value))
end
end
def require_association_class(class_name)
require_association(Inflector.underscore(class_name)) if class_name
end
def add_multiple_associated_save_callbacks(association_name)
method_name = "validate_associated_records_for_#{association_name}".to_sym
define_method(method_name) do
association = instance_variable_get("@#{association_name}")
if association.respond_to?(:loaded?)
if new_record?
association
else
association.select { |record| record.new_record? }
end.each do |record|
errors.add "#{association_name}" unless record.valid?
end
end
end
validate method_name
before_save("@new_record_before_save = new_record?; true")
after_callback = <<-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
# Doesn't use after_save as that would save associations added in after_create/after_update twice
after_create(after_callback)
after_update(after_callback)
end
def association_constructor_method(constructor, reflection, association_proxy_class)
define_method("#{constructor}_#{reflection.name}") do |*params|
attributees = params.first unless params.empty?
replace_existing = params[1].nil? ? true : params[1]
association = instance_variable_get("@#{reflection.name}")
if association.nil?
association = association_proxy_class.new(self, reflection)
instance_variable_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 count_with_associations(options = {})
join_dependency = JoinDependency.new(self, options[:include])
return count_by_sql(construct_counter_sql_with_included_associations(options, join_dependency))
end
def find_with_associations(options = {})
join_dependency = JoinDependency.new(self, options[:include])
rows = select_all_rows(options, join_dependency)
return join_dependency.instantiate(rows)
end
def configure_dependency_for_has_many(reflection)
if reflection.options[:dependent] && reflection.options[:exclusively_dependent]
raise ArgumentError, ':dependent and :exclusively_dependent are mutually exclusive options. You may specify one or the other.'
end
if reflection.options[:exclusively_dependent]
reflection.options[:dependent] = :delete_all
#warn "The :exclusively_dependent option is deprecated. Please use :dependent => :delete_all instead.")
end
# See HasManyAssociation#delete_records. Dependent associations
# delete children, otherwise foreign key is set to NULL.
case reflection.options[:dependent]
when :destroy, true
module_eval "before_destroy '#{reflection.name}.each { |o| o.destroy }'"
when :delete_all
module_eval "before_destroy { |record| #{reflection.class_name}.delete_all(%(#{reflection.primary_key_name} = \#{record.quoted_id})) }"
when :nullify
module_eval "before_destroy { |record| #{reflection.class_name}.update_all(%(#{reflection.primary_key_name} = NULL), %(#{reflection.primary_key_name} = \#{record.quoted_id})) }"
when nil, false
# pass
else
raise ArgumentError, 'The :dependent option expects either :destroy, :delete_all, or :nullify'
end
end
def configure_dependency_for_has_one(reflection)
case reflection.options[:dependent]
when :destroy, true
module_eval "before_destroy '#{reflection.name}.destroy unless #{reflection.name}.nil?'"
when :nullify
module_eval "before_destroy '#{reflection.name}.update_attribute(\"#{reflection.primary_key_name}\", nil)'"
when nil, false
# pass
else
raise ArgumentError, "The :dependent option expects either :destroy or :nullify."
end
end
def add_deprecated_api_for_has_many(association_name)
deprecated_collection_count_method(association_name)
deprecated_add_association_relation(association_name)
deprecated_remove_association_relation(association_name)
deprecated_has_collection_method(association_name)
deprecated_find_in_collection_method(association_name)
deprecated_find_all_in_collection_method(association_name)
deprecated_collection_create_method(association_name)
deprecated_collection_build_method(association_name)
end
def create_has_many_reflection(association_id, options, &extension)
options.assert_valid_keys(
:class_name, :table_name, :foreign_key,
:exclusively_dependent, :dependent,
:select, :conditions, :include, :order, :group, :limit, :offset,
:as, :through,
:finder_sql, :counter_sql,
:before_add, :after_add, :before_remove, :after_remove,
:extend
)
options[:extend] = create_extension_module(association_id, extension) if block_given?
create_reflection(:has_many, association_id, options, self)
end
def create_has_one_reflection(association_id, options)
options.assert_valid_keys(
:class_name, :foreign_key, :remote, :conditions, :order, :include, :dependent, :counter_cache, :extend, :as
)
create_reflection(:has_one, association_id, options, self)
end
def create_belongs_to_reflection(association_id, options)
options.assert_valid_keys(
:class_name, :foreign_key, :foreign_type, :remote, :conditions, :order, :include, :dependent,
:counter_cache, :extend, :polymorphic
)
reflection = create_reflection(:belongs_to, association_id, options, self)
if options[:polymorphic]
reflection.options[:foreign_type] ||= reflection.class_name.underscore + "_type"
end
reflection
end
def create_has_and_belongs_to_many_reflection(association_id, options, &extension)
options.assert_valid_keys(
:class_name, :table_name, :join_table, :foreign_key, :association_foreign_key,
:select, :conditions, :include, :order, :group, :limit, :offset,
:finder_sql, :delete_sql, :insert_sql, :uniq,
:before_add, :after_add, :before_remove, :after_remove,
:extend
)
options[:extend] = create_extension_module(association_id, extension) if block_given?
reflection = create_reflection(:has_and_belongs_to_many, association_id, options, self)
reflection.options[:join_table] ||= join_table_name(undecorated_table_name(self.to_s), undecorated_table_name(reflection.class_name))
reflection
end
def reflect_on_included_associations(associations)
[ associations ].flatten.collect { |association| reflect_on_association(association.to_s.intern) }
end
def guard_against_unlimitable_reflections(reflections, options)
if (options[:offset] || options[:limit]) && !using_limitable_reflections?(reflections)
raise(
ConfigurationError,
"You can not use offset and limit together with has_many or has_and_belongs_to_many associations"
)
end
end
def select_all_rows(options, join_dependency)
connection.select_all(
construct_finder_sql_with_included_associations(options, join_dependency),
"#{name} Load Including Associations"
)
end
def construct_counter_sql_with_included_associations(options, join_dependency)
sql = "SELECT COUNT(DISTINCT #{table_name}.#{primary_key})"
# A (slower) workaround if we're using a backend, like sqlite, that doesn't support COUNT DISTINCT.
if !Base.connection.supports_count_distinct?
sql = "SELECT COUNT(*) FROM (SELECT DISTINCT #{table_name}.#{primary_key}"
end
sql << " FROM #{table_name} "
sql << join_dependency.join_associations.collect{|join| join.association_join }.join
sql << "#{options[:joins]} " if options[:joins]
add_conditions!(sql, options[:conditions])
add_limited_ids_condition!(sql, options, join_dependency) if !using_limitable_reflections?(join_dependency.reflections) && options[:limit]
add_limit!(sql, options) if using_limitable_reflections?(join_dependency.reflections)
if !Base.connection.supports_count_distinct?
sql << ")"
end
return sanitize_sql(sql)
end
def construct_finder_sql_with_included_associations(options, join_dependency)
sql = "SELECT #{column_aliases(join_dependency)} FROM #{options[:from] || table_name} "
sql << join_dependency.join_associations.collect{|join| join.association_join }.join
sql << "#{options[:joins]} " if options[:joins]
add_conditions!(sql, options[:conditions])
add_limited_ids_condition!(sql, options, join_dependency) if !using_limitable_reflections?(join_dependency.reflections) && options[:limit]
sql << "ORDER BY #{options[:order]} " if options[:order]
add_limit!(sql, options) if using_limitable_reflections?(join_dependency.reflections)
return sanitize_sql(sql)
end
def add_limited_ids_condition!(sql, options, join_dependency)
unless (id_list = select_limited_ids_list(options, join_dependency)).empty?
sql << "#{condition_word(sql)} #{table_name}.#{primary_key} IN (#{id_list}) "
end
end
def select_limited_ids_list(options, join_dependency)
connection.select_values(
construct_finder_sql_for_association_limiting(options, join_dependency),
"#{name} Load IDs For Limited Eager Loading"
).collect { |id| connection.quote(id) }.join(", ")
end
def construct_finder_sql_for_association_limiting(options, join_dependency)
#sql = "SELECT DISTINCT #{table_name}.#{primary_key} FROM #{table_name} "
sql = "SELECT "
sql << "DISTINCT #{table_name}." if include_eager_conditions?(options) || include_eager_order?(options)
sql << "#{primary_key} FROM #{table_name} "
if include_eager_conditions?(options) || include_eager_order?(options)
sql << join_dependency.join_associations.collect{|join| join.association_join }.join
sql << "#{options[:joins]} " if options[:joins]
end
add_conditions!(sql, options[:conditions])
sql << "ORDER BY #{options[:order]} " if options[:order]
add_limit!(sql, options)
return sanitize_sql(sql)
end
def include_eager_conditions?(options)
conditions = options[:conditions]
return false unless conditions
conditions = conditions.first if conditions.is_a?(Array)
conditions.scan(/(\w+)\.\w+/).flatten.any? do |condition_table_name|
condition_table_name != table_name
end
end
def include_eager_order?(options)
order = options[:order]
return false unless order
order.scan(/(\w+)\.\w+/).flatten.any? do |order_table_name|
order_table_name != table_name
end
end
def using_limitable_reflections?(reflections)
reflections.reject { |r| [ :belongs_to, :has_one ].include?(r.macro) }.length.zero?
end
def column_aliases(join_dependency)
join_dependency.joins.collect{|join| join.column_names_with_alias.collect{|column_name, aliased_name|
"#{join.aliased_table_name}.#{connection.quote_column_name column_name} AS #{aliased_name}"}}.flatten.join(", ")
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.to_s}_for_#{association_name.to_s}"
defined_callbacks = options[callback_name.to_sym]
if options.has_key?(callback_name.to_sym)
class_inheritable_reader full_callback_name.to_sym
write_inheritable_array(full_callback_name.to_sym, [defined_callbacks].flatten)
end
end
end
def condition_word(sql)
sql =~ /where/i ? " AND " : "WHERE "
end
def create_extension_module(association_id, extension)
extension_module_name = "#{self.to_s}#{association_id.to_s.camelize}AssociationExtension"
silence_warnings do
Object.const_set(extension_module_name, Module.new(&extension))
end
extension_module_name.constantize
end
class JoinDependency
attr_reader :joins, :reflections, :table_aliases
def initialize(base, associations)
@joins = [JoinBase.new(base)]
@associations = associations
@reflections = []
@base_records_hash = {}
@base_records_in_order = []
@table_aliases = Hash.new { |aliases, table| aliases[table] = 0 }
@table_aliases[base.table_name] = 1
build(associations)
end
def join_associations
@joins[1..-1].to_a
end
def join_base
@joins[0]
end
def instantiate(rows)
rows.each_with_index do |row, i|
primary_id = join_base.record_id(row)
unless @base_records_hash[primary_id]
@base_records_in_order << (@base_records_hash[primary_id] = join_base.instantiate(row))
end
construct(@base_records_hash[primary_id], @associations, join_associations.dup, row)
end
return @base_records_in_order
end
def aliased_table_names_for(table_name)
joins.select{|join| join.table_name == table_name }.collect{|join| join.aliased_table_name}
end
protected
def build(associations, parent = nil)
parent ||= @joins.last
case associations
when Symbol, String
reflection = parent.reflections[associations.to_s.intern] or
raise ConfigurationError, "Association named '#{ associations }' was not found; perhaps you misspelled it?"
@reflections << reflection
@joins << JoinAssociation.new(reflection, self, parent)
when Array
associations.each do |association|
build(association, parent)
end
when Hash
associations.keys.sort{|a,b|a.to_s<=>b.to_s}.each do |name|
build(name, parent)
build(associations[name])
end
else
raise ConfigurationError, associations.inspect
end
end
def construct(parent, associations, joins, row)
case associations
when Symbol, String
while (join = joins.shift).reflection.name.to_s != associations.to_s
raise ConfigurationError, "Not Enough Associations" if joins.empty?
end
construct_association(parent, join, row)
when Array
associations.each do |association|
construct(parent, association, joins, row)
end
when Hash
associations.keys.sort{|a,b|a.to_s<=>b.to_s}.each do |name|
association = construct_association(parent, joins.shift, row)
construct(association, associations[name], joins, row) if association
end
else
raise ConfigurationError, associations.inspect
end
end
def construct_association(record, join, row)
case join.reflection.macro
when :has_many, :has_and_belongs_to_many
collection = record.send(join.reflection.name)
collection.loaded
return nil if record.id.to_s != join.parent.record_id(row).to_s or row[join.aliased_primary_key].nil?
association = join.instantiate(row)
collection.target.push(association) unless collection.target.include?(association)
when :has_one, :belongs_to
return if record.id.to_s != join.parent.record_id(row).to_s or row[join.aliased_primary_key].nil?
association = join.instantiate(row)
record.send("set_#{join.reflection.name}_target", association)
else
raise ConfigurationError, "unknown macro: #{join.reflection.macro}"
end
return association
end
class JoinBase
attr_reader :active_record
delegate :table_name, :column_names, :primary_key, :reflections, :to=>:active_record
def initialize(active_record)
@active_record = active_record
@cached_record = {}
end
def aliased_prefix
"t0"
end
def aliased_primary_key
"#{ aliased_prefix }_r0"
end
def aliased_table_name
active_record.table_name
end
def column_names_with_alias
unless @column_names_with_alias
@column_names_with_alias = []
([primary_key] + (column_names - [primary_key])).each_with_index do |column_name, i|
@column_names_with_alias << [column_name, "#{ aliased_prefix }_r#{ i }"]
end
end
return @column_names_with_alias
end
def extract_record(row)
column_names_with_alias.inject({}){|record, (cn, an)| record[cn] = row[an]; record}
end
def record_id(row)
row[aliased_primary_key]
end
def instantiate(row)
@cached_record[record_id(row)] ||= active_record.instantiate(extract_record(row))
end
end
class JoinAssociation < JoinBase
attr_reader :reflection, :parent, :aliased_table_name, :aliased_prefix
delegate :options, :klass, :to=>:reflection
def initialize(reflection, join_dependency, parent = nil)
super(reflection.klass)
@parent = parent
@reflection = reflection
@aliased_prefix = "t#{ join_dependency.joins.size }"
@aliased_table_name = table_name # start with the table name
unless join_dependency.table_aliases[aliased_table_name].zero?
# if the table name has been used, then use an alias
# if the alias has been used, add a '_n' suffix to the end.
@aliased_table_name = "#{parent.active_record.to_s.underscore}_#{reflection.name}_#{join_dependency.table_aliases[aliased_table_name]}".gsub(/_1$/, '')
end
join_dependency.table_aliases[aliased_table_name] += 1
end
def association_join
join = case reflection.macro
when :has_and_belongs_to_many
join_table_name =
" LEFT OUTER JOIN %s ON %s.%s = %s.%s " % [
options[:join_table], options[:join_table],
options[:foreign_key] || reflection.active_record.to_s.classify.foreign_key,
reflection.active_record.table_name, reflection.active_record.primary_key] +
" LEFT OUTER JOIN %s AS %s ON %s.%s = %s.%s " % [
table_name, aliased_table_name, aliased_table_name, klass.primary_key,
options[:join_table], options[:association_foreign_key] || klass.table_name.classify.foreign_key
]
when :has_many, :has_one
case
when reflection.macro == :has_many && reflection.options[:through]
through_reflection = parent.active_record.reflect_on_association(reflection.options[:through])
aliased_through_table = "#{parent.table_name}_#{through_reflection.klass.table_name}"
if through_reflection.options[:as] # has_many :through against a polymorphic join
polymorphic_foreign_key = through_reflection.options[:as].to_s + '_id'
polymorphic_foreign_type = through_reflection.options[:as].to_s + '_type'
" LEFT OUTER JOIN %s AS %s ON (%s.%s = %s.%s AND %s.%s = %s) " % [through_reflection.klass.table_name, aliased_through_table,
aliased_through_table, polymorphic_foreign_key,
parent.aliased_table_name, parent.primary_key,
aliased_through_table, polymorphic_foreign_type, klass.quote(parent.active_record.base_class.name)] +
" LEFT OUTER JOIN %s AS %s ON %s.%s = %s.%s " % [table_name, aliased_table_name,
aliased_table_name, primary_key, aliased_through_table, options[:foreign_key] || reflection.klass.to_s.classify.foreign_key
]
else # has_many :through against a normal join
" LEFT OUTER JOIN %s AS %s ON %s.%s = %s.%s " % [through_reflection.klass.table_name, aliased_through_table,
aliased_through_table, through_reflection.options[:foreign_key] || through_reflection.active_record.to_s.classify.foreign_key,
parent.aliased_table_name, parent.primary_key] +
" LEFT OUTER JOIN %s AS %s ON %s.%s = %s.%s " % [table_name, aliased_table_name,
aliased_table_name, primary_key, aliased_through_table, options[:foreign_key] || klass.to_s.classify.foreign_key
]
end
when reflection.macro == :has_many && reflection.options[:as]
" LEFT OUTER JOIN %s AS %s ON %s.%s = %s.%s AND %s.%s = %s" % [table_name, aliased_table_name,
aliased_table_name, "#{reflection.options[:as]}_id",
parent.aliased_table_name, parent.primary_key,
aliased_table_name, "#{reflection.options[:as]}_type",
klass.quote(parent.active_record.base_class.name)
]
else
" LEFT OUTER JOIN %s AS %s ON %s.%s = %s.%s " % [table_name, aliased_table_name,
aliased_table_name, options[:foreign_key] || reflection.active_record.to_s.classify.foreign_key,
parent.aliased_table_name, parent.primary_key
]
end
when :belongs_to
" LEFT OUTER JOIN %s AS %s ON %s.%s = %s.%s " % [table_name, aliased_table_name,
aliased_table_name, reflection.klass.primary_key,
parent.aliased_table_name, options[:foreign_key] || klass.to_s.classify.foreign_key
]
else
""
end
join << %(AND %s.%s = %s ) % [
aliased_table_name,
reflection.active_record.connection.quote_column_name(reflection.active_record.inheritance_column),
klass.quote(klass.name)] unless join.blank? || klass.descends_from_active_record?
join
end
end
end
end
end
end