From 2b22564c4efaa63d4bbc006762838c4025c1bdca Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Thu, 15 Dec 2011 20:35:04 +0000 Subject: Move DefaultScope and NamedScope under Scoping --- activerecord/lib/active_record.rb | 11 +- activerecord/lib/active_record/base.rb | 3 +- activerecord/lib/active_record/default_scope.rb | 138 --------------- activerecord/lib/active_record/named_scope.rb | 200 --------------------- activerecord/lib/active_record/scoping.rb | 5 + activerecord/lib/active_record/scoping/default.rb | 140 +++++++++++++++ activerecord/lib/active_record/scoping/named.rb | 202 ++++++++++++++++++++++ 7 files changed, 357 insertions(+), 342 deletions(-) delete mode 100644 activerecord/lib/active_record/default_scope.rb delete mode 100644 activerecord/lib/active_record/named_scope.rb create mode 100644 activerecord/lib/active_record/scoping/default.rb create mode 100644 activerecord/lib/active_record/scoping/named.rb (limited to 'activerecord/lib') diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb index de73715e35..2463f3951f 100644 --- a/activerecord/lib/active_record.rb +++ b/activerecord/lib/active_record.rb @@ -58,7 +58,6 @@ module ActiveRecord autoload :Base autoload :Callbacks autoload :CounterCache - autoload :DefaultScope autoload :DynamicMatchers autoload :DynamicFinderMatch autoload :DynamicScopeMatch @@ -69,7 +68,6 @@ module ActiveRecord autoload :Migration autoload :Migrator, 'active_record/migration' autoload :ModelSchema - autoload :NamedScope autoload :NestedAttributes autoload :Observer autoload :Persistence @@ -129,6 +127,15 @@ module ActiveRecord end end + module Scoping + extend ActiveSupport::Autoload + + eager_autoload do + autoload :Named + autoload :Default + end + end + autoload :TestCase autoload :TestFixtures, 'active_record/fixtures' end diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index 819c616927..b479c403a7 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -686,7 +686,6 @@ module ActiveRecord #:nodoc: extend Translation include Inheritance include Scoping - include DefaultScope extend DynamicMatchers include Sanitization include Integration @@ -697,7 +696,7 @@ module ActiveRecord #:nodoc: include Locking::Optimistic, Locking::Pessimistic include AttributeMethods include Callbacks, ActiveModel::Observing, Timestamp - include Associations, NamedScope + include Associations include IdentityMap include ActiveModel::SecurePassword include Explain diff --git a/activerecord/lib/active_record/default_scope.rb b/activerecord/lib/active_record/default_scope.rb deleted file mode 100644 index dd637ad6c5..0000000000 --- a/activerecord/lib/active_record/default_scope.rb +++ /dev/null @@ -1,138 +0,0 @@ -require 'active_support/concern' - -module ActiveRecord - module DefaultScope - extend ActiveSupport::Concern - - included do - # Stores the default scope for the class - class_attribute :default_scopes, :instance_writer => false - self.default_scopes = [] - end - - module ClassMethods - # Returns a scope for this class without taking into account the default_scope. - # - # class Post < ActiveRecord::Base - # def self.default_scope - # where :published => true - # end - # end - # - # Post.all # Fires "SELECT * FROM posts WHERE published = true" - # Post.unscoped.all # Fires "SELECT * FROM posts" - # - # This method also accepts a block meaning that all queries inside the block will - # not use the default_scope: - # - # Post.unscoped { - # Post.limit(10) # Fires "SELECT * FROM posts LIMIT 10" - # } - # - # It is recommended to use block form of unscoped because chaining unscoped with scope - # does not work. Assuming that published is a scope following two statements are same. - # - # Post.unscoped.published - # Post.published - def unscoped #:nodoc: - block_given? ? relation.scoping { yield } : relation - end - - def before_remove_const #:nodoc: - self.current_scope = nil - end - - protected - - # Use this macro in your model to set a default scope for all operations on - # the model. - # - # class Article < ActiveRecord::Base - # default_scope where(:published => true) - # end - # - # Article.all # => SELECT * FROM articles WHERE published = true - # - # The default_scope is also applied while creating/building a record. It is not - # applied while updating a record. - # - # Article.new.published # => true - # Article.create.published # => true - # - # You can also use default_scope 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 call to the default_scope - # macro, and it will be called when building the default scope.) - # - # If you use multiple default_scope declarations in your model then they will - # be merged together: - # - # class Article < ActiveRecord::Base - # default_scope where(:published => true) - # default_scope where(:rating => 'G') - # end - # - # Article.all # => SELECT * FROM articles WHERE published = true AND rating = 'G' - # - # This is also the case with inheritance and module includes where the parent or module - # defines a default_scope and the child or including class defines a second one. - # - # 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 = {}) - scope = Proc.new if block_given? - self.default_scopes = default_scopes + [scope] - end - - def build_default_scope #:nodoc: - if method(:default_scope).owner != DefaultScope::ClassMethods - 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) - else - default_scope.merge(scope) - end - end - end - end - end - - def ignore_default_scope? #:nodoc: - Thread.current["#{self}_ignore_default_scope"] - end - - def ignore_default_scope=(ignore) #:nodoc: - Thread.current["#{self}_ignore_default_scope"] = ignore - end - - # The ignore_default_scope flag is used to prevent an infinite recursion situation where - # a default scope references a scope which has a default scope which references a scope... - def evaluate_default_scope - return if ignore_default_scope? - - begin - self.ignore_default_scope = true - yield - ensure - self.ignore_default_scope = false - end - end - - end - end -end diff --git a/activerecord/lib/active_record/named_scope.rb b/activerecord/lib/active_record/named_scope.rb deleted file mode 100644 index 0313abe456..0000000000 --- a/activerecord/lib/active_record/named_scope.rb +++ /dev/null @@ -1,200 +0,0 @@ -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 - module NamedScope - extend ActiveSupport::Concern - - module ClassMethods - # Returns an anonymous \scope. - # - # posts = Post.scoped - # 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 = 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) - else - if current_scope - current_scope.clone - else - scope = relation.clone - scope.default_scoped = true - scope - end - end - end - - ## - # Collects attributes from scopes that should be applied when creating - # an AR instance for the particular class this is called on. - def scope_attributes # :nodoc: - if current_scope - current_scope.scope_for_create - else - scope = relation.clone - scope.default_scoped = true - scope.scope_for_create - end - end - - ## - # Are there default attributes associated with this scope? - def scope_attributes? # :nodoc: - current_scope || default_scopes.any? - end - - # Adds a class method for retrieving and querying objects. A \scope represents a narrowing of a database query, - # such as where(:color => :red).select('shirts.*').includes(:washing_instructions). - # - # class Shirt < ActiveRecord::Base - # scope :red, where(:color => 'red') - # scope :dry_clean_only, joins(:washing_instructions).where('washing_instructions.dry_clean_only = ?', true) - # end - # - # The above calls to scope define class methods Shirt.red and Shirt.dry_clean_only. Shirt.red, - # in effect, represents the query Shirt.where(:color => 'red'). - # - # Note that this is simply 'syntactic sugar' for defining an actual class method: - # - # class Shirt < ActiveRecord::Base - # def self.red - # where(:color => 'red') - # end - # end - # - # Unlike Shirt.find(...), however, the object returned by Shirt.red is not an Array; it - # resembles the association object constructed by a has_many declaration. For instance, - # you can invoke Shirt.red.first, Shirt.red.count, Shirt.red.where(:size => 'small'). - # Also, just as with the association objects, named \scopes act like an Array, implementing Enumerable; - # Shirt.red.each(&block), Shirt.red.first, and Shirt.red.inject(memo, &block) - # all behave as if Shirt.red really was an Array. - # - # These named \scopes are composable. For instance, Shirt.red.dry_clean_only will produce - # all shirts that are both red and dry clean only. - # Nested finds and calculations also work with these compositions: Shirt.red.dry_clean_only.count - # returns the number of garments for which these criteria obtain. Similarly with - # Shirt.red.dry_clean_only.average(:thread_count). - # - # All \scopes are available as class methods on the ActiveRecord::Base descendant upon which - # the \scopes were defined. But they are also available to has_many associations. If, - # - # class Person < ActiveRecord::Base - # has_many :shirts - # end - # - # then elton.shirts.red.dry_clean_only will return all of Elton's red, dry clean - # only shirts. - # - # Named \scopes can also be procedural: - # - # class Shirt < ActiveRecord::Base - # scope :colored, lambda { |color| where(:color => color) } - # end - # - # In this example, Shirt.colored('puce') finds all puce shirts. - # - # On Ruby 1.9 you can use the 'stabby lambda' syntax: - # - # scope :colored, ->(color) { where(:color => color) } - # - # Note that scopes defined with \scope will be evaluated when they are defined, rather than - # when they are used. For example, the following would be incorrect: - # - # class Post < ActiveRecord::Base - # scope :recent, where('published_at >= ?', Time.now - 1.week) - # end - # - # The example above would be 'frozen' to the Time.now value when the Post - # class was defined, and so the resultant SQL query would always be the same. The correct - # way to do this would be via a lambda, which will re-evaluate the scope each time - # it is called: - # - # class Post < ActiveRecord::Base - # scope :recent, lambda { where('published_at >= ?', Time.now - 1.week) } - # end - # - # Named \scopes can also have extensions, just as with has_many declarations: - # - # class Shirt < ActiveRecord::Base - # scope :red, where(:color => 'red') do - # def dom_id - # 'red_shirts' - # end - # end - # end - # - # Scopes can also be used while creating/building a record. - # - # class Article < ActiveRecord::Base - # scope :published, where(:published => true) - # end - # - # Article.published.new.published # => true - # Article.published.create.published # => true - # - # Class methods on your model are automatically available - # on scopes. Assuming the following setup: - # - # class Article < ActiveRecord::Base - # scope :published, where(:published => true) - # scope :featured, where(:featured => true) - # - # def self.latest_article - # order('published_at desc').first - # end - # - # def self.titles - # map(&:title) - # end - # - # end - # - # We are able to call the methods like this: - # - # 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) ? scope_options.call(*args) : scope_options - options = scoped.apply_finder_options(options) if options.is_a?(Hash) - - relation = scoped.merge(options) - - extension ? relation.extending(extension) : relation - end - - singleton_class.send(:redefine_method, name, &scope_proc) - end - - protected - - def valid_scope_name?(name) - if respond_to?(name, true) - logger.warn "Creating scope :#{name}. " \ - "Overwriting existing method #{self.name}.#{name}." - end - end - end - end -end diff --git a/activerecord/lib/active_record/scoping.rb b/activerecord/lib/active_record/scoping.rb index 50cd7d77e9..a8f5e96190 100644 --- a/activerecord/lib/active_record/scoping.rb +++ b/activerecord/lib/active_record/scoping.rb @@ -4,6 +4,11 @@ module ActiveRecord module Scoping extend ActiveSupport::Concern + included do + include Default + include Named + end + module ClassMethods # with_scope lets you apply options to inner block incrementally. It takes a hash and the keys must be # :find or :create. :find parameter is Relation while diff --git a/activerecord/lib/active_record/scoping/default.rb b/activerecord/lib/active_record/scoping/default.rb new file mode 100644 index 0000000000..9840cbccae --- /dev/null +++ b/activerecord/lib/active_record/scoping/default.rb @@ -0,0 +1,140 @@ +require 'active_support/concern' + +module ActiveRecord + module Scoping + module Default + extend ActiveSupport::Concern + + included do + # Stores the default scope for the class + class_attribute :default_scopes, :instance_writer => false + self.default_scopes = [] + end + + module ClassMethods + # Returns a scope for this class without taking into account the default_scope. + # + # class Post < ActiveRecord::Base + # def self.default_scope + # where :published => true + # end + # end + # + # Post.all # Fires "SELECT * FROM posts WHERE published = true" + # Post.unscoped.all # Fires "SELECT * FROM posts" + # + # This method also accepts a block meaning that all queries inside the block will + # not use the default_scope: + # + # Post.unscoped { + # Post.limit(10) # Fires "SELECT * FROM posts LIMIT 10" + # } + # + # It is recommended to use block form of unscoped because chaining unscoped with scope + # does not work. Assuming that published is a scope following two statements are same. + # + # Post.unscoped.published + # Post.published + def unscoped #:nodoc: + block_given? ? relation.scoping { yield } : relation + end + + def before_remove_const #:nodoc: + self.current_scope = nil + end + + protected + + # Use this macro in your model to set a default scope for all operations on + # the model. + # + # class Article < ActiveRecord::Base + # default_scope where(:published => true) + # end + # + # Article.all # => SELECT * FROM articles WHERE published = true + # + # The default_scope is also applied while creating/building a record. It is not + # applied while updating a record. + # + # Article.new.published # => true + # Article.create.published # => true + # + # You can also use default_scope 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 call to the default_scope + # macro, and it will be called when building the default scope.) + # + # If you use multiple default_scope declarations in your model then they will + # be merged together: + # + # class Article < ActiveRecord::Base + # default_scope where(:published => true) + # default_scope where(:rating => 'G') + # end + # + # Article.all # => SELECT * FROM articles WHERE published = true AND rating = 'G' + # + # This is also the case with inheritance and module includes where the parent or module + # defines a default_scope and the child or including class defines a second one. + # + # 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 = {}) + scope = Proc.new if block_given? + self.default_scopes = default_scopes + [scope] + end + + def build_default_scope #:nodoc: + if method(:default_scope).owner != ActiveRecord::Scoping::Default::ClassMethods + 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) + else + default_scope.merge(scope) + end + end + end + end + end + + def ignore_default_scope? #:nodoc: + Thread.current["#{self}_ignore_default_scope"] + end + + def ignore_default_scope=(ignore) #:nodoc: + Thread.current["#{self}_ignore_default_scope"] = ignore + end + + # The ignore_default_scope flag is used to prevent an infinite recursion situation where + # a default scope references a scope which has a default scope which references a scope... + def evaluate_default_scope + return if ignore_default_scope? + + begin + self.ignore_default_scope = true + yield + ensure + self.ignore_default_scope = false + end + end + + end + end + end +end diff --git a/activerecord/lib/active_record/scoping/named.rb b/activerecord/lib/active_record/scoping/named.rb new file mode 100644 index 0000000000..f7512bbf5f --- /dev/null +++ b/activerecord/lib/active_record/scoping/named.rb @@ -0,0 +1,202 @@ +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 + module Scoping + module Named + extend ActiveSupport::Concern + + module ClassMethods + # Returns an anonymous \scope. + # + # posts = Post.scoped + # 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 = 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) + else + if current_scope + current_scope.clone + else + scope = relation.clone + scope.default_scoped = true + scope + end + end + end + + ## + # Collects attributes from scopes that should be applied when creating + # an AR instance for the particular class this is called on. + def scope_attributes # :nodoc: + if current_scope + current_scope.scope_for_create + else + scope = relation.clone + scope.default_scoped = true + scope.scope_for_create + end + end + + ## + # Are there default attributes associated with this scope? + def scope_attributes? # :nodoc: + current_scope || default_scopes.any? + end + + # Adds a class method for retrieving and querying objects. A \scope represents a narrowing of a database query, + # such as where(:color => :red).select('shirts.*').includes(:washing_instructions). + # + # class Shirt < ActiveRecord::Base + # scope :red, where(:color => 'red') + # scope :dry_clean_only, joins(:washing_instructions).where('washing_instructions.dry_clean_only = ?', true) + # end + # + # The above calls to scope define class methods Shirt.red and Shirt.dry_clean_only. Shirt.red, + # in effect, represents the query Shirt.where(:color => 'red'). + # + # Note that this is simply 'syntactic sugar' for defining an actual class method: + # + # class Shirt < ActiveRecord::Base + # def self.red + # where(:color => 'red') + # end + # end + # + # Unlike Shirt.find(...), however, the object returned by Shirt.red is not an Array; it + # resembles the association object constructed by a has_many declaration. For instance, + # you can invoke Shirt.red.first, Shirt.red.count, Shirt.red.where(:size => 'small'). + # Also, just as with the association objects, named \scopes act like an Array, implementing Enumerable; + # Shirt.red.each(&block), Shirt.red.first, and Shirt.red.inject(memo, &block) + # all behave as if Shirt.red really was an Array. + # + # These named \scopes are composable. For instance, Shirt.red.dry_clean_only will produce + # all shirts that are both red and dry clean only. + # Nested finds and calculations also work with these compositions: Shirt.red.dry_clean_only.count + # returns the number of garments for which these criteria obtain. Similarly with + # Shirt.red.dry_clean_only.average(:thread_count). + # + # All \scopes are available as class methods on the ActiveRecord::Base descendant upon which + # the \scopes were defined. But they are also available to has_many associations. If, + # + # class Person < ActiveRecord::Base + # has_many :shirts + # end + # + # then elton.shirts.red.dry_clean_only will return all of Elton's red, dry clean + # only shirts. + # + # Named \scopes can also be procedural: + # + # class Shirt < ActiveRecord::Base + # scope :colored, lambda { |color| where(:color => color) } + # end + # + # In this example, Shirt.colored('puce') finds all puce shirts. + # + # On Ruby 1.9 you can use the 'stabby lambda' syntax: + # + # scope :colored, ->(color) { where(:color => color) } + # + # Note that scopes defined with \scope will be evaluated when they are defined, rather than + # when they are used. For example, the following would be incorrect: + # + # class Post < ActiveRecord::Base + # scope :recent, where('published_at >= ?', Time.now - 1.week) + # end + # + # The example above would be 'frozen' to the Time.now value when the Post + # class was defined, and so the resultant SQL query would always be the same. The correct + # way to do this would be via a lambda, which will re-evaluate the scope each time + # it is called: + # + # class Post < ActiveRecord::Base + # scope :recent, lambda { where('published_at >= ?', Time.now - 1.week) } + # end + # + # Named \scopes can also have extensions, just as with has_many declarations: + # + # class Shirt < ActiveRecord::Base + # scope :red, where(:color => 'red') do + # def dom_id + # 'red_shirts' + # end + # end + # end + # + # Scopes can also be used while creating/building a record. + # + # class Article < ActiveRecord::Base + # scope :published, where(:published => true) + # end + # + # Article.published.new.published # => true + # Article.published.create.published # => true + # + # Class methods on your model are automatically available + # on scopes. Assuming the following setup: + # + # class Article < ActiveRecord::Base + # scope :published, where(:published => true) + # scope :featured, where(:featured => true) + # + # def self.latest_article + # order('published_at desc').first + # end + # + # def self.titles + # map(&:title) + # end + # + # end + # + # We are able to call the methods like this: + # + # 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) ? scope_options.call(*args) : scope_options + options = scoped.apply_finder_options(options) if options.is_a?(Hash) + + relation = scoped.merge(options) + + extension ? relation.extending(extension) : relation + end + + singleton_class.send(:redefine_method, name, &scope_proc) + end + + protected + + def valid_scope_name?(name) + if respond_to?(name, true) + logger.warn "Creating scope :#{name}. " \ + "Overwriting existing method #{self.name}.#{name}." + end + end + end + end + end +end -- cgit v1.2.3