Skip to content

Commit

Permalink
Change has_many :through to use the :source option to specify the sou…
Browse files Browse the repository at this point in the history
…rce association. :class_name is now ignored. [Rick Olson]

git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@4022 5ecf4fe2-1ee6-0310-87b1-e25e094e27de
  • Loading branch information
technoweenie committed Mar 24, 2006
1 parent cb069ea commit 38bae0a
Show file tree
Hide file tree
Showing 7 changed files with 83 additions and 21 deletions.
13 changes: 13 additions & 0 deletions activerecord/CHANGELOG
@@ -1,5 +1,18 @@
*SVN* *SVN*


* Change has_many :through to use the :source option to specify the source association. :class_name is now ignored. [Rick Olson]

class Connection < ActiveRecord::Base
belongs_to :user
belongs_to :channel
end

class Channel < ActiveRecord::Base
has_many :connections
has_many :contacts, :through => :connections, :class_name => 'User' # OLD
has_many :contacts, :through => :connections, :source => :user # NEW
end

* Fixed DB2 adapter so nullable columns will be determines correctly now and quotes from column default values will be removed #4350 [contact@maik-schmidt.de] * Fixed DB2 adapter so nullable columns will be determines correctly now and quotes from column default values will be removed #4350 [contact@maik-schmidt.de]


* Allow overriding of find parameters in scoped has_many :through calls [Rick Olson] * Allow overriding of find parameters in scoped has_many :through calls [Rick Olson]
Expand Down
70 changes: 61 additions & 9 deletions activerecord/lib/active_record/associations.rb
Expand Up @@ -15,7 +15,7 @@ def initialize(reflection)
end end


def message def message
"Could not find the association '#{@reflection.options[:through]}' in model #{@reflection.klass}" "Could not find the association #{@reflection.options[:through].inspect} in model #{@reflection.klass}"
end end
end end


Expand All @@ -32,13 +32,15 @@ def message
end end


class HasManyThroughSourceAssociationNotFoundError < ActiveRecordError class HasManyThroughSourceAssociationNotFoundError < ActiveRecordError
def initialize(through_reflection, source_reflection_names) def initialize(reflection)
@through_reflection = through_reflection @reflection = reflection
@source_reflection_names = source_reflection_names @through_reflection = reflection.through_reflection
@source_reflection_names = reflection.source_reflection_names
@source_associations = reflection.through_reflection.klass.reflect_on_all_associations.collect { |a| a.name.inspect }
end end


def message def message
"Could not find the source associations #{@source_reflection_names.to_sentence} in model #{@through_reflection.klass}" "Could not find the source association(s) #{@source_reflection_names.collect(&:inspect).to_sentence :connector => 'or'} in model #{@through_reflection.klass}. Try 'has_many #{@reflection.name.inspect}, :through => #{@through_reflection.name.inspect}, :source => <name>'. Is it one of #{@source_associations.to_sentence :connector => 'or'}?"
end end
end end


Expand All @@ -48,7 +50,7 @@ def initialize(reflection)
end end


def message def message
"Can not eagerly load the polymorphic association '#{@reflection.name}'" "Can not eagerly load the polymorphic association #{@reflection.name.inspect}"
end end
end end


Expand Down Expand Up @@ -201,6 +203,46 @@ def clear_association_cache #:nodoc:
# has_many :people, :extend => FindOrCreateByNameExtension # has_many :people, :extend => FindOrCreateByNameExtension
# end # end
# #
# == Association Join Models
#
# Has Many associations can be configured with the :through option to use an explicit join model to retrieve the data. This
# operates similarly to a <tt>has_and_belongs_to_many</tt> association. The advantage is that you're able to add validations,
# callbacks, and extra attributes on the join model. Consider the following schema:
#
# class Author < ActiveRecord::Base
# has_many :authorships
# has_many :books, :through => :authorships
# end
#
# class Authorship < ActiveRecord::Base
# belongs_to :author
# belongs_to :book
# end
#
# @author = Author.find :first
# @author.authorships.collect { |a| a.book } # selects all books that the author's authorships belong to.
# @author.books # selects all books by using the Authorship join model
#
# You can also go through a has_many association on the join model:
#
# class Firm < ActiveRecord::Base
# has_many :clients
# has_many :invoices, :through => :clients
# end
#
# class Client < ActiveRecord::Base
# belongs_to :firm
# has_many :invoices
# end
#
# class Invoice < ActiveRecord::Base
# belongs_to :client
# end
#
# @firm = Firm.find :first
# @firm.clients.collect { |c| c.invoices }.flatten # select all invoices for all clients of the firm
# @firm.invoices # selects all invoices by going through the Client join model.
#
# == Caching # == Caching
# #
# All of the methods are built on a simple caching principle that will keep the result of the last query around unless specifically # All of the methods are built on a simple caching principle that will keep the result of the last query around unless specifically
Expand Down Expand Up @@ -263,7 +305,7 @@ def clear_association_cache #:nodoc:
# #
# It's currently not possible to use eager loading on multiple associations from the same table. Eager loading will not pull # It's currently not possible to use eager loading on multiple associations from the same table. Eager loading will not pull
# additional attributes on join tables, so "rich associations" with has_and_belongs_to_many is not a good fit for eager loading. # additional attributes on join tables, so "rich associations" with has_and_belongs_to_many is not a good fit for eager loading.
# #
# == Table Aliasing # == Table Aliasing
# #
# ActiveRecord uses table aliasing in the case that a table is referenced multiple times in a join. If a table is referenced only once, # ActiveRecord uses table aliasing in the case that a table is referenced multiple times in a join. If a table is referenced only once,
Expand Down Expand Up @@ -423,18 +465,28 @@ module ClassMethods
# * <tt>:offset</tt>: An integer determining the offset from where the rows should be fetched. So at 5, it would skip the first 4 rows. # * <tt>:offset</tt>: An integer determining the offset from where the rows should be fetched. So at 5, it would skip the first 4 rows.
# * <tt>:select</tt>: By default, this is * as in SELECT * FROM, but can be changed if you for example want to do a join, but not # * <tt>:select</tt>: 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. # include the joined columns.
# * <tt>:through</tt>: Specifies a Join Model to perform the query through. Options for <tt>:class_name</tt> and <tt>:foreign_key</tt>
# are ignored, as the association uses the source reflection. You can only use a <tt>:through</tt> query through a <tt>belongs_to</tt>
# or <tt>has_many</tt> association.
# * <tt>:source</tt>: 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. <tt>has_many :subscribers, :through => :subscriptions</tt> will look for either +:subscribers+ or
# +:subscriber+ on +Subscription+, unless a +:source+ is given.
# #
# Option examples: # Option examples:
# has_many :comments, :order => "posted_on" # has_many :comments, :order => "posted_on"
# has_many :comments, :include => :author # has_many :comments, :include => :author
# has_many :people, :class_name => "Person", :conditions => "deleted = 0", :order => "name" # has_many :people, :class_name => "Person", :conditions => "deleted = 0", :order => "name"
# has_many :tracks, :order => "position", :dependent => :destroy # has_many :tracks, :order => "position", :dependent => :destroy
# has_many :comments, :dependent => :nullify # has_many :comments, :dependent => :nullify
# has_many :subscribers, :through => :subscriptions, :source => :user
# has_many :subscribers, :class_name => "Person", :finder_sql => # has_many :subscribers, :class_name => "Person", :finder_sql =>
# 'SELECT DISTINCT people.* ' + # 'SELECT DISTINCT people.* ' +
# 'FROM people p, post_subscriptions ps ' + # 'FROM people p, post_subscriptions ps ' +
# 'WHERE ps.post_id = #{id} AND ps.person_id = p.id ' + # 'WHERE ps.post_id = #{id} AND ps.person_id = p.id ' +
# 'ORDER BY p.first_name' # 'ORDER BY p.first_name'
#
# Specifying the :through option
#
def has_many(association_id, options = {}, &extension) def has_many(association_id, options = {}, &extension)
reflection = create_has_many_reflection(association_id, options, &extension) reflection = create_has_many_reflection(association_id, options, &extension)


Expand Down Expand Up @@ -953,7 +1005,7 @@ def create_has_many_reflection(association_id, options, &extension)
:class_name, :table_name, :foreign_key, :class_name, :table_name, :foreign_key,
:exclusively_dependent, :dependent, :exclusively_dependent, :dependent,
:select, :conditions, :include, :order, :group, :limit, :offset, :select, :conditions, :include, :order, :group, :limit, :offset,
:as, :through, :as, :through, :source,
:finder_sql, :counter_sql, :finder_sql, :counter_sql,
:before_add, :after_add, :before_remove, :after_remove, :before_add, :after_add, :before_remove, :after_remove,
:extend :extend
Expand Down Expand Up @@ -1320,7 +1372,7 @@ def initialize(reflection, join_dependency, parent = nil)
end end


if reflection.macro == :has_and_belongs_to_many || (reflection.macro == :has_many && reflection.options[:through]) if reflection.macro == :has_and_belongs_to_many || (reflection.macro == :has_many && reflection.options[:through])
@aliased_join_table_name = reflection.macro == :has_and_belongs_to_many ? reflection.options[:join_table] : parent.active_record.reflect_on_association(reflection.options[:through]).klass.table_name @aliased_join_table_name = reflection.macro == :has_and_belongs_to_many ? reflection.options[:join_table] : reflection.through_reflection.klass.table_name
unless join_dependency.table_aliases[aliased_join_table_name].zero? unless join_dependency.table_aliases[aliased_join_table_name].zero?
@aliased_join_table_name = active_record.connection.table_alias_for "#{pluralize(reflection.name)}_#{parent_table_name}_join" @aliased_join_table_name = active_record.connection.table_alias_for "#{pluralize(reflection.name)}_#{parent_table_name}_join"
table_index = join_dependency.table_aliases[aliased_join_table_name] table_index = join_dependency.table_aliases[aliased_join_table_name]
Expand Down
Expand Up @@ -72,7 +72,7 @@ def construct_conditions
when :belongs_to, :has_many when :belongs_to, :has_many
"#{@reflection.through_reflection.table_name}.#{@reflection.through_reflection.primary_key_name} = #{@owner.quoted_id}" "#{@reflection.through_reflection.table_name}.#{@reflection.through_reflection.primary_key_name} = #{@owner.quoted_id}"
else else
raise ActiveRecordError, "Invalid source reflection macro :#{@reflection.source_reflection.macro} for has_many #{@reflection.name}, :through => #{@reflection.through_reflection.name}" raise ActiveRecordError, "Invalid source reflection macro :#{@reflection.source_reflection.macro} for has_many #{@reflection.name}, :through => #{@reflection.through_reflection.name}. Use :source to specify the source reflection."
end end
end end
conditions << " AND (#{sql_conditions})" if sql_conditions conditions << " AND (#{sql_conditions})" if sql_conditions
Expand Down
9 changes: 3 additions & 6 deletions activerecord/lib/active_record/reflection.rb
Expand Up @@ -144,14 +144,11 @@ def through_reflection
@through_reflection ||= options[:through] ? active_record.reflect_on_association(options[:through]) : false @through_reflection ||= options[:through] ? active_record.reflect_on_association(options[:through]) : false
end end


# Gets an array of possible :through reflection names # Gets an array of possible :through source reflection names
# #
# [singularized, pluralized] # [singularized, pluralized]
def source_reflection_names def source_reflection_names
@source_reflection_names ||= (options[:class_name] ? @source_reflection_names ||= (options[:source] ? [options[:source]] : [name.to_s.singularize, name]).collect { |n| n.to_sym }
[options[:class_name].underscore, options[:class_name].underscore.pluralize] :
[name.to_s.singularize, name]
).collect { |n| n.to_sym }
end end


# Gets the source of the through reflection. It checks both a singularized and pluralized form for :belongs_to or :has_many. # Gets the source of the through reflection. It checks both a singularized and pluralized form for :belongs_to or :has_many.
Expand All @@ -173,7 +170,7 @@ def check_validity!
end end


if source_reflection.nil? if source_reflection.nil?
raise HasManyThroughSourceAssociationNotFoundError.new(through_reflection, source_reflection_names) raise HasManyThroughSourceAssociationNotFoundError.new(self)
end end


if source_reflection.options[:polymorphic] if source_reflection.options[:polymorphic]
Expand Down
4 changes: 2 additions & 2 deletions activerecord/test/associations_cascaded_eager_loading_test.rb
Expand Up @@ -87,7 +87,7 @@ def test_eager_association_loading_with_belongs_to_sti
end end


def test_eager_association_loading_with_multiple_stis_and_order def test_eager_association_loading_with_multiple_stis_and_order
author = Author.find(:first, :include => { :posts => [ :special_comments , :very_special_comment ] }, :order => 'authors.name, special_comments.body, very_special_comments.body', :conditions => 'posts.id = 4') author = Author.find(:first, :include => { :posts => [ :special_comments , :very_special_comment ] }, :order => 'authors.name, comments.body, very_special_comments_posts.body', :conditions => 'posts.id = 4')
assert_equal authors(:david), author assert_equal authors(:david), author
assert_no_queries do assert_no_queries do
author.posts.first.special_comments author.posts.first.special_comments
Expand All @@ -96,7 +96,7 @@ def test_eager_association_loading_with_multiple_stis_and_order
end end


def test_eager_association_loading_of_stis_with_multiple_references def test_eager_association_loading_of_stis_with_multiple_references
authors = Author.find(:all, :include => { :posts => { :special_comments => { :post => [ :special_comments, :very_special_comment ] } } }, :order => 'special_comments.body, very_special_comments.body', :conditions => 'posts.id = 4') authors = Author.find(:all, :include => { :posts => { :special_comments => { :post => [ :special_comments, :very_special_comment ] } } }, :order => 'comments.body, very_special_comments_posts.body', :conditions => 'posts.id = 4')
assert_equal [authors(:david)], authors assert_equal [authors(:david)], authors
assert_no_queries do assert_no_queries do
authors.first.posts.first.special_comments.first.post.special_comments authors.first.posts.first.special_comments.first.post.special_comments
Expand Down
2 changes: 1 addition & 1 deletion activerecord/test/fixtures/author.rb
Expand Up @@ -4,7 +4,7 @@ class Author < ActiveRecord::Base
has_many :posts_with_categories, :include => :categories, :class_name => "Post" has_many :posts_with_categories, :include => :categories, :class_name => "Post"
has_many :posts_with_comments_and_categories, :include => [ :comments, :categories ], :order => "posts.id", :class_name => "Post" has_many :posts_with_comments_and_categories, :include => [ :comments, :categories ], :order => "posts.id", :class_name => "Post"
has_many :comments, :through => :posts has_many :comments, :through => :posts
has_many :funky_comments, :through => :posts, :class_name => 'Comment' has_many :funky_comments, :through => :posts, :source => :comments


has_many :special_posts, :class_name => "Post" has_many :special_posts, :class_name => "Post"
has_many :hello_posts, :class_name => "Post", :conditions=>"\#{aliased_table_name}.body = 'hello'" has_many :hello_posts, :class_name => "Post", :conditions=>"\#{aliased_table_name}.body = 'hello'"
Expand Down
4 changes: 2 additions & 2 deletions activerecord/test/fixtures/post.rb
Expand Up @@ -28,12 +28,12 @@ def add_joins_and_select
end end
end end


has_many :funky_tags, :through => :taggings, :class_name => 'Tag' has_many :funky_tags, :through => :taggings, :source => :tag
has_many :super_tags, :through => :taggings has_many :super_tags, :through => :taggings
has_one :tagging, :as => :taggable has_one :tagging, :as => :taggable


has_many :invalid_taggings, :as => :taggable, :class_name => "Tagging", :conditions => 'taggings.id < 0' has_many :invalid_taggings, :as => :taggable, :class_name => "Tagging", :conditions => 'taggings.id < 0'
has_many :invalid_tags, :through => :invalid_taggings, :class_name => "Tag" has_many :invalid_tags, :through => :invalid_taggings, :source => :tag


has_many :categorizations, :foreign_key => :category_id has_many :categorizations, :foreign_key => :category_id
has_many :authors, :through => :categorizations has_many :authors, :through => :categorizations
Expand Down

0 comments on commit 38bae0a

Please sign in to comment.