Scopes for Rails ActiveRecord Models

Photo by Jp Valery on Unsplash

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 database record includes 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 Album.where("category:rock")

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 Album.rock.create(name:"New Album")

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 in the first place! The problem is not over! Whenever an Article is created, it will automatically have the attribute published as true , will affect your model initialization. This is a huge factor you need to consider when using

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 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 method of ActiveRecord in our queries and consequently applying concept. You can read more about it here.

Testing Scopes

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.

Conclusion

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 , 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.

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Ali Erbay

Ali Erbay

7 Followers

Full Stack Developer BDD/TDD || Ruby on Rails || React || React Native || NodeJS || Express https://github.com/kermit-klein