Scopes are a great Rails tool to keep stuff DRY and well organized. It’s not complicated though, it’s just a set of pre-defined queries that can easily be chained to build complex queries. Let’s see how to use them efficiently and what to do when they’re not enough anymore.
Basics
Let’s start with the basics. We’ll use a sample model named Article
to illustrate what scopes allow you to do.
This model has the following attributes:
title (string)
content (text)
published (boolean)
author_name (string)
On your homepage, you only want to show published articles of course. So in your action, you would use the following code to ensure that.
def index
Article.where(published: true)
end
You will probably need to re-use this code somewhere else so it would be better to define it in the model directly. Plus, it has to do with querying data so it kinda belongs in the model and not the controller.
class Article < ActiveRecord::Base
scope :published, -> { where(published: true) }
end
And now we can just do this in our controller and anywhere else we need it:
def index
Article.published
end
Super clean! So that’s how you use a scope. Note that scopes don’t return an array of elements. Instead, they return an ActiveRecord::Relation.
With Parameters
Scopes can also receive parameters. Let’s see how we can create a scope to get all the articles written by a specific author.
class Article < ActiveRecord::Base
scope :author, -> (name) { where(author_name: name) }
end
Which allows us to do this in our controller:
def index
Article.author(params[:author_name])
end
Note that you can give the name you want to this scope. Here are a few other options for you:
written_by
authored_by
by_author
Chaining
Now that you know how to create scopes, here’s where they really shine: they’re completely chainable and only one query will be executed!
So with this:
class Article < ActiveRecord::Base
scope :published, -> { where(published: true) }
scope :author, -> (name) { where(author_name: name) }
end
You can do that:
def index
Article.published.author('thibault')
end
And Rails will generate one SQL query that will look something like this:
SELECT * FROM articles WHERE published = true AND author_name = 'thibault';
Lazy Loading
Note that the previous query is actually lazy-loaded so until you try to do something with the result, the actual SQL query won’t happen.
For example, this does not trigger the query:
Article.published.author('thibault')
But this does:
Article.published.author('thibault').to_a
Class methods
You can also use class methods to define scopes.
class Article < ActiveRecord::Base
scope :written_by_thibault, -> { where(author_name: 'thibault') }
def self.published
where(published: true)
end
def self.author(name)
where(author_name: name)
end
end
Defining scopes with the scope
option is cleaner but if you need to do some kind of processing, you might want to put that inside a class method instead.
Interesting Methods
There are a bunch of interesting methods related to scopes.
default_scope
Don’t. Use. This.
Default Scope allows you to set a scope that’s always called when you access your model. Why you should not use it is explained in this stackoverflow answer.
unscoped
If you ever stumble upon code using a default scope and you want to access models that are outside of that scope, you can use the unscoped
method like this:
Article.uncoped.some_other_scope
all
This method now returns a relation and can be used to get a scope without filtering.
articles = Article.all
articles = articles.published
all
used to return an array and you had to use scoped
instead.
none
Just the opposite of all
. You’re getting no record at all as an ActiveRecord::Relation
.
Going further
You can also use scopes to do ordering, eager loading and anything else you want as long as you return a scope. In the following example, we pre-load the relation to make the query more efficient.
class User < ActiveRecord::Base
attr_accessible :id, :username
has_many :addresses
scope :by_country, -> (name) { includes(:addresses).where("addresses.country = ?", "Thailand") }
end
class Address < ActiveRecord::Base
attr_accessible :id, :street, :city, :country
belongs_to :user
end
You can also create ordering scopes.
class Articles < ActiveRecord::Base
scope :ordered, -> { order('created_at desc') }
end
The End
This is the end of the beginner’s guide to scoping with Ruby on Rails. Scopes are truly awesome and will allow you to keep your code clean. DRY!