aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/lib/active_record/associations/nested_has_many_through.rb
blob: 2d03b8112851594fad75115dd3f6304b269afdb2 (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
147
148
149
150
151
152
153
154
155
156
module ActiveRecord
  module Associations
    module NestedHasManyThrough
      def self.included(klass)
        klass.alias_method_chain :construct_conditions, :nesting
        klass.alias_method_chain :construct_joins,      :nesting
      end

      def construct_joins_with_nesting(custom_joins = nil)
        if nested?
          @nested_join_attributes ||= construct_nested_join_attributes
          "#{construct_nested_join_attributes[:joins]} #{@reflection.options[:joins]} #{custom_joins}"
        else
          construct_joins_without_nesting(custom_joins)
        end
      end

      def construct_conditions_with_nesting
        if nested?
          @nested_join_attributes ||= construct_nested_join_attributes
          if @reflection.through_reflection && @reflection.through_reflection.macro == :belongs_to
            "#{@nested_join_attributes[:remote_key]} = #{belongs_to_quoted_key} #{@nested_join_attributes[:conditions]}"
          else
            "#{@nested_join_attributes[:remote_key]} = #{@owner.quoted_id} #{@nested_join_attributes[:conditions]}"
          end
        else
          construct_conditions_without_nesting
        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.macro == :has_one) && 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).to_sql}" if association_class.finder_needs_type_condition?
          # 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

        def belongs_to_quoted_key
          attribute = @reflection.through_reflection.primary_key_name
          column    = @owner.column_for_attribute attribute

          @owner.send(:quote_value, @owner.send(attribute), column)
        end

        def nested?
          through_source_reflection? || through_through_reflection?
        end

        def through_source_reflection?
          @reflection.source_reflection && @reflection.source_reflection.options[:through]
        end

        def through_through_reflection?
          @reflection.through_reflection && @reflection.through_reflection.options[:through]
        end
    end
  end
end