Query Object pattern overview

2018, May 15    

Database queries are very common in the world of web applications, and even if you’re not particularly familiar with SQL, you’re probably using it regularly anyway. A single, not especially huge application may be firing out thousands of different queries per second, so regardless of the performance, maintaining the code that contains hundreds of queries may become really hard. Today we’ll take a look at the Query Object pattern, which is a simple way to make this part of an application easier to develop and maintain. But it depends, of course.

What it is

According to the definition, a Query Object is

"An object that represents a database query"

As simple as that, but what does it exactly mean? It means that you can form an SQL query by using a parameterized method, which generates the query for you. Thanks to that you don’t need to remember exactly how does the database schema look like, as you can refer to classes and fields rather than tables and columns when creating a Query Object finder methods.

When to use it

Obviously, Query Objects are not a silver bullet and they don’t fit every application. Actually, their usage seems to be quite limited. First of all, to make use of Query Objects you need a kind of data mapping between the database and the objects in your system. Without the implementation of such mechanism in your application, it seems to be hard to use Query Objects. They are also rarely used in the projects, which have a handbuilt data source layer.

I work mostly with Rails-based projects on my daily basis, which means that there should be an ORM in at least some of them by default. Even if we try to separate app’s data source layer with e.g. Repository pattern, we still can try to take advantage of Query Objects. In Rails world, I think a good moment to consider implementing Query Object comes when a model scope joins in other tables or interacts with more than one column. What’s worth to remember here is to always start with a minimal working example. Every time you feel you may need a Query Object, you should create a minimally functional one for your current needs and evolve it as those needs grow. Don’t forget to stick to a certain naming convention and proper namespacing to keep your codebase clean.

Example

Here’s an example of a very simple Query Object. As you can see, there’s nothing fancy here:

class FindPagesWithMediaQuery
  def initialize(media_resource)
    @media_resource = media_resource
  end

  def call
    Page.includes(components: :partials)
      where(partials: { media_resource_id: @media_resource.id })
  end
end

But when you take care of proper namespacing and take a look at this from a broader scope, you can see it may provide some value to the project:

Query objects namespacing example

The queries are nicely packed together in one place, instead of being scattered among the models/other classes.

Pros

  • no need to remember database schema - you can use objects and fields names instead of columns and tables
  • helps you to support e.g. multiple databases (you can create a query that produces different SQL depending on the database the query run against)
  • simple and easy to use (with not too complex queries)
  • optimizing to avoid multiple queries
  • easy testing
  • better encapsulation / less SRP violations
  • slimmer models in Rails

Cons

  • hard to implement complicated/ad-hoc queries
  • may lead to duplication in the SQL statements
  • when used improperly, you may end up with hundreds of classes and a lot of boilerplate code that doesn’t provide too much value, but a simple methods

TL;DR

As we know just because the patterns exist, doesn’t mean we should use them in our applications. However, I think in the ORM-based Rails world, Query Objects may be helpful and fulfill some of our needs without being difficult to implement. The trick is to limit their functionality to no more than we actually need.