Essential RubyOnRails patterns — part 2: Query Objects
Query Objects (also referred to as queries) is another pattern that helps in decomposing your fat ActiveRecord models and keeping your code both slim and readable. While this article is written with Ruby On Rails in mind, it easily applies to other frameworks, especially MVC based and applying
When to use Query Objects?
One should consider using query objects pattern when in need of performing complex queries on
ActiveRecord relation. Usually using scopes for such purposes is not recommended. As a rule of thumb, if scope interacts with more than one column and/or joins in other tables, it should be considered to be moved to query object — as a side-effect we can limit amount of scopes defined in our models to a necessary minimum. Also whenever a chain of scopes is to be dealt with, using query object as well should be considered. (read more…).
How to make most out of Query Object pattern?
#1 Stick to one naming convention
To make inventing query object class name easier, we may establish some base rules that we will than follow. One idea is to always suffix query object name with
Query, so we are constantly aware we’re dealing with query, and not
ActiveRecord descendant. Another idea is to always use pluralized name of a model, a given query is designed to work with. This way it will be clear for us, that i.e.
RecentProjectUsersQuery will return a users’ relation when called. Whatever approach we choose to apply, most of the time it is beneficial to stick to one way of naming classes of given pattern, as it tends to reduce confusion whenever we need to introduce a new class.
#2 Use .
call method returning a relation to call query objects
In contrast to service objects, where we have some level of freedom in naming the method dedicated to use a service object, in order to make the most out of query object pattern in rails, we should implement
.call method that returns a relation object. If we follow this rule, such query objects can be easily used to construct scopes if required. (read more…)
#3 Always accept relation like object as first argument
It is a good practice to accept a relation as a first argument when calling query objects we introduce. Not only is this required when using query objects as scopes (see recommendation above) but also this way we can make our query objects chainable, which gives us an additional level of flexibility. To keep the ease of use intact, make sure to provide a default entry relation, so such query object can be used without providing an argument. It is also important to always return relation from query object with the same subject (table) as the relation query object was provided with.
#4 Provide a way to accept extra options
While sometimes such necessity can be avoided by subclassing the existing or introducing the new query objects, sooner or later we will need to accept some extra options to query object we’ve introduced. This can be used to customize the logic of how given query object returns its results, that may effectively turn such query object into a flexible filter. To maintain good level of readability it is recommended to only pass such options as hash / keyword arguments and always provide default values.
#5 Focus on readability of your querying method
Regardless of whether we decide to store the core logic of our query in the
.call method itself or any other method of our query object, we should make it as readable as possible. This will be the first place other developers will look into to find out what such query object is about, so this little extra effort may make their lives easier.
#6 Group query objects in namespaces
Depending on complexity of our project and to what extent it takes advantage of
ActiveRecord, we may end up with quite a lot of query objects. To arrange our code better it is a good practice to group similar query objects into namespaces. One idea on grouping is to use name of model those queries deal with, but it can be anything reasonable. As usual, by sticking to one way of grouping query objects, it will be easy for us to decide on appropriate location for such class once we introduce a new one. Storing all of our query objects in
app/queries is also recommended.
#7 Consider delegating all methods to result of .call
One might consider implementing method_missing for query object to delegate all methods to the result of
.call method. This way a query object could be used just as a regular relation — i.e.
RecentProjectUsersQuery.where(first_name: “Tony”) instead of
RecentProjectUsersQuery.call.where(first_name: “Tony”). However, as with any case of metaprogramming, following this approach should be a conscious and justified decision.
A query object is a simple, easily testable pattern that helps in abstracting queries, relations and scope chains with complex implementation. By following a few simple rules outlined above, we can make sure that this pattern remains readable, flexible and easy to use not only for us, but, first of all, for others who might work with our code in the future. An example below presents one way of implementing such object.
DEFAULT_RANGE = 2.days
def self.call(relation = User.all, time_range: DEFAULT_RANGE)
where('projects.created_at > ?', time_range.ago).
If you feel you might need some simple abstraction around query object pattern, consider using thin wrapper provided by the rails-patterns gem.