提交 b689834b 编写于 作者: B Bodaniel Jeanes

Initial nested_has_many_through support [#1152]

上级 14d2feee
......@@ -111,6 +111,7 @@ module Associations # :nodoc:
autoload :HasAndBelongsToManyAssociation, 'active_record/associations/has_and_belongs_to_many_association'
autoload :HasManyAssociation, 'active_record/associations/has_many_association'
autoload :HasManyThroughAssociation, 'active_record/associations/has_many_through_association'
autoload :NestedHasManyThroughAssociation, 'active_record/associations/nested_has_many_through_association'
autoload :HasOneAssociation, 'active_record/associations/has_one_association'
autoload :HasOneThroughAssociation, 'active_record/associations/has_one_through_association'
......
require "active_record/associations/through_association_scope"
require "active_record/associations/nested_has_many_through"
require 'active_support/core_ext/object/blank'
module ActiveRecord
......@@ -6,6 +7,7 @@ module ActiveRecord
module Associations
class HasManyThroughAssociation < HasManyAssociation #:nodoc:
include ThroughAssociationScope
include NestedHasManyThrough
alias_method :new, :build
......
module ActiveRecord
module Associations
module NestedHasManyThrough
def self.included(klass)
klass.alias_method_chain :construct_conditions, :nesting
klass.alias_method_chain :construct_joins, :nesting
end
def construct_joins_with_nesting(custom_joins = nil)
if nested?
@nested_join_attributes ||= construct_nested_join_attributes
"#{construct_nested_join_attributes[:joins]} #{@reflection.options[:joins]} #{custom_joins}"
else
construct_joins_without_nesting(custom_joins)
end
end
def construct_conditions_with_nesting
if nested?
@nested_join_attributes ||= construct_nested_join_attributes
if @reflection.through_reflection && @reflection.through_reflection.macro == :belongs_to
"#{@nested_join_attributes[:remote_key]} = #{belongs_to_quoted_key} #{@nested_join_attributes[:conditions]}"
else
"#{@nested_join_attributes[:remote_key]} = #{@owner.quoted_id} #{@nested_join_attributes[:conditions]}"
end
else
construct_conditions_without_nesting
end
end
protected
# Given any belongs_to or has_many (including has_many :through) association,
# return the essential components of a join corresponding to that association, namely:
#
# * <tt>:joins</tt>: any additional joins required to get from the association's table
# (reflection.table_name) to the table that's actually joining to the active record's table
# * <tt>:remote_key</tt>: the name of the key in the join table (qualified by table name) which will join
# to a field of the active record's table
# * <tt>:local_key</tt>: the name of the key in the local table (not qualified by table name) which will
# take part in the join
# * <tt>:conditions</tt>: any additional conditions (e.g. filtering by type for a polymorphic association,
# or a :conditions clause explicitly given in the association), including a leading AND
def construct_nested_join_attributes(reflection = @reflection, association_class = reflection.klass, table_ids = {association_class.table_name => 1})
if (reflection.macro == :has_many || reflection.macro == :has_one) && reflection.through_reflection
construct_has_many_through_attributes(reflection, table_ids)
else
construct_has_many_or_belongs_to_attributes(reflection, association_class, table_ids)
end
end
def construct_has_many_through_attributes(reflection, table_ids)
# Construct the join components of the source association, so that we have a path from
# the eventual target table of the association up to the table named in :through, and
# all tables involved are allocated table IDs.
source_attrs = construct_nested_join_attributes(reflection.source_reflection, reflection.klass, table_ids)
# Determine the alias of the :through table; this will be the last table assigned
# when constructing the source join components above.
through_table_alias = through_table_name = reflection.through_reflection.table_name
through_table_alias += "_#{table_ids[through_table_name]}" unless table_ids[through_table_name] == 1
# Construct the join components of the through association, so that we have a path to
# the active record's table.
through_attrs = construct_nested_join_attributes(reflection.through_reflection, reflection.through_reflection.klass, table_ids)
# Any subsequent joins / filters on owner attributes will act on the through association,
# so that's what we return for the conditions/keys of the overall association.
conditions = through_attrs[:conditions]
conditions += " AND #{interpolate_sql(reflection.klass.send(:sanitize_sql, reflection.options[:conditions]))}" if reflection.options[:conditions]
{
:joins => "%s INNER JOIN %s ON ( %s = %s.%s %s) %s %s" % [
source_attrs[:joins],
through_table_name == through_table_alias ? through_table_name : "#{through_table_name} #{through_table_alias}",
source_attrs[:remote_key],
through_table_alias, source_attrs[:local_key],
source_attrs[:conditions],
through_attrs[:joins],
reflection.options[:joins]
],
:remote_key => through_attrs[:remote_key],
:local_key => through_attrs[:local_key],
:conditions => conditions
}
end
# reflection is not has_many :through; it's a standard has_many / belongs_to instead
# TODO: see if we can defer to rails code here a bit more
def construct_has_many_or_belongs_to_attributes(reflection, association_class, table_ids)
# Determine the alias used for remote_table_name, if any. In all cases this will already
# have been assigned an ID in table_ids (either through being involved in a previous join,
# or - if it's the first table in the query - as the default value of table_ids)
remote_table_alias = remote_table_name = association_class.table_name
remote_table_alias += "_#{table_ids[remote_table_name]}" unless table_ids[remote_table_name] == 1
# Assign a new alias for the local table.
local_table_alias = local_table_name = reflection.active_record.table_name
if table_ids[local_table_name]
table_id = table_ids[local_table_name] += 1
local_table_alias += "_#{table_id}"
else
table_ids[local_table_name] = 1
end
conditions = ''
# Add type_condition, if applicable
conditions += " AND #{association_class.send(:type_condition).to_sql}" if association_class.finder_needs_type_condition?
# Add custom conditions
conditions += " AND (#{interpolate_sql(association_class.send(:sanitize_sql, reflection.options[:conditions]))})" if reflection.options[:conditions]
if reflection.macro == :belongs_to
if reflection.options[:polymorphic]
conditions += " AND #{local_table_alias}.#{reflection.options[:foreign_type]} = #{reflection.active_record.quote_value(association_class.base_class.name.to_s)}"
end
{
:joins => reflection.options[:joins],
:remote_key => "#{remote_table_alias}.#{association_class.primary_key}",
:local_key => reflection.primary_key_name,
:conditions => conditions
}
else
# Association is has_many (without :through)
if reflection.options[:as]
conditions += " AND #{remote_table_alias}.#{reflection.options[:as]}_type = #{reflection.active_record.quote_value(reflection.active_record.base_class.name.to_s)}"
end
{
:joins => "#{reflection.options[:joins]}",
:remote_key => "#{remote_table_alias}.#{reflection.primary_key_name}",
:local_key => reflection.klass.primary_key,
:conditions => conditions
}
end
end
def belongs_to_quoted_key
attribute = @reflection.through_reflection.primary_key_name
column = @owner.column_for_attribute attribute
@owner.send(:quote_value, @owner.send(attribute), column)
end
def nested?
through_source_reflection? || through_through_reflection?
end
def through_source_reflection?
@reflection.source_reflection && @reflection.source_reflection.options[:through]
end
def through_through_reflection?
@reflection.through_reflection && @reflection.through_reflection.options[:through]
end
end
end
end
......@@ -378,9 +378,9 @@ def check_validity!
raise HasManyThroughAssociationPolymorphicError.new(active_record.name, self, source_reflection)
end
unless [:belongs_to, :has_many, :has_one].include?(source_reflection.macro) && source_reflection.options[:through].nil?
raise HasManyThroughSourceAssociationMacroError.new(self)
end
# unless [:belongs_to, :has_many, :has_one].include?(source_reflection.macro) && source_reflection.options[:through].nil?
# raise HasManyThroughSourceAssociationMacroError.new(self)
# end
check_validity_of_inverse!
end
......
......@@ -394,14 +394,6 @@ def test_include_has_many_through_polymorphic_has_many
end
end
def test_has_many_through_has_many_through
assert_raise(ActiveRecord::HasManyThroughSourceAssociationMacroError) { authors(:david).tags }
end
def test_has_many_through_habtm
assert_raise(ActiveRecord::HasManyThroughSourceAssociationMacroError) { authors(:david).post_categories }
end
def test_eager_load_has_many_through_has_many
author = Author.find :first, :conditions => ['name = ?', 'David'], :include => :comments, :order => 'comments.id'
SpecialComment.new; VerySpecialComment.new
......
require "cases/helper"
require 'models/author'
require 'models/post'
require 'models/person'
require 'models/reference'
require 'models/job'
require 'models/reader'
require 'models/comment'
require 'models/tag'
require 'models/tagging'
require 'models/owner'
require 'models/pet'
require 'models/toy'
require 'models/contract'
require 'models/company'
require 'models/developer'
require 'models/subscriber'
require 'models/book'
require 'models/subscription'
class NestedHasManyThroughAssociationsTest < ActiveRecord::TestCase
fixtures :authors, :books, :posts, :subscriptions, :subscribers, :tags, :taggings
def test_has_many_through_a_has_many_through_association_on_source_reflection
author = authors(:david)
assert_equal [tags(:general), tags(:general)], author.tags
end
def test_has_many_through_a_has_many_through_association_on_through_reflection
author = authors(:david)
assert_equal [subscribers(:first), subscribers(:second), subscribers(:second)], author.subscribers
end
def test_distinct_has_many_through_a_has_many_through_association_on_source_reflection
author = authors(:david)
assert_equal [tags(:general)], author.distinct_tags
end
def test_distinct_has_many_through_a_has_many_through_association_on_through_reflection
author = authors(:david)
assert_equal [subscribers(:first), subscribers(:second)], author.distinct_subscribers
end
end
\ No newline at end of file
awdr:
author_id: 1
id: 1
name: "Agile Web Development with Rails"
rfr:
author_id: 1
id: 2
name: "Ruby for Rails"
......@@ -83,14 +83,20 @@ def testing_proxy_target
has_many :author_favorites
has_many :favorite_authors, :through => :author_favorites, :order => 'name'
has_many :tagging, :through => :posts # through polymorphic has_one
has_many :taggings, :through => :posts, :source => :taggings # through polymorphic has_many
has_many :tags, :through => :posts # through has_many :through
has_many :tagging, :through => :posts # through polymorphic has_one
has_many :taggings, :through => :posts, :source => :taggings # through polymorphic has_many
has_many :tags, :through => :posts # through has_many :through (on source reflection + polymorphic)
has_many :distinct_tags, :through => :posts, :source => :tags, :select => "DISTINCT tags.*", :order => "tags.name"
has_many :post_categories, :through => :posts, :source => :categories
has_many :books
has_many :subscriptions, :through => :books
has_many :subscribers, :through => :subscriptions # through has_many :through (on through reflection)
has_many :distinct_subscribers, :through => :subscriptions, :source => :subscriber, :select => "DISTINCT subscribers.*", :order => "subscribers.nick"
has_one :essay, :primary_key => :name, :as => :writer
belongs_to :author_address, :dependent => :destroy
belongs_to :author_address, :dependent => :destroy
belongs_to :author_address_extra, :dependent => :delete, :class_name => "AuthorAddress"
scope :relation_include_posts, includes(:posts)
......
class Book < ActiveRecord::Base
has_many :authors
has_many :citations, :foreign_key => 'book1_id'
has_many :references, :through => :citations, :source => :reference_of, :uniq => true
......
......@@ -71,6 +71,7 @@ def create_table(*args, &block)
end
create_table :books, :force => true do |t|
t.integer :author_id
t.column :name, :string
end
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册