Skip to content

Commit 94924dc

Browse files
committed
Simplify/fix implementation of default scopes
The previous implementation was necessary in order to support stuff like: class Post < ActiveRecord::Base default_scope where(published: true) scope :ordered, order("created_at") end If we didn't evaluate the default scope at the last possible moment before sending the SQL to the database, it would become impossible to do: Post.unscoped.ordered This is because the default scope would already be bound up in the "ordered" scope, and therefore wouldn't be removed by the "Post.unscoped" part. In 4.0, we have deprecated all "eager" forms of scopes. So now you must write: class Post < ActiveRecord::Base default_scope { where(published: true) } scope :ordered, -> { order("created_at") } end This prevents the default scope getting bound up inside the "ordered" scope, which means we can now have a simpler/better/more natural implementation of default scoping. A knock on effect is that some things that didn't work properly now do. For example it was previously impossible to use #except to remove a part of the default scope, since the default scope was evaluated after the call to #except.
1 parent e66c148 commit 94924dc

File tree

12 files changed

+31
-74
lines changed

12 files changed

+31
-74
lines changed

activerecord/lib/active_record/associations/association.rb

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -122,11 +122,7 @@ def klass
122122
# Can be overridden (i.e. in ThroughAssociation) to merge in other scopes (i.e. the
123123
# through association's scope)
124124
def target_scope
125-
all = klass.all
126-
scope = AssociationRelation.new(klass, klass.arel_table, self)
127-
scope.merge! all
128-
scope.default_scoped = all.default_scoped?
129-
scope
125+
AssociationRelation.new(klass, klass.arel_table, self).merge!(klass.all)
130126
end
131127

132128
# Loads the \target if needed and returns it.

activerecord/lib/active_record/associations/collection_proxy.rb

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ class CollectionProxy < Relation
3333
def initialize(klass, association) #:nodoc:
3434
@association = association
3535
super klass, klass.arel_table
36-
self.default_scoped = true
3736
merge! association.scope(nullify: false)
3837
end
3938

activerecord/lib/active_record/associations/preloader/association.rb

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,6 @@ def reflection_scope
9898

9999
def build_scope
100100
scope = klass.unscoped
101-
scope.default_scoped = true
102101

103102
values = reflection_scope.values
104103
preload_values = preload_scope.values
@@ -113,7 +112,7 @@ def build_scope
113112
scope.where!(klass.table_name => { reflection.type => model.base_class.sti_name })
114113
end
115114

116-
scope
115+
klass.default_scoped.merge(scope)
117116
end
118117
end
119118
end

activerecord/lib/active_record/associations/through_association.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ def target_scope
1515
scope = super
1616
chain[1..-1].each do |reflection|
1717
scope.merge!(
18-
reflection.klass.all.with_default_scope.
18+
reflection.klass.all.
1919
except(:select, :create_with, :includes, :preload, :joins, :eager_load)
2020
)
2121
end

activerecord/lib/active_record/relation.rb

Lines changed: 15 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,14 @@ class Relation
1717
include FinderMethods, Calculations, SpawnMethods, QueryMethods, Batches, Explain, Delegation
1818

1919
attr_reader :table, :klass, :loaded
20-
attr_accessor :default_scoped
2120
alias :model :klass
2221
alias :loaded? :loaded
23-
alias :default_scoped? :default_scoped
2422

2523
def initialize(klass, table, values = {})
26-
@klass = klass
27-
@table = table
28-
@values = values
29-
@loaded = false
30-
@default_scoped = false
24+
@klass = klass
25+
@table = table
26+
@values = values
27+
@loaded = false
3128
end
3229

3330
def initialize_copy(other)
@@ -313,7 +310,7 @@ def update_all(updates)
313310
stmt.table(table)
314311
stmt.key = table[primary_key]
315312

316-
if with_default_scope.joins_values.any?
313+
if joins_values.any?
317314
@klass.connection.join_to_update(stmt, arel)
318315
else
319316
stmt.take(arel.limit)
@@ -438,7 +435,7 @@ def delete_all(conditions = nil)
438435
stmt = Arel::DeleteManager.new(arel.engine)
439436
stmt.from(table)
440437

441-
if with_default_scope.joins_values.any?
438+
if joins_values.any?
442439
@klass.connection.join_to_delete(stmt, arel, table[primary_key])
443440
else
444441
stmt.wheres = arel.constraints
@@ -512,12 +509,11 @@ def to_sql
512509
# User.where(name: 'Oscar').where_values_hash
513510
# # => {name: "Oscar"}
514511
def where_values_hash
515-
scope = with_default_scope
516-
equalities = scope.where_values.grep(Arel::Nodes::Equality).find_all { |node|
512+
equalities = where_values.grep(Arel::Nodes::Equality).find_all { |node|
517513
node.left.relation.name == table_name
518514
}
519515

520-
binds = Hash[scope.bind_values.find_all(&:first).map { |column, v| [column.name, v] }]
516+
binds = Hash[bind_values.find_all(&:first).map { |column, v| [column.name, v] }]
521517
binds.merge!(Hash[bind_values.find_all(&:first).map { |column, v| [column.name, v] }])
522518

523519
Hash[equalities.map { |where|
@@ -565,16 +561,6 @@ def pretty_print(q)
565561
q.pp(self.to_a)
566562
end
567563

568-
def with_default_scope #:nodoc:
569-
if default_scoped? && default_scope = klass.send(:build_default_scope)
570-
default_scope = default_scope.merge(self)
571-
default_scope.default_scoped = false
572-
default_scope
573-
else
574-
self
575-
end
576-
end
577-
578564
# Returns true if relation is blank.
579565
def blank?
580566
to_a.blank?
@@ -594,22 +580,16 @@ def inspect
594580
private
595581

596582
def exec_queries
597-
default_scoped = with_default_scope
598-
599-
if default_scoped.equal?(self)
600-
@records = eager_loading? ? find_with_associations : @klass.find_by_sql(arel, bind_values)
583+
@records = eager_loading? ? find_with_associations : @klass.find_by_sql(arel, bind_values)
601584

602-
preload = preload_values
603-
preload += includes_values unless eager_loading?
604-
preload.each do |associations|
605-
ActiveRecord::Associations::Preloader.new(@records, associations).run
606-
end
607-
608-
@records.each { |record| record.readonly! } if readonly_value
609-
else
610-
@records = default_scoped.to_a
585+
preload = preload_values
586+
preload += includes_values unless eager_loading?
587+
preload.each do |associations|
588+
ActiveRecord::Associations::Preloader.new(@records, associations).run
611589
end
612590

591+
@records.each { |record| record.readonly! } if readonly_value
592+
613593
@loaded = true
614594
@records
615595
end

activerecord/lib/active_record/relation/calculations.rb

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -91,20 +91,14 @@ def sum(*args)
9191
#
9292
# Person.sum("2 * age")
9393
def calculate(operation, column_name, options = {})
94-
relation = with_default_scope
95-
9694
if column_name.is_a?(Symbol) && attribute_aliases.key?(column_name.to_s)
9795
column_name = attribute_aliases[column_name.to_s].to_sym
9896
end
9997

100-
if relation.equal?(self)
101-
if has_include?(column_name)
102-
construct_relation_for_association_calculations.calculate(operation, column_name, options)
103-
else
104-
perform_calculation(operation, column_name, options)
105-
end
98+
if has_include?(column_name)
99+
construct_relation_for_association_calculations.calculate(operation, column_name, options)
106100
else
107-
relation.calculate(operation, column_name, options)
101+
perform_calculation(operation, column_name, options)
108102
end
109103
end
110104

activerecord/lib/active_record/relation/finder_methods.rb

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,6 @@ def exists?(conditions = :none)
211211
relation = relation.where(table[primary_key].eq(conditions)) if conditions != :none
212212
end
213213

214-
relation = relation.with_default_scope
215214
connection.select_value(relation.arel, "#{name} Exists", relation.bind_values)
216215
end
217216

@@ -354,7 +353,7 @@ def find_first
354353
if loaded?
355354
@records.first
356355
else
357-
@first ||= find_first_with_limit(with_default_scope.order_values, 1).first
356+
@first ||= find_first_with_limit(order_values, 1).first
358357
end
359358
end
360359

activerecord/lib/active_record/relation/merger.rb

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,6 @@ class Merger # :nodoc:
4242
attr_reader :relation, :values, :other
4343

4444
def initialize(relation, other)
45-
if other.default_scoped? && other.klass != relation.klass
46-
other = other.with_default_scope
47-
end
48-
4945
@relation = relation
5046
@values = other.values
5147
@other = other

activerecord/lib/active_record/relation/query_methods.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -795,7 +795,7 @@ def reverse_order! # :nodoc:
795795

796796
# Returns the Arel object associated with the relation.
797797
def arel
798-
@arel ||= with_default_scope.build_arel
798+
@arel ||= build_arel
799799
end
800800

801801
# Like #arel, but ignores the default scope of the model.

activerecord/lib/active_record/relation/spawn_methods.rb

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,6 @@ def only(*onlies)
6565

6666
def relation_with(values) # :nodoc:
6767
result = Relation.new(klass, table, values)
68-
result.default_scoped = default_scoped
6968
result.extend(*extending_values) if extending_values.any?
7069
result
7170
end

activerecord/lib/active_record/scoping/named.rb

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -25,22 +25,18 @@ def all
2525
if current_scope
2626
current_scope.clone
2727
else
28-
scope = relation
29-
scope.default_scoped = true
30-
scope
28+
default_scoped
3129
end
3230
end
3331

32+
def default_scoped # :nodoc:
33+
relation.merge(build_default_scope)
34+
end
35+
3436
# Collects attributes from scopes that should be applied when creating
3537
# an AR instance for the particular class this is called on.
3638
def scope_attributes # :nodoc:
37-
if current_scope
38-
current_scope.scope_for_create
39-
else
40-
scope = relation
41-
scope.default_scoped = true
42-
scope.scope_for_create
43-
end
39+
all.scope_for_create
4440
end
4541

4642
# Are there default attributes associated with this scope?

activerecord/test/cases/scoping/default_scoping_test.rb

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -153,9 +153,8 @@ def test_unscope_with_limit_in_query
153153
end
154154

155155
def test_order_to_unscope_reordering
156-
expected = DeveloperOrderedBySalary.all.collect { |dev| [dev.name, dev.id] }
157-
received = DeveloperOrderedBySalary.order('salary DESC, name ASC').reverse_order.unscope(:order).collect { |dev| [dev.name, dev.id] }
158-
assert_equal expected, received
156+
scope = DeveloperOrderedBySalary.order('salary DESC, name ASC').reverse_order.unscope(:order)
157+
assert !(scope.to_sql =~ /order/i)
159158
end
160159

161160
def test_unscope_reverse_order

0 commit comments

Comments
 (0)