DEV Community

loading...
Cover image for All About ActiveRecord Scopes

All About ActiveRecord Scopes

tundeiness profile image Tunde Oretade ・6 min read

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

or

  def index
    @equips = Equipment.all
    render json: @equips, status: 200
  end

  def availability
    @equip = Equipment.is_available
     render json: @equip, status: 200
  end
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

So we can now disable the scope in this manner

 equip = Equipment.first
 equip.unscoped
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
  • 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
  Equipment.chaining(["scope_one", "scope_two"])
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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/

Discussion

pic
Editor guide
Collapse
oluseyeo profile image
oluseyeo

Great read, Tunde.

Collapse
tundeiness profile image