Share query logic in ActiveRecord models

Tram Ho

I recently worked on an application where we have a class method / scope is used to return a specific group of users. This is fine and good until I find myself needing to grasp the same logic at the instance level. Ideally, I was hoping to avoid logical duplication, and at the same time keep things efficient.

Users in my application can be configured to receive notifications. I have a class method on the User model that is used to query for all users who can receive messages:

In other words, the criterion for can_receive_alerts is that the user has enabled email or sms notifications and at least one AlertConfiguration is defined. This method will result in the following SQL query:

I want to be able to reuse this same query logic at the instance level, so that I can use user.can_receive_alerts ?. Thankfully, I can take advantage of the ActiveRecord's lazy query evaluation (which I will explain at the end of this article) and build on the relationship returned from the class method :

This new Intance method , can_receive_alerts ?, directly reuses the query logic from the class method , which can be used with the user object. Using the where(id: id) , and then using exists? To create an optimal efficient SQL query:

In the past, I might have copied logic at instance level (and made sure they were out of sync) or could have used scope to scan all users. Instead, I have an exact query that will return only the absolute minimum data needed, while reusing the query logic directly from the scope.

ActiveRecord's lazy query evaluation

Using User.find (1) will return an unknown object – it will find the user with ID = 1 and give it to you as a Ruby object. But this behavior is really unusual. Most queries do not actually return a Ruby object, but only spoof it. For example:

It may look like it returns an array containing a User object like:

But try running User.where (id: 1) .class and you will see that it is not an Array, it is actually an instance of ActiveRecord :: Relation. Relation looks like array but it can do many things that array n’t do.

The ActiveRecord query works and returns the relation . There, basically, there’s no reason to actually ask the database to execute the query until the last minute possible. What if you really never need to use that query? What if you wanted to make it more complicated before implementing it? Relation gives you much more flexibility and use efficiency than your database.

Relation only done when absolutely necessary to know what is inside them. So, if the controller takes 5 posts on the blog using @posts = Post.limit(5) , then it passes the view a relation . It is only when the code in the view actually calls a method on @posts (like @posts.first.title ) that the query will be run and the relationship will be stored as a real Ruby object in memory.

This behavior can be a bit difficult to observe if you use something like the Rails Console to test them, because queries will actually be run right inside the console because it runs in the background as something like a method .inspect on relation , requires queries to be run. But try building a query like the one above and testing its class , you will see it returns ActiveRecord :: Relation.

Chia sẻ bài viết ngay

Nguồn bài viết : Viblo