问题
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 thescope
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