Welcome back folks! I hope you have been enjoying the series on design patterns so far. Today I wanted to carve out some time to talk about query objects. Now you might say something like, “but ActiveRecord already has scopes!”..and you would be 100% correct. The problem is in the ease of use of scopes and their ability to chain. Take the following example:
Do you see? It’s almost too easy to take all of these utility scopes and chain them to create some powerful query. While there is nothing inheritantly wrong in chaining scopes, there is a price paid. In cases like this the price is lack of meaning and difficulty in tests. By “lack of meaning” what I’m really trying to say is that it’s unclear why this is the correct query. Let’s explore this a little by pulling out a query object.
I know all we did was extract the query logic, but let’s take a look at the controller now.
We can clearly see that a collection is being fetched specifically for our use case of reporting income. That is pretty cool. This also gives us only one place to change if the requirements for this report change. Not sold yet? Let’s add some requirements! After all, we are building real world applications and concepts like reports change often. Here are the new requirements:
- All existing conditions, except the date range, are solid
- The report needs to allow filtering by a start and end date
- The report needs to allow filtering by the status of a customer
- The report needs to allow filtering by a minimum amount of the transaction
- The report needs to allow filtering by user_id
Wowza! Leave it up to the finance team to add complexity to our simple little report. But do not fear, our query object is here :)
Our little query object sure grew up fast! It’s now responsible for more than the original static concept of a query for the income report. It also knows how to filter that query. Even though that’s true, I’d argue it doesn’t violate the Single responsibility principle. The only responsibility our query object has is to return the data needed for an income report. Let’s move onto the controller and see how these changing requirements impacted that code.
Well look at that. Our controller doesn’t change that much after all. The only thing we need to do is accept filter parameters and pass them to the query object. Not to shabby. Before I leave you to try this pattern on your own, I’ll enumerate a few good use-cases I have found for query objects:
- When there are complex queries or scope chaining
- When the query needs filtering that is not re-used
- When the scope/query interacts with more models then itself
And here are some rules I apply when creating this query objects:
- Accept a scope as the first argument (scope, relation, etc) are all good names
- Stick to a naming convention (DoSomethingQuery, OtherThingQuery, etc)
- Use a Base query object to extract away the common interface (attribute readers, initialize, etc)
- Always return an ActiveRecord scope
- Define .resolve as a class method that accepts the same arguments as
initialize(helps with testing)
There are a lot if little details we kind of glossed over here. Details such as the littly bit of meta-programming in the query object for applying filters, the usage of
yield_self, and the lack of data santization on the filters themselves. These are the things I leave to you, the readers, to explore yourselves. What you see here is how I approach these problems in real world RoR applications servicing hundreds to thousands of users on a daily basis. This pattern has been relatively easy to maintain and has stood the test of time on numerous RoR applications as old as eleven years. Don’t just take my word for it though. Try it out in your application(s) and see how it goes.
Next week, instead of moving onto the next design pattern (Presenters) we will be covering how to test the code you saw above. So buckle up and get ready for some test slinging!
As always, thank you for taking the time to read my ramblings. You truly are the best!