Introduction
When I started learning Rails, I came across scopes while rummaging google for solutions to several Rails project challenges. During those times, Scopes were terrifying for me at first, and every opportunity I had, I'd sidestep using this incredible concept in any of my Rails projects.
The fact is that as one proceeds in our different software development journeys, there are some useful concepts and tools that are important to pick up to make our coding experience scale faster. I accepted scope because I later understood what it was all about and how it could help my backend coding journey.
In this article, I will be explaining what scope is and how to use it in different parts of any Rails application. I have set up an Equipment API with Rails, and this will be the examples utilized as the codebase in this write-up. Check out this repository to follow along. Let's dive in.
What Are Scopes
Scopes are SQL queries that you can build in any rails model. Often time, we tend to run similar queries in our rails console to query our database and also understand the structure of the result we are receiving before going further with the development of our applications.
According to the official ruby on rails guide, "Scoping allows us to make use of commonly-used queries which can be referenced as method calls on the association objects or models." The general expectation is that all scope bodies should return an ActiveRecord::Relation
or nil. As a result of this, it makes it easy to call other ActiveRecord methods on it. Simply put, a scope is just a custom chain of active record methods. They are sets of pre-defined queries that can be chained to build other complex Queries.
Why use Scopes
Scopes help you D.R.Y out your active record calls and also keep your codes organized. Scopes make it easy to find the records you need at a particular time.
Also, using scopes helps us develop a healthy habit of keeping the heavy stuffs away from the controller. Rails convention suggests that implementation of the business logic should exist in Rails model instead of the controller/view.
Scoping also allows you to specify frequently used queries which can be referenced as method calls on the association objects or models.
Furthermore, during testing sometimes, you do not want your test to go into your database to fetch results. With scopes, this is achievable as it allows for easier stubbing.
Types of Scopes
Default Scopes vs Named scopes
Default scopes take the name default_scope
when defined in the model and, they are scopes that are applied across all queries to the model in question. An example can be seen in the code block below:
class Equipment < ApplicationRecord
has_many :requests
has_many :customers, through: :requests
validates :brand, presence: true
validates :model, presence: true
validates :equipment_type, presence: true
validates :serial_no, presence: true
validates :accessories, presence: true
default_scope, -> {order("updated_at desc")}
end
Conversely, named scopes are scopes that take a name and are defined in a model for active record database queries. Typically, a named scope is made up of the scope
keyword, followed by the name you want to give to the scope, and a lambda. Below is an example of a named scope:
class Equipment < ApplicationRecord
has_many :requests
has_many :customers, through: :requests
validates :brand, presence: true
validates :model, presence: true
validates :equipment_type, presence: true
validates :serial_no, presence: true
validates :accessories, presence: true
scope :not_available, -> {where("available = ?", false)}
end
The difference between both examples is glaring: one has a custom name and, one has a statutory name called default_scope. In the above example, the named scope above is called not_available. The following arrow sign ->
(lambda) can be re-written as:
scope :not_available, lambda {where("available = ?", false)}
note that ->
is replaced with lambda. A similar approach can be used with the default_scope as well.
So in my rails console I can now do this:
equip = Equipment.all
equip.is_available
The lambda is what actually does the query implementation.
It will produce an ActiveRecord::Relation
object, that contains list of available equipment.
This also gives us the opportunity to use the created scope in the controller in this manner:
def index
@equip = Equipment.is_available
render json: @equip, status: 200
end
or
def index
@equips = Equipment.all
render json: @equips, status: 200
end
def availability
@equip = Equipment.is_available
render json: @equip, status: 200
end
In the first code block, the route to index
will always display only available equipment and not all equipment. However, in the second code-block, index
will display a list of all equipment irrespective of whether it is available or not. Also, availability
will only display a list of all equipment that is available if I hit the availability
route. In this way, the controller is being used to decide what information is rendered. When it comes to which scope type to use, please avoid using default_scopes. Want to know why? see this article
Taking arguments
Named scopes can also take arguments like so:
class Equipment < ApplicationRecord
has_many :requests
has_many :customers, through: :requests
validates :brand, presence: true
validates :model, presence: true
validates :equipment_type, presence: true
validates :serial_no, presence: true
validates :accessories, presence: true
scope :not_available, ->(bool) {where("available = ?", bool) }
end
With the above arrangement, it is now possible to pass parameters from the controller when a request is made to the corresponding route.
Using Unscoped
If you have to work with default scopes, you may need to use the unscoped method to disable all currently applied default scopes. Let's work with an example:
class Equipment < ApplicationRecord
has_many :requests
has_many :customers, through: :requests
validates :brand, presence: true
validates :model, presence: true
validates :equipment_type, presence: true
validates :serial_no, presence: true
validates :accessories, presence: true
default_scope -> {where("available = ?", true) }
end
So we can now disable the scope in this manner
equip = Equipment.first
equip.unscoped
What can you do with scopes
Now let's see examples of what we can do with scopes:
- chaining scopes
we can chain scopes together to build a bigger SQL statement. See the example below:
class Equipment < ApplicationRecord
scope :available, ->{where("available = ?", true) }
scope :available_and_created, ->
{available.order(:created_at)}
end
First, we define the scope we want to chain, and then define a second scope. In the body of the second scope, we will chain the first scope and an ActiveRecord
method to make our query. The chaining can be seen, when we run the kind of query below in the rails console:
Equipment.brand.is_available
- Using Conditionals with scopes.
The scope named available
, in the code block below is a typical example of how to use conditionals with scopes.
class Equipment < ApplicationRecord
scope :available,(bool) ->{where("available = ?",
bool) if bool.present? }
end
Here we are saying, select all equipment if available is true.
- Calling multiple scopes in a class method scope
This is achievable by using the send()
method.
We can supply several scopes as arguments in the send method and call send on each one.
The code block below (chaining
) allows us to dynamically call as many scopes as we want. The code block below is another way of writing scopes and it is called a class method.
We define a class method with the self
keyword and it takes an array of arguments called multiple_method
. This is then supplied to the send
method in inject()
.
def self.chaining(multiple_method)
multiple_method.inject(self, :send)
end
Equipment.chaining(["scope_one", "scope_two"])
so in the controller we can have this kind of arrangement:
def index
if params[:equipment]
args = params[:equipment][:multiple_method]
@equip = Equipment.chaining(args)
else
@equip = Equipment.all
end
end
Summary & Conclusion
By now, using scopes should no longer be a difficult task. With scopes, we can easily create SQL queries on the fly.
Using the equipment API as examples in this article, we can now create a new scope from scratch, chain scopes together, use conditionals in scopes, and supply multiple scopes as an argument in another scope using the send method. The article focused strictly on creating scopes with the lambda approach. Another way to create scope is the use of a class method (which was mentioned when calling multiple scopes as arguments in a scope.).
Found this article to be useful? Please like, share or drop a comment below. You can also reach me via my twitter handle.
References
https://medium.com/@pojotorshemi/ruby-on-rails-named-scope-and-default-scope-74ee3db2a15f
https://devblast.com/b/jutsu-11-rails-scopes
https://jasoncharnes.com/importance-rails-scopes/
https://www.sitepoint.com/dynamically-chain-scopes-to-clean-up-large-sql-queries/
Top comments (2)
Great read, Tunde.
Thank you!