| In: |
vendor/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 to ease the traversel and manipulation of its relationships:
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_count # uses the milestone cache project.has_milestones? # uses the milestone cache project.milestones_count(true) # fetches milestones from the database project.milestones # uses the milestone cache
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
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 retrival 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 has_clients?.
Example: A Firm class declares has_many :clients, which will add:
The declaration can also include an options hash to specialize the generated methods.
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/activerecord/lib/active_record/associations.rb, line 131
131: def has_many(collection_id, options = {})
132: validate_options([ :foreign_key, :class_name, :exclusively_dependent, :dependent, :conditions, :order, :finder_sql ], options.keys)
133:
134: collection_name, collection_class_name, class_primary_key_name =
135: associate_identification(collection_id, options[:class_name], options[:foreign_key])
136:
137: if options[:finder_sql]
138: counter_sql = options[:finder_sql].gsub(/SELECT (.*) FROM/, "SELECT COUNT(*) FROM")
139:
140: collection_finder = "#{collection_class_name}.find_by_sql(\"#{options[:finder_sql]}\")"
141: collection_counter = "#{collection_class_name}.count_by_sql(\"#{counter_sql}\")"
142: else
143: collection_finder = "\#{collection_class_name}.find_all(\n\"\#{class_primary_key_name} = '\\\#{id}'\#{options[:conditions] ? \" AND \" + options[:conditions] : \"\"}\",\n\#{options[:order] ? \"\\\"\" + options[:order] + \"\\\"\" : \"nil\" }\n)\n"
144:
145: collection_counter = "#{collection_class_name}.count(\"#{class_primary_key_name} = '\#{id}'\")"
146: end
147:
148: has_collection_method(collection_name)
149: collection_count_method(collection_name, collection_counter)
150: collection_accessor_method_for_has_many(collection_name, collection_finder, class_primary_key_name)
151:
152: build_method("build_to_", collection_name, collection_class_name, class_primary_key_name)
153: create_method("create_in_", collection_name, collection_class_name, class_primary_key_name)
154:
155: # Can't use constrained finds with specialized finder SQL
156: unless options[:finder_sql]
157: find_in_collection_method(collection_name, collection_class_name, class_primary_key_name, options[:conditions])
158: end
159:
160: if options[:dependent]
161: module_eval "before_destroy '#{collection_name}.each { |o| o.destroy }'"
162: end
163:
164: if options[:exclusively_dependent]
165: module_eval "before_destroy \"#{collection_class_name}.delete_all(%(#{class_primary_key_name} = '\#{id}'))\""
166: end
167: end
Adds the following methods for retrival 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 has_manager?.
Example: An Account class declares has_one :beneficiary, which will add:
The declaration can also include an options hash to specialize the generated methods.
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/activerecord/lib/active_record/associations.rb, line 215
215: def has_one(association_id, options = {})
216: options.merge!({ :remote => true })
217: belongs_to(association_id, options)
218:
219: association_name, association_class_name, class_primary_key_name =
220: associate_identification(association_id, options[:class_name], options[:foreign_key])
221:
222: has_one_writer_method(association_name, class_primary_key_name)
223: build_method("build_", association_name, association_class_name, class_primary_key_name)
224: create_method("create_", association_name, association_class_name, class_primary_key_name)
225:
226: module_eval "before_destroy '#{association_name}.destroy if has_#{association_name}?'" if options[:dependent]
227: end
Adds the following methods for retrival 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 has_author?.
Example: An Post class declares has_one :author, which will add:
The declaration can also include an options hash to specialize the generated methods.
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/activerecord/lib/active_record/associations.rb, line 266
266: def belongs_to(association_id, options = {})
267: validate_options([ :class_name, :foreign_key, :remote, :conditions, :order, :dependent, :counter_cache ], options.keys)
268:
269: association_name, association_class_name, class_primary_key_name =
270: associate_identification(association_id, options[:class_name], options[:foreign_key])
271:
272: association_class_primary_key_name = options[:foreign_key] || association_class_name.gsub(/^.*::/, '').downcase + "_id"
273:
274: if options[:remote]
275: association_finder = "\#{association_class_name}.find_first(\n\"\#{class_primary_key_name} = '\\\#{id}'\#{options[:conditions] ? \" AND \" + options[:conditions] : \"\"}\",\n\#{options[:order] ? \"\\\"\" + options[:order] + \"\\\"\" : \"nil\" }\n)\n"
276: else
277: association_finder = options[:conditions] ?
278: "#{association_class_name}.find_on_conditions(#{association_class_primary_key_name}, \"#{options[:conditions]}\")" :
279: "#{association_class_name}.find(#{association_class_primary_key_name})"
280: end
281:
282: has_association_method(association_name)
283: association_reader_method(association_name, association_finder)
284: belongs_to_writer_method(association_name, association_class_primary_key_name)
285: association_comparison_method(association_name, association_class_name)
286:
287: if options[:counter_cache]
288: module_eval(
289: "after_create '#{association_class_name}.increment_counter(\"#{table_name + "_count"}\", #{association_class_primary_key_name})" +
290: " if has_#{association_name}?'"
291: )
292:
293: module_eval(
294: "before_destroy '#{association_class_name}.decrement_counter(\"#{table_name + "_count"}\", #{association_class_primary_key_name})" +
295: " if has_#{association_name}?'"
296: )
297: end
298: 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 "C". Adds the following methods for retrival and query. collection is replaced with the symbol passed as the first argument, so has_and_belongs_to_many :categories would add among others add_categories.
Example: An Developer class declares has_and_belongs_to_many :projects, which will add:
The declaration can also include an options hash to specialize the generated methods.
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"
# File vendor/activerecord/lib/active_record/associations.rb, line 356
356: def has_and_belongs_to_many(association_id, options = {})
357: validate_options([ :class_name, :table_name, :foreign_key, :association_foreign_key,
358: :join_table, :finder_sql, :delete_sql, :insert_sql ], options.keys)
359:
360: association_name, association_class_name, association_class_primary_key_name =
361: associate_identification(association_id, options[:class_name], options[:foreign_key])
362:
363:
364: association_foreign_key = options[:association_foreign_key] || association_class_name.downcase + "_id"
365:
366: association_table_name = options[:table_name] || table_name(association_class_name)
367: join_table = options[:join_table] ||
368: join_table_name(undecorated_table_name(self.to_s), undecorated_table_name(association_class_name))
369:
370: finder_sql =
371: options[:finder_sql] ||
372: "SELECT t.* FROM #{association_table_name} t, #{join_table} j " +
373: "WHERE t.\#{self.class.primary_key} = j.#{association_foreign_key} AND j.#{association_class_primary_key_name} = '\#{id}' ORDER BY t.\#{self.class.primary_key}"
374:
375: has_collection_method(association_name)
376: collection_accessor_method_for_has_and_belongs_to_many(association_name, "#{association_class_name}.find_by_sql(\"#{finder_sql}\")")
377: collection_count_method(association_name, "#{association_name}.length")
378:
379: add_association_relation(
380: association_name,
381: options[:insert_sql] ||
382: "INSERT INTO #{join_table} (#{association_class_primary_key_name}, #{association_foreign_key}) " +
383: "VALUES ('\#{id}', '\#{item.id}')"
384: )
385:
386: remove_association_relation(
387: association_name, association_foreign_key,
388: options[:delete_sql] || "DELETE FROM #{join_table} WHERE #{association_class_primary_key_name} = '\#{id}'"
389: )
390: end