---
title: A Case for Query Objects in Rails
teaser: 'When is it best to reach for query objects and how can we best structure
  them?

  '
tags: rails,ruby,query objects
author: Thiago Araújo Silva
published_on: 2022-05-13
---

You may have heard of query objects before: their main purpose is to
encapsulate a database query as a reusable, potentially composable,
and parameterizable unit. When is it best to reach for them and how
can we structure them? Let's dive into this topic.

## Reusing query filters

In Rails, it's very easy to create database queries. Provided that
we've defined an Active Record model, we can easily construct an
ad-hoc database query:

```ruby
ServiceOffering
  .where(state: "CA")
  .joins(:vendor)
  .where(vendors: {education_level: "Kindergarten"})
```

If we spread queries like that throughout our application but need to
change how the "education level" filter works, there will be many
touchpoints to update. The simplest fix for that is to represent our
filters with class methods to make them reusable:

```ruby
class ServiceOffering < ApplicationRecord
  def self.by_state(state)
    where(state: state)
  end

  def self.by_education_level(education_level)
    joins(:vendor)
      .where(vendors: {education_level: education_level})
  end

  # ...
end
```

And call our query like this:

```ruby
ServiceOffering
  .by_state("CA")
  .by_education_level("Kindergarten")
```

## Handling optional filters

What if our filters are optional? Let's make `by_state` optional:

```ruby
def self.by_state(state)
  where(state: state) if state.present?
end
```

Unfortunately, that breaks our filter chain. In the example below,
if the value of `params[:state]` is blank, we get an error:

```ruby
# undefined method `by_education_level' for nil:NilClass
ServiceOffering
  .by_state(params[:state])
  .by_education_level(params[:education_level])
```

And the solution for that is [Active Record scopes], which are lenient
to `nil` return values as to preserve the chainability of our scopes
when they return `nil`:

[Active Record scopes]: https://guides.rubyonrails.org/active_record_querying.html#scopes

```ruby
class ServiceOffering < ApplicationRecord
  scope :by_state, ->(state) { where(state: state) if state.present? }

  scope :by_education_level, ->(education_level) do
    if education_level.present?
      joins(:vendor)
        .where(vendors: {education_level: education_level})
    end
  end

  # ...
end
```

## A case for query objects: domain-specific queries

Our situation has certainly improved from the first code snippet, but
the latest version of our query still has a few smells.

1. It's a chainable query, so it's very likely that our filters
   will always be used together; we want to make sure our filters are
   [tested in the same combinations] they will actually be used in
   production, but no proper encapsulation exists;

2. Our filters are optional; the logic to skip a filter is very
   specific and may not make sense in the general context of our
   `ServiceOffering` model. If we reuse a scope like that, we may
   inadvertently introduce a bug in our application if we're not
   counting with the possibility of blank filters;

3. We are joining with other tables, which feels outside of our
   model's responsibility; whenever our query spans more than one table
   or reaches a certain complexity threshold, it's a sign we could
   represent it with a query object.

[tested in the same combinations]: https://github.com/thoughtbot/guides/pull/643#issuecomment-923151377

The main problem with the default Rails mindset is that whenever we
need a new filter, the easiest way around is to add a new method to
our model. Over time, our model tends to get littered with
inexpressive filter bits and its body doesn't form a coherent whole.

Sometimes, a subset of filters will only be used (or reused) in a
particular subdomain, so it makes sense to group them together as a
coherent unit with a single purpose and leave our models alone. And it
gets even better if we name our queries after something that makes
sense within our domain.

## Building a query object

If the service offerings belong to the marketplace context of our
application, we could create a `MarketplaceItems` class to represent
it. Today, `MarketplaceItems` returns `ServiceOffering` objects, but
tomorrow it may return something else -- so abstracting our operation
as a proper domain entity is surely beneficial.

```ruby
# For simplicity's sake, we are not applying a namespace to this class
class MarketplaceItems
  def self.call(filters)
    scope = ServiceOffering.all

    if filters[:state].present?
      scope = scope.where(state: filters[:state])
    end

    if filters[:education_level].present?
      scope = scope
        .joins(:vendor)
        .where(vendors: {education_level: filters[:education_level]})
    end

    scope
  end
end
```

Calling this query object is very simple:

```ruby
MarketplaceItems.call(state: "CA", education_level: "Kindergarten")
```

Our query object works, but is not very scalable. Repeatedly
reassigning a local variable under an `if` condition is tiring and
muddles our code, especially when more than a few filters are
involved. Would private methods improve our semantics? Let's see:

```ruby
class MarketplaceItems
  class << self
    def call(filters)
      scope = ServiceOffering.all
      scope = by_state(scope, filters[:state])
      scope = by_education_level(scope, filters[:education_level])
      scope
    end

    private

    def by_state(scope, state)
      return scope if state.blank?

      scope.where(state: state)
    end

    def by_education_level(scope, education_level)
      return scope if education_level.blank?

      scope
        .joins(:vendor)
        .where(vendors: {education_level: filters[:education_level]})
    end
  end
end
```

Not by much. We still need to pass the `scope` around and keep track
of a local variable. That's relatively manageable and not entirely
bad, but there's a better way around it.

## A better query object

Luckily, Ruby is a dynamic language and contrary to what some people
believe, extending an object at runtime is not expensive. Rails
provides us with the [`extending`] method to extend an
`ActiveRecord::Relation` at runtime, which we can use to our
advantage. Let's refactor our query object with that trick in mind:

[`extending`]: https://apidock.com/rails/ActiveRecord/QueryMethods/extending

```ruby
class MarketplaceItems
  module Scopes
    def by_state(state)
      return self if state.blank?
      
      where(state: state)
    end

    def by_education_level(education_level)
      return self if education_level.blank?
      
      joins(:vendor)
        .where(vendors: {education_level: education_level})
    end
  end

  def self.call(filters)
    ServiceOffering
      .extending(Scopes)
      .by_state(filters[:state])
      .by_education_level(filters[:education_level])
  end
end
```

Notice how much cleaner our query object feels! The `call` method is
only concerned with building our query and filter chaining, and our
scopes are neatly separated in a module. We still need to return
`self` in our scopes on blank parameters, but that does not nullify
the merits of the approach we were able to come up so far.

That approach really shines when dealing with a considerable number of
filters.

## Ways to structure a query object

There are many ways to structure a query object, for example:

- Injecting a scope in the initializer to provide more flexibility to
  the caller:

```ruby
class MarketplaceItems
  def self.call(scope, filters)
    new(scope).call(filters)
  end

  def initialize(scope = ServiceOffering.all)
    @scope = scope
  end

  def call(filters = {})
    # ...
  end
end

# Scope marketplace items to a particular vendor
MarketplaceItems.call(vendor.service_offerings, filters)
```

- Making it return raw data rather than an Active Record scope, which
  is useful when dealing with performance-sensitive queries:

```ruby
class MarketplaceItems
  COLUMNS = [:title]

  # An alternative is to have a second method to return
  # raw data, in addition to a main method that returns
  # an ActiveRecord::Relation
  def self.call(filters)
    ServiceOffering
      .extending(Scopes)
      .by_state(filters[:state])
      .by_education_level(filters[:education_level])
      .pluck(*COLUMNS)
      .map { |row| COLUMNS.zip(row).to_h }
  end
end
```

- Using raw SQL instead of the Active Record query builder.

And many others! What's important is that each option is used with a
purpose that serves the app without overengineering, unless a hard
convention exists on your project.

## Bonus: Scopes with Rails-like behavior

If we're bothered with returning `self` when a filter is blank, we can
easily solve that problem with a `Scopeable` module:

```ruby
module Scopeable
  def scope(name, body)
    define_method name do |*args, **kwargs|
      relation = instance_exec(*args, **kwargs, &body)
      relation || self
    end
  end
end
```

Now we can extend our `Scopes` module with `Scopeable` and shorten our
scopes a little bit:

```ruby
module Scopes
  extend Scopeable

  scope :by_state, ->(state) { state && where(state: state) }

  scope :by_education_level, ->(education_level) do
    education_level && joins(:vendor)
      .where(vendors: {education_level: education_level})
  end
end
```

I'm particularly not a fan of that approach (and of Active Record
scopes in general) because it may [hinder the discoverability of our
code], so I'm generally biased towards normal Ruby methods. I'd even
prefer to explicitly mark the methods I want to behave as scopes with
a `scope` annotation, as follows:

[hinder the discoverability of our code]: https://github.com/thoughtbot/guides/pull/643#issuecomment-923151377

```ruby
module Scopes
  extend Scopeable

  def by_state(state)
    state.present? && where(state: state)
  end
  scope :by_state # Decorate by_state to make it behave like a scope

  # ...
end
```

But that is left as an exercise to the reader.

## Wrapup

I'm a fan of domain-driven design and single purpose objects, so I
usually prefer to keep my models clean of specific cruft that does not
pertain to the general model entity.

Active Record models with query methods are definitely bearable when
dealing with generic and non-chainable filters, otherwise a well-named
query object is a great alternative to consider!
