diff options
Diffstat (limited to 'activerecord/lib/active_record/scoping/named.rb')
-rw-r--r-- | activerecord/lib/active_record/scoping/named.rb | 209 |
1 files changed, 209 insertions, 0 deletions
diff --git a/activerecord/lib/active_record/scoping/named.rb b/activerecord/lib/active_record/scoping/named.rb new file mode 100644 index 0000000000..5278eb29a9 --- /dev/null +++ b/activerecord/lib/active_record/scoping/named.rb @@ -0,0 +1,209 @@ +# frozen_string_literal: true + +require "active_support/core_ext/array" +require "active_support/core_ext/hash/except" +require "active_support/core_ext/kernel/singleton_class" + +module ActiveRecord + # = Active Record \Named \Scopes + module Scoping + module Named + extend ActiveSupport::Concern + + module ClassMethods + # Returns an ActiveRecord::Relation scope object. + # + # 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.all + # fruits = fruits.where(color: 'red') if options[:red_only] + # fruits = fruits.limit(10) if limited? + # + # You can define a scope that applies to all finders using + # {default_scope}[rdoc-ref:Scoping::Default::ClassMethods#default_scope]. + def all + scope = current_scope + + if scope + if self == scope.klass + scope.clone + else + relation.merge!(scope) + end + else + default_scoped + end + end + + def scope_for_association(scope = relation) # :nodoc: + if current_scope&.empty_scope? + scope + else + default_scoped(scope) + end + end + + def default_scoped(scope = relation) # :nodoc: + build_default_scope(scope) || scope + end + + def default_extensions # :nodoc: + if scope = current_scope || build_default_scope + scope.extensions + else + [] + end + end + + # Adds a class method for retrieving and querying objects. + # The method is intended to return an ActiveRecord::Relation + # object, which is composable with other scopes. + # If it returns +nil+ or +false+, an + # {all}[rdoc-ref:Scoping::Named::ClassMethods#all] scope is returned instead. + # + # A \scope represents a narrowing of a database query, such as + # <tt>where(color: :red).select('shirts.*').includes(:washing_instructions)</tt>. + # + # 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 <tt>Shirt.red</tt> and + # <tt>Shirt.dry_clean_only</tt>. <tt>Shirt.red</tt>, in effect, + # represents the query <tt>Shirt.where(color: 'red')</tt>. + # + # You should always pass a callable object to the scopes defined + # with #scope. This ensures that the scope is re-evaluated each + # time it is called. + # + # 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 <tt>Shirt.find(...)</tt>, however, the object returned by + # <tt>Shirt.red</tt> is not an Array but an ActiveRecord::Relation, + # which is composable with other scopes; it resembles the association object + # constructed by a {has_many}[rdoc-ref:Associations::ClassMethods#has_many] + # declaration. For instance, you can invoke <tt>Shirt.red.first</tt>, <tt>Shirt.red.count</tt>, + # <tt>Shirt.red.where(size: 'small')</tt>. Also, just as with the + # association objects, named \scopes act like an Array, implementing + # Enumerable; <tt>Shirt.red.each(&block)</tt>, <tt>Shirt.red.first</tt>, + # and <tt>Shirt.red.inject(memo, &block)</tt> all behave as if + # <tt>Shirt.red</tt> really was an array. + # + # These named \scopes are composable. For instance, + # <tt>Shirt.red.dry_clean_only</tt> will produce all shirts that are + # both red and dry clean only. Nested finds and calculations also work + # with these compositions: <tt>Shirt.red.dry_clean_only.count</tt> + # returns the number of garments for which these criteria obtain. + # Similarly with <tt>Shirt.red.dry_clean_only.average(:thread_count)</tt>. + # + # 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}[rdoc-ref:Associations::ClassMethods#has_many] + # associations. If, + # + # class Person < ActiveRecord::Base + # has_many :shirts + # end + # + # then <tt>elton.shirts.red.dry_clean_only</tt> will return all of + # Elton's red, dry clean only shirts. + # + # \Named scopes can also have extensions, just as with + # {has_many}[rdoc-ref:Associations::ClassMethods#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 + # pluck(:title) + # end + # end + # + # We are able to call the methods like this: + # + # Article.published.featured.latest_article + # Article.featured.titles + def scope(name, body, &block) + unless body.respond_to?(:call) + raise ArgumentError, "The scope body needs to be callable." + end + + if dangerous_class_method?(name) + raise ArgumentError, "You tried to define a scope named \"#{name}\" " \ + "on the model \"#{self.name}\", but Active Record already defined " \ + "a class method with the same name." + end + + if method_defined_within?(name, Relation) + raise ArgumentError, "You tried to define a scope named \"#{name}\" " \ + "on the model \"#{self.name}\", but ActiveRecord::Relation already defined " \ + "an instance method with the same name." + end + + valid_scope_name?(name) + extension = Module.new(&block) if block + + if body.respond_to?(:to_proc) + singleton_class.define_method(name) do |*args| + scope = all._exec_scope(*args, &body) + scope = scope.extending(extension) if extension + scope + end + else + singleton_class.define_method(name) do |*args| + scope = body.call(*args) || all + scope = scope.extending(extension) if extension + scope + end + end + + generate_relation_method(name) + end + + private + + def valid_scope_name?(name) + if respond_to?(name, true) && logger + logger.warn "Creating scope :#{name}. " \ + "Overwriting existing method #{self.name}.#{name}." + end + end + end + end + end +end |