Why should I use scope if it's just syntactic sugar for a class method? "So, here are some interesting examples for you.
Areas are always chain => β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’ We use the following scenario: users will be able to filter messages by status, ordering the most recent ones. Simple enough, let me write for this:
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 zero or empty?
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
Note that I am returning everything for the nil / blank case, which in Rails 4 returns a relation (it previously returned Array of elements from the database). In Rails 3.2.x, you should use a scope instead. And here we go:
Post.by_status('').recent # SELECT "posts".* FROM "posts" ORDER BY posts.updated_at DESC
So, the advice here is: never return zero from a class method that should work as a scope, otherwise you will violate the integrity condition implied by scopes that always return a relation.
Areas are expandable => β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’ Let's consider pagination as our next example, and I'm going to use stone mantels in as a basis. The most important thing you need to do when paginating the collection is to specify which page you want to extract:
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
It all makes sense when we call things in that order, but it makes no sense to call these methods in a collection that is not paginated, right? When you write areas, you can add specific extensions that will only be available in your object if that area is called. In the case of kaminari, it only adds the page area to your Active Record models and relies on the scope extension function to add all the other functions when the page is called. In theory, the code would look like this:
scope :page, -> num {
Scope extensions are a powerful and flexible technique for our tool chain. But, of course, we can always figure it out and get it all using class methods:
def self.page(num) scope =
This is a little more verbose than using the area, but gives the same results. And the tip here: choose what works best for you, but make sure you know what the infrastructure provides before you reinvent the wheel.