Ruby on Rails ActiveRecord scopes vs class methods

那年仲夏 提交于 2019-12-21 07:01:05

问题


Rails internally converts scopes to class methods then why can't we use class methods itself instead of going with scopes.


回答1:


From the fine guide:

14 Scopes
[...]
To define a simple scope, we use the scope method inside the class, passing the query that we'd like to run when this scope is called:

class Article < ActiveRecord::Base
  scope :published, -> { where(published: true) }
end

This is exactly the same as defining a class method, and which you use is a matter of personal preference:

class Article < ActiveRecord::Base
  def self.published
    where(published: true)
  end
end

Note in particular:

This is exactly the same as defining a class method, and which you use is a matter of personal preference

And a little further (the Rails3 guide says the same thing here BTW):

14.1 Passing in arguments
[...]
Using a class method is the preferred way to accept arguments for scopes.

So which you use is a matter of preference and it is even recommended that you use class methods for scopes that take arguments.

Using scope is mostly a notational issue. If you say scope :whatever then you're explicitly saying that whatever is meant to be a query builder; if you say def self.whatever then you're not implying anything about the intent of the whatever method, you're just defining some class method that may or may not behave like a scope.

Of course, 14.1 makes a mess of this notational distinction by recommending that you not use scope when your scope takes arguments. Also keep in mind that in Rails3 you could say:

scope :published, where(published: true)

so an argumentless scope was visually "clean" and terse but adding a lambda to handle arguments would make it look messier:

scope :pancakes, ->(x) { where(things: x) }

But Rails4 wants lambdas even for argumentless scopes the distinction makes even less sense now.

I suspect that the difference is historical at this point. Scopes were probably something special back in the before times but became plain old class methods in the Rails3 era to cut down on duplication and to better mesh with the new query interface that came with Rails3.


So you can skip scope and go straight to class methods if you wish. You're even encouraged to do so when your scope takes arguments.




回答2:


Scopes are just class methods. Internally Active Record converts a scope into a class method.

"There is no difference between them" or “it is a matter of taste”. I tend to agree with both sentences, but I’d like to show some slight differences that exist between both. This blogs explains the difference very well.




回答3:


Why should I use a scope if it is just syntax sugar for a class method?”. So here are some interesting examples for you to think about.

Scopes are always chainable=> •••••••••••••••••••••••••••••••••••••••••••• Lets use the following scenario: users will be able to filter posts by statuses, ordering by most recent updated ones. Simple enough, lets write scopes for that:

  class Post < ActiveRecord::Base
      scope :by_status, -> status { where(status: status) }
       scope :recent, -> { order("posts.updated_at DESC") }
   end
   And we can call them freely like this:

   Post.by_status('published').recent
    # SELECT "posts".* FROM "posts" WHERE "posts"."status" = 'published' 
   #   ORDER BY posts.updated_at DESC
    Or with a user provided param:

   Post.by_status(params[:status]).recent
    # SELECT "posts".* FROM "posts" WHERE "posts"."status" = 'published' 
      #   ORDER BY posts.updated_at DESC
     So far, so good. Now lets move them to class methods, just for the sake of comparing:

    class Post < ActiveRecord::Base
          def self.by_status(status)
              where(status: status)
          end

         def self.recent
           order("posts.updated_at DESC")
          end
    end

Besides using a few extra lines, no big improvements. But now what happens if the :status parameter is nil or blank?

  Post.by_status(nil).recent
    # SELECT "posts".* FROM "posts" WHERE "posts"."status" IS NULL 
     #   ORDER BY posts.updated_at DESC

     Post.by_status('').recent
     # SELECT "posts".* FROM "posts" WHERE "posts"."status" = '' 
      #   ORDER BY posts.updated_at DESC
 Oooops, I don't think we wanted to allow these queries, did we? With scopes, we can easily fix that by adding a presence condition to our scope:

             scope :by_status, -> status { where(status: status) if status.present? }
    There we go:

     Post.by_status(nil).recent
          # SELECT "posts".* FROM "posts" ORDER BY           posts.updated_at DESC

    Post.by_status('').recent
                        # SELECT "posts".* FROM "posts" ORDER BY posts.updated_at DESC
     Awesome. Now lets try to do the same with our beloved class method:

  class Post < ActiveRecord::Base
    def self.by_status(status)
        where(status: status) if status.present?
      end
   end
    Running this:

     Post.by_status('').recent
                         NoMethodError: undefined method `recent' for nil:NilClass
And . The difference is that a scope will always return a relation, whereas our simple class method implementation will not. The class method should look like this instead:

       def self.by_status(status)
             if status.present?
                where(status: status)
            else
                  all
           end
       end

Notice that I'm returning all for the nil/blank case, which in Rails 4 returns a relation (it previously returned the Array of items from the database). In Rails 3.2.x, you should use scoped there instead. And there we go:

        Post.by_status('').recent
         # SELECT "posts".* FROM "posts" ORDER BY     posts.updated_at DESC

So the advice here is: never return nil from a class method that should work like a scope, otherwise you're breaking the chainability condition implied by scopes, that always return a relation.

Scopes are extensible => ••••••••••••••••••••••••••••• Lets get pagination as our next example and I'm going to use the kaminari gem as basis. The most important thing you need to do when paginating a collection is to tell which page you want to fetch:

    Post.page(2)
    After doing that you might want to say how many    records per page you want:

  Post.page(2).per(15)
    And you may to know the total number of pages, or         whether you are in the first or last page:

    posts = Post.page(2)
    posts.total_pages # => 2
     posts.first_page? # => false
    posts.last_page?  # => true

This all makes sense when we call things in this order, but it doesn't make any sense to call these methods in a collection that is not paginated, does it? When you write scopes, you can add specific extensions that will only be available in your object if that scope is called. In case of kaminari, it only adds the page scope to your Active Record models, and relies on the scope extensions feature to add all other functionality when page is called. Conceptually, the code would look like this:

     scope :page, -> num { # some limit + offset logic here for pagination } do
     def per(num)
      # more logic here
     end

     def total_pages
        # some more here
     end

     def first_page?
          # and a bit more
     end

      def last_page?
         # and so on
      end
     end

Scope extensions is a powerful and flexible technique to have in our toolchain. But of course, we can always go wild and get all that with class methods too:

  def self.page(num)
   scope = # some limit + offset logic here for pagination
   scope.extend PaginationExtensions
   scope
  end

  module PaginationExtensions
     def per(num)
     # more logic here
     end

    def total_pages
       # some more here
    end

    def first_page?
        # and a bit more
     end

     def last_page?
        # and so on
     end
  end

It is a bit more verbose than using a scope, but it yields the same results. And the advice here is: pick what works better for you but make sure you know what the framework provides before reinventing the wheel.



来源:https://stackoverflow.com/questions/32930312/ruby-on-rails-activerecord-scopes-vs-class-methods

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!