Skip to content

Commit 770e689

Browse files
jonleightontenderlove
authored andcommitted
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.
1 parent 4411184 commit 770e689

12 files changed

+76
-125
lines changed

activerecord/lib/active_record/associations/association_collection.rb

Lines changed: 10 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -333,23 +333,16 @@ def proxy_respond_to?(method, include_private = false)
333333

334334
protected
335335

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
353346
@reflection.options[:uniq] && "DISTINCT #{@reflection.quoted_table_name}.*"
354347
end
355348

activerecord/lib/active_record/associations/association_proxy.rb

Lines changed: 42 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -165,9 +165,7 @@ def send(method, *args)
165165
end
166166

167167
def scoped
168-
target_scope.
169-
apply_finder_options(@finder_options).
170-
create_with(@creation_attributes)
168+
target_scope & @association_scope
171169
end
172170

173171
protected
@@ -180,27 +178,32 @@ def sanitize_sql(sql, table_name = @reflection.klass.table_name)
180178
@reflection.klass.send(:sanitize_sql, sql, table_name)
181179
end
182180

183-
# Construct the data used for the scope for this association
181+
# Construct the scope for this association.
184182
#
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.
189187
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
194198
end
195199

196-
# Implemented by subclasses
197-
def finder_options
198-
raise NotImplementedError
200+
def select_value
201+
@reflection.options[:select]
199202
end
200203

201204
# Implemented by (some) subclasses
202205
def creation_attributes
203-
{}
206+
{ }
204207
end
205208

206209
def aliased_table
@@ -226,6 +229,29 @@ def target_scope
226229
target_klass.scoped
227230
end
228231

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+
229255
private
230256
# Forwards any missing method call to the \target.
231257
def method_missing(method, *args)

activerecord/lib/active_record/associations/belongs_to_association.rb

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -58,23 +58,6 @@ def find_target
5858
scoped.first.tap { |record| set_inverse_instance(record) }
5959
end
6060

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-
7861
def foreign_key_present?
7962
!@owner[@reflection.foreign_key].nil?
8063
end

activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -88,14 +88,14 @@ def construct_owner_conditions
8888
super(join_table)
8989
end
9090

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]]
9999
end
100100

101101
# Join tables with additional columns on top of the two foreign keys must be considered

activerecord/lib/active_record/associations/has_association.rb

Lines changed: 0 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -9,34 +9,6 @@ def set_owner_attributes(record)
99
construct_owner_attributes.each { |key, value| record[key] = value }
1010
end
1111
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
4012
end
4113
end
4214
end

activerecord/lib/active_record/associations/has_many_association.rb

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,9 +69,7 @@ def delete_records(records)
6969
end
7070
end
7171

72-
def creation_attributes
73-
construct_owner_attributes
74-
end
72+
alias creation_attributes construct_owner_attributes
7573
end
7674
end
7775
end

activerecord/lib/active_record/associations/has_one_association.rb

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -68,19 +68,11 @@ def find_target
6868
scoped.first.tap { |record| set_inverse_instance(record) }
6969
end
7070

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])
7973
end
8074

81-
def creation_attributes
82-
construct_owner_attributes
83-
end
75+
alias creation_attributes construct_owner_attributes
8476

8577
def new_record
8678
record = scoped.scoping { yield @reflection }

activerecord/lib/active_record/associations/through_association.rb

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,12 @@ def target_scope
99
super & @reflection.through_reflection.klass.scoped
1010
end
1111

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
1818
end
1919

2020
# This scope affects the creation of the associated records (not the join records). At the
@@ -98,18 +98,13 @@ def conditions
9898
end
9999

100100
def build_conditions
101-
association_conditions = @reflection.options[:conditions]
102101
through_conditions = build_through_conditions
103102
source_conditions = @reflection.source_reflection.options[:conditions]
104103
uses_sti = !@reflection.through_reflection.klass.descends_from_active_record?
105104

106-
if association_conditions || through_conditions || source_conditions || uses_sti
105+
if through_conditions || source_conditions || uses_sti
107106
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
113108
all << through_conditions if through_conditions
114109
all << build_sti_condition if uses_sti
115110

activerecord/test/cases/associations/has_one_associations_test.rb

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -227,14 +227,6 @@ def test_dependence_with_missing_association_and_nullify
227227
firm.destroy
228228
end
229229

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-
238230
def test_assignment_before_child_saved
239231
firm = Firm.find(1)
240232
firm.account = a = Account.new("credit_limit" => 1000)

activerecord/test/cases/reflection_test.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -181,8 +181,8 @@ def test_association_reflection_in_modules
181181

182182
def test_reflection_of_all_associations
183183
# 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
186186
assert_equal 10, Firm.reflect_on_all_associations(:has_one).size
187187
assert_equal 0, Firm.reflect_on_all_associations(:belongs_to).size
188188
end

activerecord/test/cases/relation_scoping_test.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -259,7 +259,8 @@ def test_should_maintain_default_scope_on_associations
259259
end
260260

261261
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
263264
end
264265

265266
def test_should_maintain_default_scope_on_eager_loaded_associations

activerecord/test/models/company.rb

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,6 @@ class Firm < Company
4949
has_many :exclusively_dependent_clients_of_firm, :foreign_key => "client_of", :class_name => "Client", :order => "id", :dependent => :delete_all
5050
has_many :limited_clients, :class_name => "Client", :limit => 1
5151
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}'
5352
has_many :clients_like_ms_with_hash_conditions, :conditions => { :name => 'Microsoft' }, :class_name => "Client", :order => "id"
5453
has_many :clients_using_sql, :class_name => "Client", :finder_sql => 'SELECT * FROM companies WHERE client_of = #{id}'
5554
has_many :clients_using_multiline_sql, :class_name => "Client", :finder_sql => '

0 commit comments

Comments
 (0)