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 < ActiveRecord::Base scope :published, where(status: 'published') scope :draft, -> { 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 < ActiveRecord::Base scope :published_last_week, where('published_at >= ?', 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 < 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 :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.
Tags: activerecord, rails, rails 3.2, rails 4, scopes
This entry was posted on Thursday, February 7th, 2013 at 3:51 pm and is filed under English. You can follow any responses to this entry through the RSS 2.0 feed. Both comments and pings are currently closed.-
http://jackdempsey.me Jack Dempsey
-
http://profiles.google.com/sadjow Sadjow Medeiros Leão
-
http://twitter.com/pawelgoscicki Paweł Gościcki
-
http://carlosantoniodasilva.wordpress.com Carlos Antonio
-
http://twitter.com/galtenberg C Galtenberg
-
http://twitter.com/pawelgoscicki Paweł Gościcki
-
http://twitter.com/ACampolonghi Andrea Campolonghi
-
Chuck Bergeron
-
http://carlosantoniodasilva.wordpress.com Carlos Antonio
-
http://carlosantoniodasilva.wordpress.com Carlos Antonio
-
Peter Marreck
-
http://twitter.com/pawelgoscicki Paweł Gościcki
-
Oleg Keene
-
Giovanni Messina
-
http://twitter.com/NewArtRiot Amir
-
http://twitter.com/pawelgoscicki Paweł Gościcki
-
http://carlosantoniodasilva.wordpress.com Carlos Antonio
-
http://carlosantoniodasilva.wordpress.com Carlos Antonio

All
English only
Em português apenas