Module | ActiveRecord::Associations::ClassMethods |
In: |
vendor/rails/activerecord/lib/active_record/deprecated_associations.rb
vendor/rails/activerecord/lib/active_record/associations.rb |
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 was 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:
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) )
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.
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.
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 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 its no catch-all for performance problems, but its a great way to cut down on the number of queries in a situation as the one described above.
Please note that because eager loading is fetching both models and associations in the same grab, it doesn’t make sense to use the :limit and :offset options on has_many and has_and_belongs_to_many associations and an ConfigurationError exception will be raised if attempted. It does, however, work just fine with has_one and belongs_to associations.
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.
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
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.
All of the association macros can be specialized through options which makes more complex cases than the simple and guessable ones possible.
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?.
Example: A Post class declares belongs_to :author, which will add:
The declaration can also include an options hash to specialize the behavior of the association.
Options are:
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}'
# File vendor/rails/activerecord/lib/active_record/associations.rb, line 432 432: def belongs_to(association_id, options = {}) 433: validate_options([ :class_name, :foreign_key, :remote, :conditions, :order, :dependent, :counter_cache ], options.keys) 434: 435: association_name, association_class_name, class_primary_key_name = 436: associate_identification(association_id, options[:class_name], options[:foreign_key], false) 437: 438: require_association_class(association_class_name) 439: 440: association_class_primary_key_name = options[:foreign_key] || Inflector.underscore(Inflector.demodulize(association_class_name)) + "_id" 441: 442: association_accessor_methods(association_name, association_class_name, association_class_primary_key_name, options, BelongsToAssociation) 443: association_constructor_method(:build, association_name, association_class_name, association_class_primary_key_name, options, BelongsToAssociation) 444: association_constructor_method(:create, association_name, association_class_name, association_class_primary_key_name, options, BelongsToAssociation) 445: 446: module_eval do 447: before_save "association = instance_variable_get(\"@\#{association_name}\")\nif not association.nil? and association.new_record?\nassociation.save(true)\nself[\"\#{association_class_primary_key_name}\"] = association.id\nassociation.send(:construct_sql)\nend\n" 448: end 449: 450: if options[:counter_cache] 451: module_eval( 452: "after_create '#{association_class_name}.increment_counter(\"#{self.to_s.underscore.pluralize + "_count"}\", #{association_class_primary_key_name})" + 453: " unless #{association_name}.nil?'" 454: ) 455: 456: module_eval( 457: "before_destroy '#{association_class_name}.decrement_counter(\"#{self.to_s.underscore.pluralize + "_count"}\", #{association_class_primary_key_name})" + 458: " unless #{association_name}.nil?'" 459: ) 460: end 461: 462: # deprecated api 463: deprecated_has_association_method(association_name) 464: deprecated_association_comparison_method(association_name, association_class_name) 465: 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?.
Example: An Developer class declares has_and_belongs_to_many :projects, which will add:
The declaration may include an options hash to specialize the behavior of the association.
Options are:
Option examples:
has_and_belongs_to_many :projects 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}'
# File vendor/rails/activerecord/lib/active_record/associations.rb, line 548 548: def has_and_belongs_to_many(association_id, options = {}) 549: validate_options([ :class_name, :table_name, :foreign_key, :association_foreign_key, :conditions, 550: :join_table, :finder_sql, :delete_sql, :insert_sql, :order, :uniq, :before_add, :after_add, 551: :before_remove, :after_remove ], options.keys) 552: association_name, association_class_name, association_class_primary_key_name = 553: associate_identification(association_id, options[:class_name], options[:foreign_key]) 554: 555: require_association_class(association_class_name) 556: 557: options[:join_table] ||= join_table_name(undecorated_table_name(self.to_s), undecorated_table_name(association_class_name)) 558: 559: add_multiple_associated_save_callbacks(association_name) 560: 561: collection_accessor_methods(association_name, association_class_name, association_class_primary_key_name, options, HasAndBelongsToManyAssociation) 562: 563: before_destroy_sql = "DELETE FROM #{options[:join_table]} WHERE #{association_class_primary_key_name} = \\\#{self.quoted_id}" 564: module_eval(%{before_destroy "self.connection.delete(%{#{before_destroy_sql}})"}) # " 565: add_association_callbacks(association_name, options) 566: 567: # deprecated api 568: deprecated_collection_count_method(association_name) 569: deprecated_add_association_relation(association_name) 570: deprecated_remove_association_relation(association_name) 571: deprecated_has_collection_method(association_name) 572: end
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?.
Example: A Firm class declares has_many :clients, which will add:
The declaration can also include an options hash to specialize the behavior of the association.
Options are:
Option examples:
has_many :comments, :order => "posted_on" has_many :people, :class_name => "Person", :conditions => "deleted = 0", :order => "name" has_many :tracks, :order => "position", :dependent => true 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'
# File vendor/rails/activerecord/lib/active_record/associations.rb, line 286 286: def has_many(association_id, options = {}) 287: validate_options([ :foreign_key, :class_name, :exclusively_dependent, :dependent, :conditions, :order, :finder_sql, :counter_sql, 288: :before_add, :after_add, :before_remove, :after_remove ], options.keys) 289: association_name, association_class_name, association_class_primary_key_name = 290: associate_identification(association_id, options[:class_name], options[:foreign_key]) 291: 292: require_association_class(association_class_name) 293: 294: if options[:dependent] and options[:exclusively_dependent] 295: raise ArgumentError, ':dependent and :exclusively_dependent are mutually exclusive options. You may specify one or the other.' # ' ruby-mode 296: # See HasManyAssociation#delete_records. Dependent associations 297: # delete children, otherwise foreign key is set to NULL. 298: elsif options[:dependent] 299: module_eval "before_destroy '#{association_name}.each { |o| o.destroy }'" 300: elsif options[:exclusively_dependent] 301: module_eval "before_destroy { |record| #{association_class_name}.delete_all(%(#{association_class_primary_key_name} = \#{record.quoted_id})) }" 302: end 303: 304: add_multiple_associated_save_callbacks(association_name) 305: add_association_callbacks(association_name, options) 306: 307: collection_accessor_methods(association_name, association_class_name, association_class_primary_key_name, options, HasManyAssociation) 308: 309: # deprecated api 310: deprecated_collection_count_method(association_name) 311: deprecated_add_association_relation(association_name) 312: deprecated_remove_association_relation(association_name) 313: deprecated_has_collection_method(association_name) 314: deprecated_find_in_collection_method(association_name) 315: deprecated_find_all_in_collection_method(association_name) 316: deprecated_collection_create_method(association_name) 317: deprecated_collection_build_method(association_name) 318: 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?.
Example: An Account class declares has_one :beneficiary, which will add:
The declaration can also include an options hash to specialize the behavior of the association.
Options are:
an "ORDER BY" sql fragment, such as "last_name, first_name DESC"
Option examples:
has_one :credit_card, :dependent => true has_one :last_comment, :class_name => "Comment", :order => "posted_on" has_one :project_manager, :class_name => "Person", :conditions => "role = 'project_manager'"
# File vendor/rails/activerecord/lib/active_record/associations.rb, line 360 360: def has_one(association_id, options = {}) 361: validate_options([ :class_name, :foreign_key, :remote, :conditions, :order, :dependent, :counter_cache ], options.keys) 362: 363: association_name, association_class_name, association_class_primary_key_name = 364: associate_identification(association_id, options[:class_name], options[:foreign_key], false) 365: 366: require_association_class(association_class_name) 367: 368: module_eval do 369: after_save "association = instance_variable_get(\"@\#{association_name}\")\nunless association.nil?\nassociation[\"\#{association_class_primary_key_name}\"] = id\nassociation.save(true)\nassociation.send(:construct_sql)\nend\n" 370: end 371: 372: association_accessor_methods(association_name, association_class_name, association_class_primary_key_name, options, HasOneAssociation) 373: association_constructor_method(:build, association_name, association_class_name, association_class_primary_key_name, options, HasOneAssociation) 374: association_constructor_method(:create, association_name, association_class_name, association_class_primary_key_name, options, HasOneAssociation) 375: 376: module_eval "before_destroy '#{association_name}.destroy unless #{association_name}.nil?'" if options[:dependent] 377: 378: # deprecated api 379: deprecated_has_association_method(association_name) 380: deprecated_association_comparison_method(association_name, association_class_name) 381: end