Here at Plataformatec we use Github Pull Requests a lot for code review and this usually yields tons of constructive comments and excellent discussions from time to time. One of the recent topics was about whether we should use scopes or class methods throughout the project to be consistent. It’s also not hard to find discussions about it all over the internet. The classic comment usually boils down to “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.
Defining a scope
First of all, lets get a better understanding about how scopes are used. In Rails 3 you can define a scope in two ways:
class Post { where(status: 'draft') }
end
The main difference between both usages is that the :published
condition is evaluated when the class is first loaded, whereas the :draft
one is lazy evaluated when it is called. Because of that, in Rails 4 the first way is going to be deprecated which means you will always need to declare scopes with a callable object as argument. This is to avoid issues when trying to declare a scope with some sort of Time argument:
class Post = ?', 1.week.ago)
end
Because this won’t work as expected: 1.week.ago
will be evaluated when the class is loaded, not every time the scope is called.
Scopes are just class methods
Internally Active Record converts a scope into a class method. Conceptually, its simplified implementation in Rails master looks something like this:
def self.scope(name, body)
singleton_class.send(:define_method, name, &body)
end
Which ends up as a class method with the given name and body, like this:
def self.published
where(status: 'published')
end
And I think that’s why most people think: “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 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
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
Running this:
Post.by_status('').recent
NoMethodError: undefined method `recent' for nil:NilClass
And :bomb:. 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.
Wrapping up
I personally tend to use scopes when the logic is very small, for simple where/order clauses, and class methods when it involves a bit more complexity, but whether it receives an argument or not doesn't really matter much to me. I also tend to rely more on scopes when doing extensions like showed here, since it's a feature that Active Record already gives us for free.
I think it's important to clarify the main differences between scopes and class methods, so that you can pick the right tool for the job™, or the tool that makes you more comfortable. Whether you use one or another, I don't think it really matters, as long as you write them clear and consistently throughout your application.
Do you have any thought about using scopes vs class methods? Make sure to leave a comment below telling us what you think, we'd love to hear.
Nice writeup. I still use scopes as it’s just felt like the write thing, and these are great examples of why I believe I’m correct 🙂 Thanks for sharing.
Nice post.
Scopes should generally be avoided. You’ll be surprised to know that when you chain scopes dealing with the same column, like: `Post.published_before(date).published_after(date)` – the 2nd scope will completely override the first one, which is something that does not happen when using class methods.
Yeah I’m aware of it, but I intentionally decided to left this bit out of the blog post because it’d generate more confusion than show a point, and it’s likely something that will change in Rails 4 – there are related issues opened about it, and I hope to be able to work on them if no one is able to do before me. So I’d not say scopes should be completely avoided, I’d rather say this is a bug and will hopefully get fixed :).
Anyway, thanks for sharing this information with others here as well.
Good article, thanks
From what I understand there is no way to avoid this issue if you want to keep `default_scope` in. Only way is to remove the `default_scope`, which I’m all for doing, but not really sure if it will happen.
Nice article. Thanks for sharing
Nice article, I’ve had tackled this problem in the past.
It’d be cool to mention how to return a relation from a class method as well, so you could keep it chainable instead of returning nil.
Hey Chuck, thanks for your feedback.
The last class method example in the chainable section adds a condition that returns a relation with “all” in case the given status is blank. In Rails 4, “all” returns a relation and not an array of records anymore, whereas in Rails 3 you should use “scoped” to receive a relation back. In short, this example should always return a relation making the method always chainable.
Yeah, as you I think that default_scope is not going anywhere any time soon. But the problem of chaining two scopes that deal with the same condition is something I think we should be able to handle, lets see where it goes. Cheers.
Considering that’s a logical consequence of scoping on the same field twice, I don’t think that’s an argument against scopes. Are you saying it should raise an error instead, and doesn’t in order to support default_scope overriding?
> Considering that’s a logical consequence of scoping on the same field twice
It’s not a logical consequence. I would expect them to work together the same way as the do when you normally chain Arel methods.
All I’m saying is `default_scope` with all its quirks should be removed and then this changed.
Can someone explain why scopes are working weird when you inherit from STI class with scopes.
For ex:
class User
scope :active, where(…)
class Admin < User
Admin.active.to_sql will have extra condition of type:
select * from users where type in ('Admin', 'User')..
maybe I do not understand the point, when I write two scopes of type
scope :created_after, -> date { where(“created_at > ? “, date )}
scope :created_before, -> data { where(“created_at ‘2013-01-17’ ) AND (created_at < '2013-02-03' )
Gio
Good post, thanks 🙂
Yeah, my example wasn’t too good. But the issue is still there, just it manifests itself a little bit differently. Here are the details: https://github.com/rails/rails/issues/8511
Oleg, I’ve never seem this before, but if you think it’s a Rails issue I’d ask you to open an issue giving as much information as possible, with more code examples showing your issue there. Thanks!
Yup, I agree that we should make scopes work the same as chaining Arel methods, being combined in the same field instead of overriding the condition. And the same should happen for default_scope, it should combine using scopes or where methods, so that to override any condition you should be very specific about it.