Skip to content
This repository
Browse code

Construct an actual ActiveRecord::Relation object for the association…

… scope, rather than a hash which is passed to apply_finder_options. This allows more flexibility in how the scope is created, for example because scope.where(a, b) and scope.where(a).where(b) mean different things.
  • Loading branch information...
commit 770e6893b9f2aaaebe3de10576931dc7194451bc 1 parent 4411184
Jon Leighton authored January 06, 2011 tenderlove committed January 07, 2011
27  activerecord/lib/active_record/associations/association_collection.rb
@@ -333,23 +333,16 @@ def proxy_respond_to?(method, include_private = false)
333 333
 
334 334
       protected
335 335
 
336  
-        def finder_options
337  
-          {
338  
-            :conditions => construct_conditions,
339  
-            :select     => construct_select,
340  
-            :readonly   => @reflection.options[:readonly],
341  
-            :order      => @reflection.options[:order],
342  
-            :limit      => @reflection.options[:limit],
343  
-            :include    => @reflection.options[:include],
344  
-            :joins      => @reflection.options[:joins],
345  
-            :group      => @reflection.options[:group],
346  
-            :having     => @reflection.options[:having],
347  
-            :offset     => @reflection.options[:offset]
348  
-          }
349  
-        end
350  
-
351  
-        def construct_select
352  
-          @reflection.options[:select] ||
  336
+        def association_scope
  337
+          options = @reflection.options.slice(:order, :limit, :joins, :group, :having, :offset)
  338
+          super.apply_finder_options(options)
  339
+        end
  340
+
  341
+        def select_value
  342
+          super || uniq_select_value
  343
+        end
  344
+
  345
+        def uniq_select_value
353 346
           @reflection.options[:uniq] && "DISTINCT #{@reflection.quoted_table_name}.*"
354 347
         end
355 348
 
58  activerecord/lib/active_record/associations/association_proxy.rb
@@ -165,9 +165,7 @@ def send(method, *args)
165 165
       end
166 166
 
167 167
       def scoped
168  
-        target_scope.
169  
-          apply_finder_options(@finder_options).
170  
-          create_with(@creation_attributes)
  168
+        target_scope & @association_scope
171 169
       end
172 170
 
173 171
       protected
@@ -180,27 +178,32 @@ def sanitize_sql(sql, table_name = @reflection.klass.table_name)
180 178
           @reflection.klass.send(:sanitize_sql, sql, table_name)
181 179
         end
182 180
 
183  
-        # Construct the data used for the scope for this association
  181
+        # Construct the scope for this association.
184 182
         #
185  
-        # Note that we don't actually build the scope here, we just construct the options and
186  
-        # attributes. We must only build the scope when it's actually needed, because at that
187  
-        # point the call may be surrounded by scope.scoping { ... } or with_scope { ... } etc,
188  
-        # which affects the scope which actually gets built.
  183
+        # Note that the association_scope is merged into the targed_scope only when the
  184
+        # scoped method is called. This is because at that point the call may be surrounded
  185
+        # by scope.scoping { ... } or with_scope { ... } etc, which affects the scope which
  186
+        # actually gets built.
189 187
         def construct_scope
190  
-          if target_klass
191  
-            @finder_options      = finder_options
192  
-            @creation_attributes = creation_attributes
193  
-          end
  188
+          @association_scope = association_scope if target_klass
  189
+        end
  190
+
  191
+        def association_scope
  192
+          scope = target_klass.unscoped
  193
+          scope = scope.create_with(creation_attributes)
  194
+          scope = scope.apply_finder_options(@reflection.options.slice(:conditions, :readonly, :include))
  195
+          scope = scope.where(construct_owner_conditions)
  196
+          scope = scope.select(select_value) if select_value = self.select_value
  197
+          scope
194 198
         end
195 199
 
196  
-        # Implemented by subclasses
197  
-        def finder_options
198  
-          raise NotImplementedError
  200
+        def select_value
  201
+          @reflection.options[:select]
199 202
         end
200 203
 
201 204
         # Implemented by (some) subclasses
202 205
         def creation_attributes
203  
-          {}
  206
+          { }
204 207
         end
205 208
 
206 209
         def aliased_table
@@ -226,6 +229,29 @@ def target_scope
226 229
           target_klass.scoped
227 230
         end
228 231
 
  232
+        # Returns a hash linking the owner to the association represented by the reflection
  233
+        def construct_owner_attributes(reflection = @reflection)
  234
+          attributes = {}
  235
+          if reflection.macro == :belongs_to
  236
+            attributes[reflection.association_primary_key] = @owner[reflection.foreign_key]
  237
+          else
  238
+            attributes[reflection.foreign_key] = @owner[reflection.active_record_primary_key]
  239
+
  240
+            if reflection.options[:as]
  241
+              attributes["#{reflection.options[:as]}_type"] = @owner.class.base_class.name
  242
+            end
  243
+          end
  244
+          attributes
  245
+        end
  246
+
  247
+        # Builds an array of arel nodes from the owner attributes hash
  248
+        def construct_owner_conditions(table = aliased_table, reflection = @reflection)
  249
+          conditions = construct_owner_attributes(reflection).map do |attr, value|
  250
+            table[attr].eq(value)
  251
+          end
  252
+          table.create_and(conditions)
  253
+        end
  254
+
229 255
       private
230 256
         # Forwards any missing method call to the \target.
231 257
         def method_missing(method, *args)
17  activerecord/lib/active_record/associations/belongs_to_association.rb
@@ -58,23 +58,6 @@ def find_target
58 58
           scoped.first.tap { |record| set_inverse_instance(record) }
59 59
         end
60 60
 
61  
-        def finder_options
62  
-          {
63  
-            :conditions => construct_conditions,
64  
-            :select     => @reflection.options[:select],
65  
-            :include    => @reflection.options[:include],
66  
-            :readonly   => @reflection.options[:readonly]
67  
-          }
68  
-        end
69  
-
70  
-        def construct_conditions
71  
-          conditions = aliased_table[@reflection.association_primary_key].
72  
-                       eq(@owner[@reflection.foreign_key])
73  
-
74  
-          conditions = conditions.and(Arel.sql(sql_conditions)) if sql_conditions
75  
-          conditions
76  
-        end
77  
-
78 61
         def foreign_key_present?
79 62
           !@owner[@reflection.foreign_key].nil?
80 63
         end
16  activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb
@@ -88,14 +88,14 @@ def construct_owner_conditions
88 88
           super(join_table)
89 89
         end
90 90
 
91  
-        def finder_options
92  
-          super.merge(
93  
-            :joins    => construct_joins,
94  
-            :readonly => ambiguous_select?(@reflection.options[:select]),
95  
-            :select   => @reflection.options[:select] || [
96  
-                @reflection.klass.arel_table[Arel.star],
97  
-                join_table[Arel.star]]
98  
-          )
  91
+        def association_scope
  92
+          scope = super.joins(construct_joins)
  93
+          scope = scope.readonly if ambiguous_select?(@reflection.options[:select])
  94
+          scope
  95
+        end
  96
+
  97
+        def select_value
  98
+          super || [@reflection.klass.arel_table[Arel.star], join_table[Arel.star]]
99 99
         end
100 100
 
101 101
         # Join tables with additional columns on top of the two foreign keys must be considered
28  activerecord/lib/active_record/associations/has_association.rb
@@ -9,34 +9,6 @@ def set_owner_attributes(record)
9 9
             construct_owner_attributes.each { |key, value| record[key] = value }
10 10
           end
11 11
         end
12  
-
13  
-        # Returns a hash linking the owner to the association represented by the reflection
14  
-        def construct_owner_attributes(reflection = @reflection)
15  
-          attributes = {}
16  
-          if reflection.macro == :belongs_to
17  
-            attributes[reflection.association_primary_key] = @owner.send(reflection.foreign_key)
18  
-          else
19  
-            attributes[reflection.foreign_key] = @owner.send(reflection.active_record_primary_key)
20  
-
21  
-            if reflection.options[:as]
22  
-              attributes["#{reflection.options[:as]}_type"] = @owner.class.base_class.name
23  
-            end
24  
-          end
25  
-          attributes
26  
-        end
27  
-
28  
-        # Builds an array of arel nodes from the owner attributes hash
29  
-        def construct_owner_conditions(table = aliased_table, reflection = @reflection)
30  
-          construct_owner_attributes(reflection).map do |attr, value|
31  
-            table[attr].eq(value)
32  
-          end
33  
-        end
34  
-
35  
-        def construct_conditions
36  
-          conditions = construct_owner_conditions
37  
-          conditions << Arel.sql(sql_conditions) if sql_conditions
38  
-          aliased_table.create_and(conditions)
39  
-        end
40 12
     end
41 13
   end
42 14
 end
4  activerecord/lib/active_record/associations/has_many_association.rb
@@ -69,9 +69,7 @@ def delete_records(records)
69 69
           end
70 70
         end
71 71
 
72  
-        def creation_attributes
73  
-          construct_owner_attributes
74  
-        end
  72
+        alias creation_attributes construct_owner_attributes
75 73
     end
76 74
   end
77 75
 end
14  activerecord/lib/active_record/associations/has_one_association.rb
@@ -68,19 +68,11 @@ def find_target
68 68
           scoped.first.tap { |record| set_inverse_instance(record) }
69 69
         end
70 70
 
71  
-        def finder_options
72  
-          {
73  
-            :conditions => construct_conditions,
74  
-            :select     => @reflection.options[:select],
75  
-            :include    => @reflection.options[:include],
76  
-            :readonly   => @reflection.options[:readonly],
77  
-            :order      => @reflection.options[:order]
78  
-          }
  71
+        def association_scope
  72
+          super.order(@reflection.options[:order])
79 73
         end
80 74
 
81  
-        def creation_attributes
82  
-          construct_owner_attributes
83  
-        end
  75
+        alias creation_attributes construct_owner_attributes
84 76
 
85 77
         def new_record
86 78
           record = scoped.scoping { yield @reflection }
21  activerecord/lib/active_record/associations/through_association.rb
@@ -9,12 +9,12 @@ def target_scope
9 9
         super & @reflection.through_reflection.klass.scoped
10 10
       end
11 11
 
12  
-      def finder_options
13  
-        super.merge(
14  
-          :joins   => construct_joins,
15  
-          :include => @reflection.options[:include] ||
16  
-                      @reflection.source_reflection.options[:include]
17  
-        )
  12
+      def association_scope
  13
+        scope = super.joins(construct_joins).where(conditions)
  14
+        unless @reflection.options[:include]
  15
+          scope = scope.includes(@reflection.source_reflection.options[:include])
  16
+        end
  17
+        scope
18 18
       end
19 19
 
20 20
       # This scope affects the creation of the associated records (not the join records). At the
@@ -98,18 +98,13 @@ def conditions
98 98
       end
99 99
 
100 100
       def build_conditions
101  
-        association_conditions = @reflection.options[:conditions]
102 101
         through_conditions = build_through_conditions
103 102
         source_conditions = @reflection.source_reflection.options[:conditions]
104 103
         uses_sti = !@reflection.through_reflection.klass.descends_from_active_record?
105 104
 
106  
-        if association_conditions || through_conditions || source_conditions || uses_sti
  105
+        if through_conditions || source_conditions || uses_sti
107 106
           all = []
108  
-
109  
-          [association_conditions, source_conditions].each do |conditions|
110  
-            all << interpolate_sql(sanitize_sql(conditions)) if conditions
111  
-          end
112  
-
  107
+          all << interpolate_sql(sanitize_sql(source_conditions)) if source_conditions
113 108
           all << through_conditions  if through_conditions
114 109
           all << build_sti_condition if uses_sti
115 110
 
8  activerecord/test/cases/associations/has_one_associations_test.rb
@@ -227,14 +227,6 @@ def test_dependence_with_missing_association_and_nullify
227 227
     firm.destroy
228 228
   end
229 229
 
230  
-  def test_finding_with_interpolated_condition
231  
-    firm = Firm.find(:first)
232  
-    superior = firm.clients.create(:name => 'SuperiorCo')
233  
-    superior.rating = 10
234  
-    superior.save
235  
-    assert_equal 10, firm.clients_with_interpolated_conditions.first.rating
236  
-  end
237  
-
238 230
   def test_assignment_before_child_saved
239 231
     firm = Firm.find(1)
240 232
     firm.account = a = Account.new("credit_limit" => 1000)
4  activerecord/test/cases/reflection_test.rb
@@ -181,8 +181,8 @@ def test_association_reflection_in_modules
181 181
 
182 182
   def test_reflection_of_all_associations
183 183
     # FIXME these assertions bust a lot
184  
-    assert_equal 37, Firm.reflect_on_all_associations.size
185  
-    assert_equal 27, Firm.reflect_on_all_associations(:has_many).size
  184
+    assert_equal 36, Firm.reflect_on_all_associations.size
  185
+    assert_equal 26, Firm.reflect_on_all_associations(:has_many).size
186 186
     assert_equal 10, Firm.reflect_on_all_associations(:has_one).size
187 187
     assert_equal 0, Firm.reflect_on_all_associations(:belongs_to).size
188 188
   end
3  activerecord/test/cases/relation_scoping_test.rb
@@ -259,7 +259,8 @@ def test_should_maintain_default_scope_on_associations
259 259
   end
260 260
 
261 261
   def test_should_default_scope_on_associations_is_overriden_by_association_conditions
262  
-    assert_equal [], people(:michael).fixed_bad_references
  262
+    reference = references(:michael_unicyclist).becomes(BadReference)
  263
+    assert_equal [reference], people(:michael).fixed_bad_references
263 264
   end
264 265
 
265 266
   def test_should_maintain_default_scope_on_eager_loaded_associations
1  activerecord/test/models/company.rb
@@ -49,7 +49,6 @@ class Firm < Company
49 49
   has_many :exclusively_dependent_clients_of_firm, :foreign_key => "client_of", :class_name => "Client", :order => "id", :dependent => :delete_all
50 50
   has_many :limited_clients, :class_name => "Client", :limit => 1
51 51
   has_many :clients_like_ms, :conditions => "name = 'Microsoft'", :class_name => "Client", :order => "id"
52  
-  has_many :clients_with_interpolated_conditions, :class_name => "Client", :conditions => 'rating > #{rating}'
53 52
   has_many :clients_like_ms_with_hash_conditions, :conditions => { :name => 'Microsoft' }, :class_name => "Client", :order => "id"
54 53
   has_many :clients_using_sql, :class_name => "Client", :finder_sql => 'SELECT * FROM companies WHERE client_of = #{id}'
55 54
   has_many :clients_using_multiline_sql, :class_name => "Client", :finder_sql => '

0 notes on commit 770e689

Bruno Bornsztein

This line is giving me errors when trying to assign an object through a polymorphic belongs_to association:

class Comment
  belongs_to :commentable, :polymorphic => true
end

class User
  has_many :comments, :as => :commentable
end

comment = Comment.new
user = User.new
comment.commentable = user
comment.save!

-> NoMethodError: undefined method `unscoped' for Object:Class on line 185 of association_proxy.rb

It seems that target_klass is returning an Object instead of a User, but I don't understand the associations code well enough to figure out why. Any ideas?

Jon Leighton

Hi, thanks for the bug report. I'll look into it.

Jon Leighton

Hi,

I am unable to reproduce this bug, with the following test (the models are different because these models already exist in the AR test suite):

def test_assign_polymorphic
  tagging = Tagging.new
  post    = Post.new :title => "foo", :body => "foo"

  tagging.taggable = post
  tagging.save!

  assert_equal post, tagging.reload.taggable
end

If you still think the bug exists, could you please create a failing test case against the AR test suite and open a ticket on Lighthouse with the test attached? Or provide more details at least, because as far as I can see this works. You can assign the ticket to me.

Thanks,
Jon

Bruno Bornsztein

Hi, I think I may have tracked this down do a default value being set for the commentable_id (or, in your case, it would be the taggable_id). For some reason the migration specified a default of 0 for the commentable_id; removing that seems to have solved the problem.

Please sign in to comment.
Something went wrong with that request. Please try again.