It’s quite probable that at work you have encountered with overloaded models and a huge number of calls in the controllers for quite a lot of time. Basing on the knowledge in the Rails environment, in this article, I am going to propose a simple solution to this problem.
A very important aspect of the rails application is to minimize the number of redundant dependencies, which is why the entire Rails environment has recently been promoting the service object approach and the use of the PORO (Pure Old Ruby Object) method. A description of how to use such a solution you will find here. In this article, we will solve the concept step by step and adapt it to the problem.
In a hypothetical application, we are dealing with a complicated transaction system. Our model, representing each transaction, has a set of scopes, that help you to get data. It is a great job facilitation as it can be found in one place. However, this doesn’t last for long. With the development of the application, the project is becoming more and more complicated. Scopes no longer have simple ‘where’ references, we lack data and start to load relationships. After a while, it reminds a complicated system of mirrors. And, what’s worse, we do not know how to do a multi-line lambda!
Below, you will find an already expanded application model. The payment system transactions are stored in. As you can see in the example below:
The model is one thing, but as the scale of our project increases, the controllers also start to swell. Let’s look at the example below:
Here we can see many lines of chained methods alongside with additional joins that we do not want to perform in many places, only in this particular one. The attached data is later used by the apply_filters method, which adds the appropriate data filtering, based on the GET parameters. Of course, we can transfer some of these references to scope, but isn’t this the problem that we are actually trying to resolve?
Since we already know about a problem we have, we must solve this. Based on the reference in the introduction, we will use the PORO approach here. In this exact case, this approach is called the query object, which is a development of the service objects concept.
Let’s create a new directory named “services”, located in the apps directory of our project. There we will create a class named `TransactionsQuery`.
As a next step, we need to create an initializer where a default call path for our object will be created
Thanks to this, we will be able to transfer the relationship from the active record to our facility. Now we can transfer all our scopes to the class, which are needed only in the presented controller.
We still miss the most important part, ie collecting data into one string and making the interface public. The method where we will stick everything together will be named a “call”.
What is really important is that we will use the @scope instance variable there, where the scope of our call is located.
The entire class presents itself as the following:
After our cleaning-up, the model looks definitely lighter. There we focus only on the data validation and relationships between other models.
The controller has already implemented our solution; we have moved all additional queries to a separate class. However, the calls, we did not have in the model, remain an unresolved issue. After some changes, our index action looks like this:
In the case of implementing good practices and conventions, a good idea may be to replace all similar occurrences of a given problem. Therefore, we will move the SQL query from the index action to the separate query object. We will call this a `TransactionsFilterableQuery` class. The style, which we prepare the class in, will be similar to that one presented in `TransactionsQuery`. As part of the code changes, a more intuitive record of large SQL queries will be smuggled, using multiline character strings called heredoc. The solution available you will find below:
In case of changes in the controller, we reduce the mass of lines by adding the query object. It is important that we separate everything except the part responsible for pagination.
Query object changes a lot in the approach to writing SQL queries. In ActiveRecord, it is very easy to place all business and database logic in the model since everything is in one place. This will work quite well for smaller applications. As the complexity of the project increases, we set the logic to other places. The same query object allows you to group member query queries into a specific problem.
Thanks to this, we have an easy possibility of the code’s later inheritance and because of duck typing, you can also use these solutions in other models. The disadvantage of this solution is a larger amount of code and fragmentation of responsibility. However, whether we want to take up such a challenge or not, depends on us and how badly we are disturbed by fat models.