Skip to content

Instantly share code, notes, and snippets.

@Aryk
Created June 14, 2011 05:05
Show Gist options
  • Save Aryk/1024359 to your computer and use it in GitHub Desktop.
Save Aryk/1024359 to your computer and use it in GitHub Desktop.
Nested has_many :through support
module NestedHasManyThrough
module Reflection # :nodoc:
def self.included(base)
base.send :alias_method_chain, :check_validity!, :nested_has_many_through
end
def check_validity_with_nested_has_many_through!
check_validity_without_nested_has_many_through!
rescue ActiveRecord::HasManyThroughSourceAssociationMacroError => e
# now we permit has many through to a :though source
raise e unless source_reflection.options[:through]
end
end
module Association
def self.included(base)
base.class_eval do
def construct_conditions
@nested_join_attributes ||= construct_nested_join_attributes
"#{@nested_join_attributes[:remote_key]} = #{@owner.quoted_id} #{@nested_join_attributes[:conditions]}"
end
def construct_joins(custom_joins = nil)
@nested_join_attributes ||= construct_nested_join_attributes
"#{@nested_join_attributes[:joins]} #{custom_joins}"
end
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.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, remote_table_alias)}" unless association_class.descends_from_active_record?
# 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
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment