{"id":3354,"date":"2013-02-07T15:51:06","date_gmt":"2013-02-07T17:51:06","guid":{"rendered":"http:\/\/blog.plataformatec.com.br\/?p=3354"},"modified":"2013-02-07T15:51:06","modified_gmt":"2013-02-07T17:51:06","slug":"active-record-scopes-vs-class-methods","status":"publish","type":"post","link":"https:\/\/blog.plataformatec.com.br\/2013\/02\/active-record-scopes-vs-class-methods\/","title":{"rendered":"Active Record scopes vs class methods"},"content":{"rendered":"
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<\/a>. The classic comment usually boils down to “there is no difference between them”<\/em> or “it is a matter of taste”<\/em>. I tend to agree with both sentences, but I’d like to show some slight differences that exist between both.<\/p>\n First of all, lets get a better understanding about how scopes are used. In Rails 3 you can define a scope in two ways:<\/p>\n The main difference between both usages is that the Because this won’t work as expected: Internally Active Record converts a scope into a class method. Conceptually, its simplified implementation in Rails master looks something like this:<\/p>\n Which ends up as a class method with the given name and body, like this:<\/p>\n 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?”<\/em>. So here are some interesting examples for you to think about.<\/p>\n 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:<\/p>\n And we can call them freely like this:<\/p>\n Or with a user provided param:<\/p>\n So far, so good. Now lets move them to class methods, just for the sake of comparing:<\/p>\n Besides using a few extra lines, no big improvements. But now what happens if the 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:<\/p>\n There we go:<\/p>\n Awesome. Now lets try to do the same with our beloved class method:<\/p>\n Running this:<\/p>\n 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:<\/p>\n Notice that I'm returning So the advice here is: never return Lets get pagination as our next example and I'm going to use the kaminari<\/a> gem as basis. The most important thing you need to do when paginating a collection is to tell which page you want to fetch:<\/p>\n After doing that you might want to say how many records per page you want:<\/p>\n And you may to know the total number of pages, or whether you are in the first or last page:<\/p>\nDefining a scope<\/h2>\n
\nclass Post < ActiveRecord::Base\n scope :published, where(status: 'published')\n scope :draft, -> { where(status: 'draft') } \nend\n<\/pre>\n
:published<\/code> condition is evaluated when the class is first loaded, whereas the
:draft<\/code> 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:<\/p>\n
\nclass Post < ActiveRecord::Base\n scope :published_last_week, where('published_at >= ?', 1.week.ago)\nend\n<\/pre>\n
1.week.ago<\/code> will be evaluated when the class is loaded, not every time the scope is called.<\/p>\n
Scopes are just class methods<\/h2>\n
\ndef self.scope(name, body)\n singleton_class.send(:define_method, name, &body)\nend\n<\/pre>\n
\ndef self.published\n where(status: 'published')\nend\n<\/pre>\n
Scopes are always chainable<\/h2>\n
\nclass Post < ActiveRecord::Base\n scope :by_status, -> status { where(status: status) }\n scope :recent, -> { order(\"posts.updated_at DESC\") }\nend\n<\/pre>\n
\nPost.by_status('published').recent\n# SELECT \"posts\".* FROM \"posts\" WHERE \"posts\".\"status\" = 'published' \n# ORDER BY posts.updated_at DESC\n<\/pre>\n
\nPost.by_status(params[:status]).recent\n# SELECT \"posts\".* FROM \"posts\" WHERE \"posts\".\"status\" = 'published' \n# ORDER BY posts.updated_at DESC\n<\/pre>\n
\nclass Post < ActiveRecord::Base\n def self.by_status(status)\n where(status: status)\n end\n \n def self.recent\n order(\"posts.updated_at DESC\")\n end\nend\n<\/pre>\n
:status<\/code> parameter is
nil<\/code> or
blank<\/code>?<\/p>\n
\nPost.by_status(nil).recent\n# SELECT \"posts\".* FROM \"posts\" WHERE \"posts\".\"status\" IS NULL \n# ORDER BY posts.updated_at DESC\n\nPost.by_status('').recent\n# SELECT \"posts\".* FROM \"posts\" WHERE \"posts\".\"status\" = '' \n# ORDER BY posts.updated_at DESC\n<\/pre>\n
\nscope :by_status, -> status { where(status: status) if status.present? }\n<\/pre>\n
\nPost.by_status(nil).recent\n# SELECT \"posts\".* FROM \"posts\" ORDER BY posts.updated_at DESC\n\nPost.by_status('').recent\n# SELECT \"posts\".* FROM \"posts\" ORDER BY posts.updated_at DESC\n<\/pre>\n
\nclass Post < ActiveRecord::Base\n def self.by_status(status)\n where(status: status) if status.present?\n end\nend\n<\/pre>\n
\nPost.by_status('').recent\nNoMethodError: undefined method `recent' for nil:NilClass\n<\/pre>\n
\ndef self.by_status(status)\n if status.present?\n where(status: status)\n else\n all\n end\nend\n<\/pre>\n
all<\/code> for the
nil\/blank<\/code> 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<\/code> there instead. And there we go:<\/p>\n
\nPost.by_status('').recent\n# SELECT \"posts\".* FROM \"posts\" ORDER BY posts.updated_at DESC\n<\/pre>\n
nil<\/code> 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.<\/p>\n
Scopes are extensible<\/h2>\n
\nPost.page(2)\n<\/pre>\n
\nPost.page(2).per(15)\n<\/pre>\n
\nposts = Post.page(2)\nposts.total_pages # => 2\nposts.first_page? # => false\nposts.last_page? # => true\n<\/pre>\n