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: , , , ,

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

    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.

  • http://profiles.google.com/sadjow Sadjow Medeiros Leão

    Nice post.

  • http://twitter.com/pawelgoscicki Paweł Gościcki

    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.

  • http://carlosantoniodasilva.wordpress.com Carlos Antonio

    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.

  • http://twitter.com/galtenberg C Galtenberg

    Good article, thanks

  • http://twitter.com/pawelgoscicki Paweł Gościcki

    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.

  • http://twitter.com/ACampolonghi Andrea Campolonghi

    Nice article. Thanks for sharing

  • Chuck Bergeron

    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.

  • http://carlosantoniodasilva.wordpress.com Carlos Antonio

    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.

  • http://carlosantoniodasilva.wordpress.com Carlos Antonio

    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.

  • Peter Marreck

    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?

  • http://twitter.com/pawelgoscicki Paweł Gościcki

    > 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.

  • Oleg Keene

    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')..

  • Giovanni Messina

    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

  • http://twitter.com/NewArtRiot Amir

    Good post, thanks :)

  • http://twitter.com/pawelgoscicki Paweł Gościcki

    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

  • http://carlosantoniodasilva.wordpress.com Carlos Antonio

    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!

  • http://carlosantoniodasilva.wordpress.com Carlos Antonio

    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.