aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/lib/active_record/associations/through_association.rb
blob: e777d108a52a24fa502b4e1bd07fcc56dca13cb9 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
module ActiveRecord
  # = Active Record Through Association
  module Associations
    module ThroughAssociation

      def conditions
        @conditions = build_conditions unless defined?(@conditions)
        @conditions
      end

      alias_method :sql_conditions, :conditions

      protected

        def target_scope
          super & @reflection.through_reflection.klass.scoped
        end

        def association_scope
          scope = super.joins(construct_joins).where(conditions)
          unless @reflection.options[:include]
            scope = scope.includes(@reflection.source_reflection.options[:include])
          end
          scope
        end

      private

        # 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 creation_attributes
          { }
        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_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.foreign_key
            if @reflection.options[:source_type]
              column = @reflection.source_reflection.foreign_type
              conditions <<
                right[column].eq(@reflection.options[:source_type])
            end
          else
            reflection_primary_key = @reflection.source_reflection.foreign_key
            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.foreign_key =>
              associate.send(@reflection.source_reflection.association_primary_key)
          }

          if @reflection.options[:source_type]
            join_attributes.merge!(@reflection.source_reflection.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 build_conditions
          through_conditions = build_through_conditions
          source_conditions = @reflection.source_reflection.options[:conditions]
          uses_sti = !@reflection.through_reflection.klass.descends_from_active_record?

          if through_conditions || source_conditions || uses_sti
            all = []
            all << interpolate_sql(sanitize_sql(source_conditions)) if source_conditions
            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

        def stale_state
          if @reflection.through_reflection.macro == :belongs_to
            @owner[@reflection.through_reflection.foreign_key].to_s
          end
        end

        def foreign_key_present?
          @reflection.through_reflection.macro == :belongs_to &&
          !@owner[@reflection.through_reflection.foreign_key].nil?
        end
    end
  end
end