aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/lib/active_record/associations/join_dependency.rb
diff options
context:
space:
mode:
authorJon Leighton <j@jonathanleighton.com>2011-02-27 18:17:14 +0000
committerJon Leighton <j@jonathanleighton.com>2011-02-28 22:12:43 +0000
commitb171b9e73dcc6a89b1da652da61c5127fe605b51 (patch)
treef2e263b665fc72cb143df5aeef7ee2d02c821a62 /activerecord/lib/active_record/associations/join_dependency.rb
parentd90b4e2615e8048fdeffc6dffe3246704adee01f (diff)
downloadrails-b171b9e73dcc6a89b1da652da61c5127fe605b51.tar.gz
rails-b171b9e73dcc6a89b1da652da61c5127fe605b51.tar.bz2
rails-b171b9e73dcc6a89b1da652da61c5127fe605b51.zip
Move JoinDependency and friends from ActiveRecord::Associations::ClassMethods to just ActiveRecord::Associations
Diffstat (limited to 'activerecord/lib/active_record/associations/join_dependency.rb')
-rw-r--r--activerecord/lib/active_record/associations/join_dependency.rb231
1 files changed, 231 insertions, 0 deletions
diff --git a/activerecord/lib/active_record/associations/join_dependency.rb b/activerecord/lib/active_record/associations/join_dependency.rb
new file mode 100644
index 0000000000..c7c3cf521c
--- /dev/null
+++ b/activerecord/lib/active_record/associations/join_dependency.rb
@@ -0,0 +1,231 @@
+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, :table_aliases, :active_record
+
+ def initialize(base, associations, joins)
+ @active_record = base
+ @table_joins = joins
+ @join_parts = [JoinBase.new(base)]
+ @associations = {}
+ @reflections = []
+ @table_aliases = Hash.new do |h,name|
+ h[name] = count_aliases_from_table_joins(name.downcase)
+ end
+ @table_aliases[base.table_name] = 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 count_aliases_from_table_joins(name)
+ return 0 if Arel::Table === @table_joins
+
+ # quoted_name should be downcased as some database adapters (Oracle) return quoted name in uppercase
+ quoted_name = active_record.connection.quote_table_name(name).downcase
+
+ @table_joins.map { |join|
+ # Table names + table aliases
+ join.left.downcase.scan(
+ /join(?:\s+\w+)?\s+(\S+\s+)?#{quoted_name}\son/
+ ).size
+ }.sum
+ 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
+ return if row[join_part.aliased_primary_key].nil?
+ association = join_part.instantiate(row)
+ case macro
+ when :has_many, :has_and_belongs_to_many
+ other = record.association(join_part.reflection.name)
+ other.loaded!
+ other.target.push(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