diff options
-rw-r--r-- | activerecord/CHANGELOG | 38 | ||||
-rw-r--r-- | activerecord/lib/active_record/base.rb | 57 | ||||
-rw-r--r-- | activerecord/test/cases/relation_scoping_test.rb | 193 | ||||
-rw-r--r-- | activerecord/test/models/bulb.rb | 5 | ||||
-rw-r--r-- | activerecord/test/models/car.rb | 8 | ||||
-rw-r--r-- | activerecord/test/models/categorization.rb | 5 | ||||
-rw-r--r-- | activerecord/test/models/developer.rb | 81 | ||||
-rw-r--r-- | activerecord/test/models/post.rb | 5 | ||||
-rw-r--r-- | activerecord/test/models/reference.rb | 5 | ||||
-rw-r--r-- | activerecord/test/models/without_table.rb | 4 |
10 files changed, 134 insertions, 267 deletions
diff --git a/activerecord/CHANGELOG b/activerecord/CHANGELOG index 6b3d408720..9ff29f1155 100644 --- a/activerecord/CHANGELOG +++ b/activerecord/CHANGELOG @@ -1,5 +1,16 @@ *Rails 3.1.0 (unreleased)* +* default_scope can take a block, lambda, or any other object which responds to `call` for lazy + evaluation: + + default_scope { ... } + default_scope lambda { ... } + default_scope method(:foo) + + This feature was originally implemented by Tim Morgan, but was then removed in favour of + defining a 'default_scope' class method, but has now been added back in by Jon Leighton. + The relevant lighthouse ticket is #1812. + * Default scopes are now evaluated at the latest possible moment, to avoid problems where scopes would be created which would implicitly contain the default scope, which would then be impossible to get rid of via Model.unscoped. @@ -11,25 +22,34 @@ [Jon Leighton] -* Deprecated support for passing hashes and relations to 'default_scope'. Please create a class - method for your scope instead. For example, change this: +* Calling 'default_scope' multiple times in a class (including when a superclass calls + 'default_scope') is deprecated. The current behavior is that this will merge the default + scopes together: - class Post < ActiveRecord::Base + class Post < ActiveRecord::Base # Rails 3.1 default_scope where(:published => true) + default_scope where(:hidden => false) + # The default scope is now: where(:published => true, :hidden => false) end - To this: + In Rails 3.2, the behavior will be changed to overwrite previous scopes: + + class Post < ActiveRecord::Base # Rails 3.2 + default_scope where(:published => true) + default_scope where(:hidden => false) + # The default scope is now: where(:hidden => false) + end + + If you wish to merge default scopes in special ways, it is recommended to define your default + scope as a class method and use the standard techniques for sharing code (inheritance, mixins, + etc.): class Post < ActiveRecord::Base def self.default_scope - where(:published => true) + where(:published => true).where(:hidden => false) end end - Rationale: It will make the implementation simpler because we can simply use inheritance to - handle inheritance scenarios, rather than trying to make up our own rules about what should - happen when you call default_scope multiple times or in subclasses. - [Jon Leighton] * PostgreSQL adapter only supports PostgreSQL version 8.2 and higher. diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index 08a41e2d8b..9a01d793f9 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -1177,13 +1177,11 @@ MSG Thread.current[:"#{self}_current_scope"] = scope end - # Implement this method in your model to set a default scope for all operations on + # Use this macro in your model to set a default scope for all operations on # the model. # # class Person < ActiveRecord::Base - # def self.default_scope - # order('last_name, first_name') - # end + # default_scope order('last_name, first_name') # end # # Person.all # => SELECT * FROM people ORDER BY last_name, first_name @@ -1192,40 +1190,59 @@ MSG # applied while updating a record. # # class Article < ActiveRecord::Base - # def self.default_scope - # where(:published => true) - # end + # default_scope where(:published => true) # end # # Article.new.published # => true # Article.create.published # => true # - # === Deprecation warning - # - # There is an alternative syntax as follows: + # You can also use <tt>default_scope</tt> with a block, in order to have it lazily evaluated: # - # class Person < ActiveRecord::Base - # default_scope order('last_name, first_name') + # class Article < ActiveRecord::Base + # default_scope { where(:published_at => Time.now - 1.week) } # end # - # This is now deprecated and will be removed in Rails 3.2. + # (You can also pass any object which responds to <tt>call</tt> to the <tt>default_scope</tt> + # macro, and it will be called when building the default scope.) + # + # If you need to do more complex things with a default scope, you can alternatively + # define it as a class method: + # + # class Article < ActiveRecord::Base + # def self.default_scope + # # Should return a scope, you can call 'super' here etc. + # end + # end def default_scope(scope = {}) - ActiveSupport::Deprecation.warn <<-WARN -Passing a hash or scope to default_scope is deprecated and will be removed in Rails 3.2. You should create a class method for your scope instead. For example, change this: + if default_scopes.length != 0 + ActiveSupport::Deprecation.warn <<-WARN +Calling 'default_scope' multiple times in a class (including when a superclass calls 'default_scope') is deprecated. The current behavior is that this will merge the default scopes together: -class Post < ActiveRecord::Base +class Post < ActiveRecord::Base # Rails 3.1 default_scope where(:published => true) + default_scope where(:hidden => false) + # The default scope is now: where(:published => true, :hidden => false) end -To this: +In Rails 3.2, the behavior will be changed to overwrite previous scopes: + +class Post < ActiveRecord::Base # Rails 3.2 + default_scope where(:published => true) + default_scope where(:hidden => false) + # The default scope is now: where(:hidden => false) +end + +If you wish to merge default scopes in special ways, it is recommended to define your default scope as a class method and use the standard techniques for sharing code (inheritance, mixins, etc.): class Post < ActiveRecord::Base def self.default_scope - where(:published => true) + where(:published => true).where(:hidden => false) end end -WARN + WARN + end + scope = Proc.new if block_given? self.default_scopes = default_scopes.dup << scope end @@ -1238,6 +1255,8 @@ WARN default_scopes.inject(relation) do |default_scope, scope| if scope.is_a?(Hash) default_scope.apply_finder_options(scope) + elsif !scope.is_a?(Relation) && scope.respond_to?(:call) + default_scope.merge(scope.call) else default_scope.merge(scope) end diff --git a/activerecord/test/cases/relation_scoping_test.rb b/activerecord/test/cases/relation_scoping_test.rb index 5079aec9ba..2ed676fe69 100644 --- a/activerecord/test/cases/relation_scoping_test.rb +++ b/activerecord/test/cases/relation_scoping_test.rb @@ -308,6 +308,22 @@ class DefaultScopingTest < ActiveRecord::TestCase assert_equal expected, received end + def test_default_scope_as_class_method + assert_equal [developers(:david).becomes(ClassMethodDeveloperCalledDavid)], ClassMethodDeveloperCalledDavid.all + end + + def test_default_scope_with_lambda + assert_equal [developers(:david).becomes(LazyLambdaDeveloperCalledDavid)], LazyLambdaDeveloperCalledDavid.all + end + + def test_default_scope_with_block + assert_equal [developers(:david).becomes(LazyBlockDeveloperCalledDavid)], LazyBlockDeveloperCalledDavid.all + end + + def test_default_scope_with_callable + assert_equal [developers(:david).becomes(CallableDeveloperCalledDavid)], CallableDeveloperCalledDavid.all + end + def test_default_scope_is_unscoped_on_find assert_equal 1, DeveloperCalledDavid.count assert_equal 11, DeveloperCalledDavid.unscoped.count @@ -339,6 +355,12 @@ class DefaultScopingTest < ActiveRecord::TestCase assert_equal 50000, wheres[:salary] end + def test_default_scope_with_multiple_calls + wheres = MultiplePoorDeveloperCalledJamis.scoped.where_values_hash + assert_equal "Jamis", wheres[:name] + assert_equal 50000, wheres[:salary] + end + def test_method_scope expected = Developer.find(:all, :order => 'salary DESC, name DESC').collect { |dev| dev.salary } received = DeveloperOrderedBySalary.all_ordered_by_name.collect { |dev| dev.salary } @@ -434,175 +456,18 @@ class DefaultScopingTest < ActiveRecord::TestCase assert DeveloperCalledJamis.unscoped.poor.include?(developers(:david).becomes(DeveloperCalledJamis)) assert_equal 10, DeveloperCalledJamis.unscoped.poor.length end -end - -class DeprecatedDefaultScopingTest < ActiveRecord::TestCase - fixtures :developers, :posts - - def test_default_scope - expected = Developer.find(:all, :order => 'salary DESC').collect { |dev| dev.salary } - received = DeprecatedDeveloperOrderedBySalary.find(:all).collect { |dev| dev.salary } - assert_equal expected, received - end - - def test_default_scope_is_unscoped_on_find - assert_equal 1, DeprecatedDeveloperCalledDavid.count - assert_equal 11, DeprecatedDeveloperCalledDavid.unscoped.count - end - - def test_default_scope_is_unscoped_on_create - assert_nil DeprecatedDeveloperCalledJamis.unscoped.create!.name - end - - def test_default_scope_with_conditions_string - assert_equal Developer.find_all_by_name('David').map(&:id).sort, DeprecatedDeveloperCalledDavid.find(:all).map(&:id).sort - assert_equal nil, DeprecatedDeveloperCalledDavid.create!.name - end - def test_default_scope_with_conditions_hash - assert_equal Developer.find_all_by_name('Jamis').map(&:id).sort, DeprecatedDeveloperCalledJamis.find(:all).map(&:id).sort - assert_equal 'Jamis', DeprecatedDeveloperCalledJamis.create!.name - end + def test_multiple_default_scope_calls_are_deprecated + klass = Class.new(ActiveRecord::Base) - def test_default_scoping_with_threads - 2.times do - Thread.new { assert DeprecatedDeveloperOrderedBySalary.scoped.to_sql.include?('salary DESC') }.join + assert_not_deprecated do + klass.send(:default_scope, :foo => :bar) end - end - def test_default_scoping_with_inheritance - # Inherit a class having a default scope and define a new default scope - klass = Class.new(DeprecatedDeveloperOrderedBySalary) - ActiveSupport::Deprecation.silence { klass.send :default_scope, :limit => 1 } - - # Scopes added on children should append to parent scope - assert_equal [developers(:jamis).id], klass.all.map(&:id) - - # Parent should still have the original scope - assert_equal Developer.order('salary DESC').map(&:id), DeprecatedDeveloperOrderedBySalary.all.map(&:id) - end - - def test_default_scope_called_twice_merges_conditions - Developer.destroy_all - Developer.create!(:name => "David", :salary => 80000) - Developer.create!(:name => "David", :salary => 100000) - Developer.create!(:name => "Brian", :salary => 100000) - - klass = Class.new(Developer) - ActiveSupport::Deprecation.silence do - klass.__send__ :default_scope, :conditions => { :name => "David" } - klass.__send__ :default_scope, :conditions => { :salary => 100000 } - end - assert_equal 1, klass.count - assert_equal "David", klass.first.name - assert_equal 100000, klass.first.salary - end - - def test_default_scope_called_twice_in_different_place_merges_where_clause - Developer.destroy_all - Developer.create!(:name => "David", :salary => 80000) - Developer.create!(:name => "David", :salary => 100000) - Developer.create!(:name => "Brian", :salary => 100000) - - klass = Class.new(Developer) - ActiveSupport::Deprecation.silence do - klass.class_eval do - default_scope where("name = 'David'") - default_scope where("salary = 100000") - end + assert_deprecated do + klass.send(:default_scope, :foo => :bar) end - assert_equal 1, klass.count - assert_equal "David", klass.first.name - assert_equal 100000, klass.first.salary - end - - def test_method_scope - expected = Developer.find(:all, :order => 'salary DESC, name DESC').collect { |dev| dev.salary } - received = DeprecatedDeveloperOrderedBySalary.all_ordered_by_name.collect { |dev| dev.salary } - assert_equal expected, received - end - - def test_nested_scope - expected = Developer.find(:all, :order => 'salary DESC, name DESC').collect { |dev| dev.salary } - received = DeprecatedDeveloperOrderedBySalary.send(:with_scope, :find => { :order => 'name DESC'}) do - DeprecatedDeveloperOrderedBySalary.find(:all).collect { |dev| dev.salary } - end - assert_equal expected, received - end - - def test_scope_overwrites_default - expected = Developer.find(:all, :order => 'salary DESC, name DESC').collect { |dev| dev.name } - received = DeprecatedDeveloperOrderedBySalary.by_name.find(:all).collect { |dev| dev.name } - assert_equal expected, received - end - - def test_reorder_overrides_default_scope_order - expected = Developer.order('name DESC').collect { |dev| dev.name } - received = DeprecatedDeveloperOrderedBySalary.reorder('name DESC').collect { |dev| dev.name } - assert_equal expected, received - end - - def test_nested_exclusive_scope - expected = Developer.find(:all, :limit => 100).collect { |dev| dev.salary } - received = DeprecatedDeveloperOrderedBySalary.send(:with_exclusive_scope, :find => { :limit => 100 }) do - DeprecatedDeveloperOrderedBySalary.find(:all).collect { |dev| dev.salary } - end - assert_equal expected, received - end - - def test_order_in_default_scope_should_prevail - expected = Developer.find(:all, :order => 'salary desc').collect { |dev| dev.salary } - received = DeprecatedDeveloperOrderedBySalary.find(:all, :order => 'salary').collect { |dev| dev.salary } - assert_equal expected, received - end - - def test_default_scope_using_relation - posts = DeprecatedPostWithComment.scoped - assert_equal 2, posts.to_a.length - assert_equal posts(:thinking), posts.first - end - - def test_create_attribute_overwrites_default_scoping - assert_equal 'David', DeprecatedPoorDeveloperCalledJamis.create!(:name => 'David').name - assert_equal 200000, DeprecatedPoorDeveloperCalledJamis.create!(:name => 'David', :salary => 200000).salary - end - - def test_create_attribute_overwrites_default_values - assert_equal nil, DeprecatedPoorDeveloperCalledJamis.create!(:salary => nil).salary - assert_equal 50000, DeprecatedPoorDeveloperCalledJamis.create!(:name => 'David').salary - end - - def test_default_scope_attribute - jamis = DeprecatedPoorDeveloperCalledJamis.new(:name => 'David') - assert_equal 50000, jamis.salary - end - - def test_where_attribute - aaron = DeprecatedPoorDeveloperCalledJamis.where(:salary => 20).new(:name => 'Aaron') - assert_equal 20, aaron.salary - assert_equal 'Aaron', aaron.name - end - - def test_where_attribute_merge - aaron = DeprecatedPoorDeveloperCalledJamis.where(:name => 'foo').new(:name => 'Aaron') - assert_equal 'Aaron', aaron.name - end - - def test_create_with_merge - aaron = DeprecatedPoorDeveloperCalledJamis.create_with(:name => 'foo', :salary => 20).merge( - DeprecatedPoorDeveloperCalledJamis.create_with(:name => 'Aaron')).new - assert_equal 20, aaron.salary - assert_equal 'Aaron', aaron.name - - aaron = DeprecatedPoorDeveloperCalledJamis.create_with(:name => 'foo', :salary => 20). - create_with(:name => 'Aaron').new - assert_equal 20, aaron.salary - assert_equal 'Aaron', aaron.name - end - - def test_create_with_reset - jamis = DeprecatedPoorDeveloperCalledJamis.create_with(:name => 'Aaron').create_with(nil).new - assert_equal 'Jamis', jamis.name + assert_equal 2, klass.default_scopes.length end end diff --git a/activerecord/test/models/bulb.rb b/activerecord/test/models/bulb.rb index 89ee5416bf..c68d008c26 100644 --- a/activerecord/test/models/bulb.rb +++ b/activerecord/test/models/bulb.rb @@ -1,8 +1,5 @@ class Bulb < ActiveRecord::Base - def self.default_scope - where :name => 'defaulty' - end - + default_scope where(:name => 'defaulty') belongs_to :car attr_reader :scope_after_initialize diff --git a/activerecord/test/models/car.rb b/activerecord/test/models/car.rb index a978debb58..b036f0f5c9 100644 --- a/activerecord/test/models/car.rb +++ b/activerecord/test/models/car.rb @@ -15,13 +15,9 @@ class Car < ActiveRecord::Base end class CoolCar < Car - def self.default_scope - order 'name desc' - end + default_scope :order => 'name desc' end class FastCar < Car - def self.default_scope - order 'name desc' - end + default_scope :order => 'name desc' end diff --git a/activerecord/test/models/categorization.rb b/activerecord/test/models/categorization.rb index 39441e8610..4bd980e606 100644 --- a/activerecord/test/models/categorization.rb +++ b/activerecord/test/models/categorization.rb @@ -12,10 +12,7 @@ end class SpecialCategorization < ActiveRecord::Base self.table_name = 'categorizations' - - def self.default_scope - where(:special => true) - end + default_scope where(:special => true) belongs_to :author belongs_to :category diff --git a/activerecord/test/models/developer.rb b/activerecord/test/models/developer.rb index 10385ba899..10701dd6fd 100644 --- a/activerecord/test/models/developer.rb +++ b/activerecord/test/models/developer.rb @@ -1,3 +1,5 @@ +require 'ostruct' + module DeveloperProjectsAssociationExtension def find_most_recent find(:first, :order => "id DESC") @@ -86,10 +88,7 @@ end class DeveloperOrderedBySalary < ActiveRecord::Base self.table_name = 'developers' - - def self.default_scope - order('salary DESC') - end + default_scope :order => 'salary DESC' scope :by_name, order('name DESC') @@ -102,74 +101,56 @@ end class DeveloperCalledDavid < ActiveRecord::Base self.table_name = 'developers' - - def self.default_scope - where "name = 'David'" - end + default_scope where("name = 'David'") end -class DeveloperCalledJamis < ActiveRecord::Base +class LazyLambdaDeveloperCalledDavid < ActiveRecord::Base self.table_name = 'developers' + default_scope lambda { where(:name => 'David') } +end - def self.default_scope - where :name => 'Jamis' - end +class LazyBlockDeveloperCalledDavid < ActiveRecord::Base + self.table_name = 'developers' + default_scope { where(:name => 'David') } +end - scope :poor, where('salary < 150000') +class CallableDeveloperCalledDavid < ActiveRecord::Base + self.table_name = 'developers' + default_scope OpenStruct.new(:call => where(:name => 'David')) end -class AbstractDeveloperCalledJamis < ActiveRecord::Base - self.abstract_class = true +class ClassMethodDeveloperCalledDavid < ActiveRecord::Base + self.table_name = 'developers' def self.default_scope - where :name => 'Jamis' + where(:name => 'David') end end -class PoorDeveloperCalledJamis < ActiveRecord::Base +class DeveloperCalledJamis < ActiveRecord::Base self.table_name = 'developers' + default_scope where(:name => 'Jamis') + scope :poor, where('salary < 150000') +end - def self.default_scope - where :name => 'Jamis', :salary => 50000 - end +class PoorDeveloperCalledJamis < ActiveRecord::Base + self.table_name = 'developers' + default_scope where(:name => 'Jamis', :salary => 50000) end class InheritedPoorDeveloperCalledJamis < DeveloperCalledJamis self.table_name = 'developers' - def self.default_scope - super.where :salary => 50000 + ActiveSupport::Deprecation.silence do + default_scope where(:salary => 50000) end end -ActiveSupport::Deprecation.silence do - class DeprecatedDeveloperOrderedBySalary < ActiveRecord::Base - self.table_name = 'developers' - default_scope :order => 'salary DESC' - - def self.by_name - order('name DESC') - end - - def self.all_ordered_by_name - with_scope(:find => { :order => 'name DESC' }) do - find(:all) - end - end - end - - class DeprecatedDeveloperCalledDavid < ActiveRecord::Base - self.table_name = 'developers' - default_scope :conditions => "name = 'David'" - end - - class DeprecatedDeveloperCalledJamis < ActiveRecord::Base - self.table_name = 'developers' - default_scope :conditions => { :name => 'Jamis' } - end +class MultiplePoorDeveloperCalledJamis < ActiveRecord::Base + self.table_name = 'developers' + default_scope where(:name => 'Jamis') - class DeprecatedPoorDeveloperCalledJamis < ActiveRecord::Base - self.table_name = 'developers' - default_scope :conditions => { :name => 'Jamis', :salary => 50000 } + ActiveSupport::Deprecation.silence do + default_scope where(:salary => 50000) end end diff --git a/activerecord/test/models/post.rb b/activerecord/test/models/post.rb index 34cea60053..80296032bb 100644 --- a/activerecord/test/models/post.rb +++ b/activerecord/test/models/post.rb @@ -157,10 +157,7 @@ end class FirstPost < ActiveRecord::Base self.table_name = 'posts' - - def self.default_scope - where(:id => 1) - end + default_scope where(:id => 1) has_many :comments, :foreign_key => :post_id has_one :comment, :foreign_key => :post_id diff --git a/activerecord/test/models/reference.rb b/activerecord/test/models/reference.rb index 76c0a1a32e..c5af0b5d5f 100644 --- a/activerecord/test/models/reference.rb +++ b/activerecord/test/models/reference.rb @@ -19,8 +19,5 @@ end class BadReference < ActiveRecord::Base self.table_name = 'references' - - def self.default_scope - where :favourite => false - end + default_scope where(:favourite => false) end diff --git a/activerecord/test/models/without_table.rb b/activerecord/test/models/without_table.rb index 1a63d6ceb6..184ab1649e 100644 --- a/activerecord/test/models/without_table.rb +++ b/activerecord/test/models/without_table.rb @@ -1,5 +1,3 @@ class WithoutTable < ActiveRecord::Base - def self.default_scope - where(:published => true) - end + default_scope where(:published => true) end |