aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/lib/active_record/associations/through_association.rb
diff options
context:
space:
mode:
Diffstat (limited to 'activerecord/lib/active_record/associations/through_association.rb')
-rw-r--r--activerecord/lib/active_record/associations/through_association.rb169
1 files changed, 169 insertions, 0 deletions
diff --git a/activerecord/lib/active_record/associations/through_association.rb b/activerecord/lib/active_record/associations/through_association.rb
new file mode 100644
index 0000000000..57718285f8
--- /dev/null
+++ b/activerecord/lib/active_record/associations/through_association.rb
@@ -0,0 +1,169 @@
+module ActiveRecord
+ # = Active Record Through Association
+ module Associations
+ module ThroughAssociation
+
+ def scoped
+ with_scope(@scope) do
+ @reflection.klass.scoped &
+ @reflection.through_reflection.klass.scoped
+ end
+ end
+
+ def stale_target?
+ if @target && @reflection.through_reflection.macro == :belongs_to && defined?(@through_foreign_key)
+ previous_key = @through_foreign_key.to_s
+ current_key = @owner.send(@reflection.through_reflection.primary_key_name).to_s
+
+ previous_key != current_key
+ else
+ false
+ end
+ end
+
+ protected
+
+ def construct_find_scope
+ {
+ :conditions => construct_conditions,
+ :joins => construct_joins,
+ :include => @reflection.options[:include] || @reflection.source_reflection.options[:include],
+ :select => construct_select,
+ :order => @reflection.options[:order],
+ :limit => @reflection.options[:limit],
+ :readonly => @reflection.options[:readonly]
+ }
+ end
+
+ # This scope affects the creation of the associated records (not the join records). At the
+ # moment we only support creating on a :through association when the source reflection is a
+ # belongs_to. Thus it's not necessary to set a foreign key on the associated record(s), so
+ # this scope has can legitimately be empty.
+ def construct_create_scope
+ { }
+ end
+
+ def aliased_through_table
+ name = @reflection.through_reflection.table_name
+
+ @reflection.table_name == name ?
+ @reflection.through_reflection.klass.arel_table.alias(name + "_join") :
+ @reflection.through_reflection.klass.arel_table
+ end
+
+ def construct_owner_conditions
+ super(aliased_through_table, @reflection.through_reflection)
+ end
+
+ def construct_select
+ @reflection.options[:select] ||
+ @reflection.options[:uniq] && "DISTINCT #{@reflection.quoted_table_name}.*"
+ end
+
+ def construct_joins
+ right = aliased_through_table
+ left = @reflection.klass.arel_table
+
+ conditions = []
+
+ if @reflection.source_reflection.macro == :belongs_to
+ reflection_primary_key = @reflection.source_reflection.options[:primary_key] ||
+ @reflection.klass.primary_key
+ source_primary_key = @reflection.source_reflection.primary_key_name
+ if @reflection.options[:source_type]
+ column = @reflection.source_reflection.options[:foreign_type]
+ conditions <<
+ right[column].eq(@reflection.options[:source_type])
+ end
+ else
+ reflection_primary_key = @reflection.source_reflection.primary_key_name
+ source_primary_key = @reflection.source_reflection.options[:primary_key] ||
+ @reflection.through_reflection.klass.primary_key
+ if @reflection.source_reflection.options[:as]
+ column = "#{@reflection.source_reflection.options[:as]}_type"
+ conditions <<
+ left[column].eq(@reflection.through_reflection.klass.name)
+ end
+ end
+
+ conditions <<
+ left[reflection_primary_key].eq(right[source_primary_key])
+
+ right.create_join(
+ right,
+ right.create_on(right.create_and(conditions)))
+ end
+
+ # Construct attributes for :through pointing to owner and associate.
+ def construct_join_attributes(associate)
+ # TODO: revisit this to allow it for deletion, supposing dependent option is supported
+ raise ActiveRecord::HasManyThroughCantAssociateThroughHasOneOrManyReflection.new(@owner, @reflection) if [:has_one, :has_many].include?(@reflection.source_reflection.macro)
+
+ join_attributes = {
+ @reflection.source_reflection.primary_key_name =>
+ associate.send(@reflection.source_reflection.association_primary_key)
+ }
+
+ if @reflection.options[:source_type]
+ join_attributes.merge!(@reflection.source_reflection.options[:foreign_type] => associate.class.base_class.name)
+ end
+
+ if @reflection.through_reflection.options[:conditions].is_a?(Hash)
+ join_attributes.merge!(@reflection.through_reflection.options[:conditions])
+ end
+
+ join_attributes
+ end
+
+ def conditions
+ @conditions = build_conditions unless defined?(@conditions)
+ @conditions
+ end
+
+ def build_conditions
+ association_conditions = @reflection.options[:conditions]
+ through_conditions = build_through_conditions
+ source_conditions = @reflection.source_reflection.options[:conditions]
+ uses_sti = !@reflection.through_reflection.klass.descends_from_active_record?
+
+ if association_conditions || through_conditions || source_conditions || uses_sti
+ all = []
+
+ [association_conditions, source_conditions].each do |conditions|
+ all << interpolate_sql(sanitize_sql(conditions)) if conditions
+ end
+
+ all << through_conditions if through_conditions
+ all << build_sti_condition if uses_sti
+
+ all.map { |sql| "(#{sql})" } * ' AND '
+ end
+ end
+
+ def build_through_conditions
+ conditions = @reflection.through_reflection.options[:conditions]
+ if conditions.is_a?(Hash)
+ interpolate_sql(@reflection.through_reflection.klass.send(:sanitize_sql, conditions)).gsub(
+ @reflection.quoted_table_name,
+ @reflection.through_reflection.quoted_table_name)
+ elsif conditions
+ interpolate_sql(sanitize_sql(conditions))
+ end
+ end
+
+ def build_sti_condition
+ @reflection.through_reflection.klass.send(:type_condition).to_sql
+ end
+
+ alias_method :sql_conditions, :conditions
+
+ def update_stale_state
+ construct_scope if stale_target?
+
+ if @reflection.through_reflection.macro == :belongs_to
+ @through_foreign_key = @owner.send(@reflection.through_reflection.primary_key_name)
+ end
+ end
+ end
+ end
+end