On October 17, 2012, Bryan Helmkamp, founder of Code Climate, wrote a blog post outlining 7 patterns to refactor fat ActiveRecord models in Ruby on Rails. Here at Crush & Lovely, this post is a core reference for all Rails developers on how to separate concerns, write modular, concise and expressive code, and make testing exceedingly simple.
Database queries, even simple ones, can be both repetitive and hard to read and comprehend. With more complex queries, especially ones that embed data from multiple collections or tables, this process can get messy to write and even messier to maintain.
Query objects provide a nice tool for extracting query logic and associated operations into a contained module, pulling the logic out into a more maintainable and readable structure, while also providing a very readable API where the query object is used.
Let's imagine an API endpoint that returns a JSON representation of all students that are currently passing. Without using Query Objects, we might have a function in an API controller method or a Service Object that could look like this (note the user of DetermineStudentPassingStatus, the example from the Service Objects post):
Not only are we entering into some deep layers of callback hell, we've also written some code that is very difficult to read. We can create a much more expressive module if we use a Query Object.
Encapsulating all associated operations for this query feels more organized and gives you an expressive API to integrate into your application. For example, in an Express controller method:
The data being returned to this API call will be raw, unformatted data directly from the data store, which is almost never what you want. For this example, the Query Object can be paired well with a View Object (covered next in this series), which provides a place for data transformations to prepare the data for presentation.
Another thing worth mentioning is that this pattern opens up some very interesting opportunities for composition. For example, there may be many places where you want to find all assignments for a given set of students, so we could extract that process into a separate Query Object and use it in the #fetchAssignmentsForCurrentStudents method.
Constructing Query Objects outside of the context in which they are used makes testing supremely simple. If you're using a testing database, it's all a matter of populating the necessary data to provide meaningful query results, running the Query Object, and then making sure the results are accurate.
In the next post, we'll take a look at View Objects, which are great tools for isolating view-specific model transformations.