diff options
Diffstat (limited to 'activerecord/lib/active_record/scoping')
-rw-r--r-- | activerecord/lib/active_record/scoping/default.rb | 44 | ||||
-rw-r--r-- | activerecord/lib/active_record/scoping/named.rb | 66 |
2 files changed, 50 insertions, 60 deletions
diff --git a/activerecord/lib/active_record/scoping/default.rb b/activerecord/lib/active_record/scoping/default.rb index 5f05d146f2..a2a85d4b96 100644 --- a/activerecord/lib/active_record/scoping/default.rb +++ b/activerecord/lib/active_record/scoping/default.rb @@ -1,4 +1,3 @@ -require 'active_support/concern' module ActiveRecord module Scoping @@ -7,7 +6,7 @@ module ActiveRecord included do # Stores the default scope for the class - config_attribute :default_scopes + class_attribute :default_scopes, instance_writer: false self.default_scopes = [] end @@ -30,14 +29,14 @@ module ActiveRecord # Post.limit(10) # Fires "SELECT * FROM posts LIMIT 10" # } # - # It is recommended to use the block form of unscoped because chaining - # unscoped with <tt>scope</tt> does not work. Assuming that + # It is recommended that you use the block form of unscoped because + # chaining unscoped with <tt>scope</tt> does not work. Assuming that # <tt>published</tt> is a <tt>scope</tt>, the following two statements - # are equal: the default_scope is applied on both. + # are equal: the <tt>default_scope</tt> is applied on both. # # Post.unscoped.published # Post.published - def unscoped #:nodoc: + def unscoped block_given? ? relation.scoping { yield } : relation end @@ -51,7 +50,7 @@ module ActiveRecord # the model. # # class Article < ActiveRecord::Base - # default_scope where(:published => true) + # default_scope { where(:published => true) } # end # # Article.all # => SELECT * FROM articles WHERE published = true @@ -62,12 +61,6 @@ module ActiveRecord # Article.new.published # => true # Article.create.published # => true # - # You can also use <tt>default_scope</tt> with a block, in order to have it lazily evaluated: - # - # class Article < ActiveRecord::Base - # default_scope { where(:published_at => Time.now - 1.week) } - # end - # # (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.) # @@ -75,8 +68,8 @@ module ActiveRecord # be merged together: # # class Article < ActiveRecord::Base - # default_scope where(:published => true) - # default_scope where(:rating => 'G') + # default_scope { where(:published => true) } + # default_scope { where(:rating => 'G') } # end # # Article.all # => SELECT * FROM articles WHERE published = true AND rating = 'G' @@ -92,21 +85,30 @@ module ActiveRecord # # Should return a scope, you can call 'super' here etc. # end # end - def default_scope(scope = {}) + def default_scope(scope = nil) scope = Proc.new if block_given? + + if scope.is_a?(Relation) || !scope.respond_to?(:call) + ActiveSupport::Deprecation.warn( + "Calling #default_scope without a block is deprecated. For example instead " \ + "of `default_scope where(color: 'red')`, please use " \ + "`default_scope { where(color: 'red') }`. (Alternatively you can just redefine " \ + "self.default_scope.)" + ) + end + self.default_scopes = default_scopes + [scope] end def build_default_scope #:nodoc: - if method(:default_scope).owner != ActiveRecord::Scoping::Default::ClassMethods + if !Base.is_a?(method(:default_scope).owner) + # The user has defined their own default scope method, so call that evaluate_default_scope { default_scope } elsif default_scopes.any? evaluate_default_scope do 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) + if !scope.is_a?(Relation) && scope.respond_to?(:call) + default_scope.merge(unscoped { scope.call }) else default_scope.merge(scope) end diff --git a/activerecord/lib/active_record/scoping/named.rb b/activerecord/lib/active_record/scoping/named.rb index 0edc3f1dcc..75f31229b5 100644 --- a/activerecord/lib/active_record/scoping/named.rb +++ b/activerecord/lib/active_record/scoping/named.rb @@ -1,8 +1,6 @@ require 'active_support/core_ext/array' require 'active_support/core_ext/hash/except' require 'active_support/core_ext/kernel/singleton_class' -require 'active_support/core_ext/object/blank' -require 'active_support/core_ext/class/attribute' module ActiveRecord # = Active Record Named \Scopes @@ -11,33 +9,25 @@ module ActiveRecord extend ActiveSupport::Concern module ClassMethods - # Returns an anonymous \scope. + # Returns an <tt>ActiveRecord::Relation</tt> scope object. # - # posts = Post.scoped + # posts = Post.all # posts.size # Fires "select count(*) from posts" and returns the count # posts.each {|p| puts p.name } # Fires "select * from posts" and loads post objects # - # fruits = Fruit.scoped + # fruits = Fruit.all # fruits = fruits.where(:color => 'red') if options[:red_only] # fruits = fruits.limit(10) if limited? # - # Anonymous \scopes tend to be useful when procedurally generating complex - # queries, where passing intermediate values (\scopes) around as first-class - # objects is convenient. - # # You can define a \scope that applies to all finders using # ActiveRecord::Base.default_scope. - def scoped(options = nil) - if options - scoped.apply_finder_options(options) + def all + if current_scope + current_scope.clone else - if current_scope - current_scope.clone - else - scope = relation.clone - scope.default_scoped = true - scope - end + scope = relation + scope.default_scoped = true + scope end end @@ -48,7 +38,7 @@ module ActiveRecord if current_scope current_scope.scope_for_create else - scope = relation.clone + scope = relation scope.default_scoped = true scope.scope_for_create end @@ -171,29 +161,27 @@ module ActiveRecord # Article.published.featured.latest_article # Article.featured.titles - def scope(name, scope_options = {}) - name = name.to_sym - valid_scope_name?(name) - extension = Module.new(&Proc.new) if block_given? - - scope_proc = lambda do |*args| - options = scope_options.respond_to?(:call) ? unscoped { scope_options.call(*args) } : scope_options - options = scoped.apply_finder_options(options) if options.is_a?(Hash) - - relation = scoped.merge(options) + def scope(name, body, &block) + extension = Module.new(&block) if block - extension ? relation.extending(extension) : relation + # Check body.is_a?(Relation) to prevent the relation actually being + # loaded by respond_to? + if body.is_a?(Relation) || !body.respond_to?(:call) + ActiveSupport::Deprecation.warn( + "Using #scope without passing a callable object is deprecated. For " \ + "example `scope :red, where(color: 'red')` should be changed to " \ + "`scope :red, -> { where(color: 'red') }`. There are numerous gotchas " \ + "in the former usage and it makes the implementation more complicated " \ + "and buggy. (If you prefer, you can just define a class method named " \ + "`self.red`.)" + ) end - singleton_class.send(:redefine_method, name, &scope_proc) - end - - protected + singleton_class.send(:define_method, name) do |*args| + options = body.respond_to?(:call) ? unscoped { body.call(*args) } : body + relation = all.merge(options) - def valid_scope_name?(name) - if respond_to?(name, true) - logger.warn "Creating scope :#{name}. " \ - "Overwriting existing method #{self.name}.#{name}." + extension ? relation.extending(extension) : relation end end end |