Scopes for Rails ActiveRecord Models
Recently I was going through my old projects and have seen very long ActiveRecord search queries in controllers in some of my projects. SQL database queries are handled simply but powerfully Ruby language by ActiveRecord in Rails so that we don’t have to write tedious long SQL codes. But still, as your project and models get large and complex, ActiveRecord search queries become long chains that will make your code harder to read and maintain.
These long and maybe longer queries make your controller “fat”, controllers should be light-weight, they should do CRUD actions for your records in the database not try to filter out the information. The model should do the job, the job to refine, filter, and define the information pushed to the controllers.
The good thing is Rails provide us Scopes, you can see it as a class method for your model which returns ActiveRecord association.
If we follow our example above, we can see Album database record includes category column. For instance, let’s assume we want to return all albums with the category of “rock” with the ActiveRecord scope.
Album.rock now represents the query
It is important to note that
Album.rock does not return an array but an
ActiveRecord::Relation which means that we can continue to invoke associations on it.
Album.rock.where("song_count>11") will work as intended.
Note that this is simply a syntax sugar for an actual class method seen below;
As I mentioned just above that a scope returns an ActiveRecord relation which means that if there are no results, it will return an empty array not
nil , but class methods will halt when they hit a no results query. This has an important implication on how you can efficiently use scopes rather than class methods.
This gives scopes feature of chainability without a
nil result problem,
Album.rock.long will produce all albums that are both “rock” genre and have more than 10 songs on them. You can still chain them with other methods that exist in ActiveRecord,
Album.rock.long.count will work normally.
We can use scopes when we are creating records, Instead of using
Album.create(name:"New Album",category:"rock") It is also possible to do the following
We can generalize our scope even furthermore;
This scope will accept arguments
Album.genre("jazz") , this query will serve any albums with the category “jazz”. Scopes will be more convenient when used in controllers in this way,
Don’t use the default_scope
Rails have default_scope instance private method ready for use but it might be dangerous to use. Default scope allows you to set a scope that’s always called when you access your model, let’s take a look at the following example,
Whenever we query for
Article.all it will automatically apply query filter for published articles, this might be overlooked and cause problems as the project grows. If you wish to retrieve all Articles regardless of its publish state, you could use
Article.unscoped.all , but I can imagine it would give developer headaches if they forget they are using default_scopes in the first place! The problem is not over! Whenever an Article is created, it will automatically have the attribute published as
true , default_scope will affect your model initialization. This is a huge factor you need to consider when using default_scopes.
Performance and Preloading(Eager Loading)
A scope will always query the database instead of just executing the query, that will become problematic when you loop over this query for instance in a view. It will create N+1 Query which is one of the biggest causes of slow Rails applications. If left unattended, Rails could make an unnecessary amount of SQL queries to the database when associated models exist in the application. We can remedy this situation using includes method of ActiveRecord in our queries and consequently applying Eager Loading concept. You can read more about it here.
Finally, we are here, there isn’t any consensus on how we should test scopes or even test it at all! But I believe scopes can get complicated and prone to errors, therefore, should be tested. There are two ways of testing scopes in which we use FactoryBot mock models to do database query or we can convert our ActiveRecord queries to pure SQL and compare them this involves no database queries and much simpler.
Note that the second test does not need any FactoryBot generated database record, we are simply comparing pure SQL code query.
One of the many ways that you can keep your controllers light-weight is using scopes instead of long chains of database queries. Scopes are very customizable even you can call them between your models to keep your code DRY.
But still, you need to address performance issues if your queries have large results, if you don’t optimize your scopes with eager loading, your scopes will slow down your Rails application significantly depending on the volume of your queries.
Finally, I have shown two different simple test structures for scopes in one of which you don’t have to do any database query and compare SQL query codes.