module ActiveRecord
module Associations
class JoinDependency # :nodoc:
autoload :JoinPart, 'active_record/associations/join_dependency/join_part'
autoload :JoinBase, 'active_record/associations/join_dependency/join_base'
autoload :JoinAssociation, 'active_record/associations/join_dependency/join_association'
attr_reader :join_parts, :reflections, :alias_tracker, :active_record
def initialize(base, associations, joins)
@active_record = base
@table_joins = joins
@join_parts = [JoinBase.new(base)]
@associations = {}
@reflections = []
@alias_tracker = AliasTracker.new(joins)
@alias_tracker.aliased_name_for(base.table_name) # Updates the count for base.table_name to 1
build(associations)
end
def graft(*associations)
associations.each do |association|
join_associations.detect {|a| association == a} ||
build(association.reflection.name, association.find_parent_in(self) || join_base, association.join_type)
end
self
end
def join_associations
join_parts.last(join_parts.length - 1)
end
def join_base
join_parts.first
end
def columns
join_parts.collect { |join_part|
table = join_part.aliased_table
join_part.column_names_with_alias.collect{ |column_name, aliased_name|
table[column_name].as Arel.sql(aliased_name)
}
}.flatten
end
def instantiate(rows)
primary_key = join_base.aliased_primary_key
parents = {}
records = rows.map { |model|
primary_id = model[primary_key]
parent = parents[primary_id] ||= join_base.instantiate(model)
construct(parent, @associations, join_associations, model)
parent
}.uniq
remove_duplicate_results!(active_record, records, @associations)
records
end
def remove_duplicate_results!(base, records, associations)
case associations
when Symbol, String
reflection = base.reflections[associations]
remove_uniq_by_reflection(reflection, records)
when Array
associations.each do |association|
remove_duplicate_results!(base, records, association)
end
when Hash
associations.keys.each do |name|
reflection = base.reflections[name]
remove_uniq_by_reflection(reflection, records)
parent_records = []
records.each do |record|
if descendant = record.send(reflection.name)
if reflection.collection?
parent_records.concat descendant.target.uniq
else
parent_records << descendant
end
end
end
remove_duplicate_results!(reflection.klass, parent_records, associations[name]) unless parent_records.empty?
end
end
end
protected
def cache_joined_association(association)
associations = []
parent = association.parent
while parent != join_base
associations.unshift(parent.reflection.name)
parent = parent.parent
end
ref = @associations
associations.each do |key|
ref = ref[key]
end
ref[association.reflection.name] ||= {}
end
def build(associations, parent = nil, join_type = Arel::InnerJoin)
parent ||= join_parts.last
case associations
when Symbol, String
reflection = parent.reflections[associations.to_s.intern] or
raise ConfigurationError, "Association named '#{ associations }' was not found; perhaps you misspelled it?"
unless join_association = find_join_association(reflection, parent)
@reflections << reflection
join_association = build_join_association(reflection, parent)
join_association.join_type = join_type
@join_parts << join_association
cache_joined_association(join_association)
end
join_association
when Array
associations.each do |association|
build(association, parent, join_type)
end
when Hash
associations.keys.sort_by { |a| a.to_s }.each do |name|
join_association = build(name, parent, join_type)
build(associations[name], join_association, join_type)
end
else
raise ConfigurationError, associations.inspect
end
end
def find_join_association(name_or_reflection, parent)
if String === name_or_reflection
name_or_reflection = name_or_reflection.to_sym
end
join_associations.detect { |j|
j.reflection == name_or_reflection && j.parent == parent
}
end
def remove_uniq_by_reflection(reflection, records)
if reflection && reflection.collection?
records.each { |record| record.send(reflection.name).target.uniq! }
end
end
def build_join_association(reflection, parent)
JoinAssociation.new(reflection, self, parent)
end
def construct(parent, associations, join_parts, row)
case associations
when Symbol, String
name = associations.to_s
join_part = join_parts.detect { |j|
j.reflection.name.to_s == name &&
j.parent_table_name == parent.class.table_name }
raise(ConfigurationError, "No such association") unless join_part
join_parts.delete(join_part)
construct_association(parent, join_part, row)
when Array
associations.each do |association|
construct(parent, association, join_parts, row)
end
when Hash
associations.sort_by { |k,_| k.to_s }.each do |association_name, assoc|
association = construct(parent, association_name, join_parts, row)
construct(association, assoc, join_parts, row) if association
end
else
raise ConfigurationError, associations.inspect
end
end
def construct_association(record, join_part, row)
return if record.id.to_s != join_part.parent.record_id(row).to_s
macro = join_part.reflection.macro
if macro == :has_one
return if record.association_cache.key?(join_part.reflection.name)
association = join_part.instantiate(row) unless row[join_part.aliased_primary_key].nil?
set_target_and_inverse(join_part, association, record)
else
association = join_part.instantiate(row) unless row[join_part.aliased_primary_key].nil?
case macro
when :has_many, :has_and_belongs_to_many
other = record.association(join_part.reflection.name)
other.loaded!
other.target.push(association) if association
other.set_inverse_instance(association)
when :belongs_to
set_target_and_inverse(join_part, association, record)
else
raise ConfigurationError, "unknown macro: #{join_part.reflection.macro}"
end
end
association
end
def set_target_and_inverse(join_part, association, record)
other = record.association(join_part.reflection.name)
other.target = association
other.set_inverse_instance(association)
end
end
end
end