Query Objects in the Rails world - A Different Approach

January 26, 2016

If you have worked with Ruby on Rails before then you might be familiar with ActiveRecord scopes. Using them, you can achieve what many would consider very readable code. Let’s say that we have an application where we display an inbox where users receive messages.

class Message < ActiveRecord::Base
end

Now, let’s imagine that after reading a Message, it is marked as read, and let’s represent that with a read column in the database. Additionally, our users can either archive the Message or move it to the trash. We’ll represent this concept with a location column in the messages table.

Querying the database the Rails way

Let’s say that our users want to have a way to view unread messages in their inbox. Using ActiveRecord, you could achieve this with:

Message.where(location: 'inbox', read: false)

If you were to define scopes, you can make that a bit more readable and reusable:

class Message < ActiveRecord::Base
  scope(:inbox) { where(location: 'inbox') }
  scope(:read) { where(read: false) }
end

# ...

Message.inbox.unread

Scopes also make it easier to test each individual query against the database (you probably don’t want to mock the database in this context).

However, there are a few issues with this approach. If you have enough chained scopes, you can get pretty verbose with these, and the Message class quickly becomes the junkyard for all the database related operations. Plus, what if you have several places in the application where you want to load these messages? How do you test that you are loading the correct data from the database in these chained scopes? Do you have a separate scope that combines them both? What if you have multiple combinations?

Enter…

The Query Object pattern

A Query Object represents a database query and it fits perfectly into our requirements. It can be reused across different places in the application while at the same time hiding the querying logic. It also provides a good isolated unit to test.

Back when I was doing .NET, I used and embraced this concept as much as possible but for some reason when I started using Rails a few years ago, it kind of fell off my toolbelt.

An initial implementation in Ruby

A good initial implementation comes from CodeClimate’s 7 Ways to Decompose Fat ActiveRecord Models. Following their lead, you could come up with something like this:

class UnreadInboxMessagesQuery
  def initialize(relation = Message.all)
    @relation = relation
  end

  def find_each(&block)
    @relation
      .where(location: 'inbox')
      .where(read: false)
      .find_each(&block)
  end
end

Here, we initialize the query object with a default ActiveRecord::Relation represented by the Message.all scope. Then, whenever you want to access the unread inbox Messages, you just create a new instance of this object and iterate:

unread_inbox_messages = UnreadInboxMessagesQuery.new
unread_inbox_messages.find_each do |message|
  puts message.body
end

When I started using this approach heavily, I found myself wanting to use more than just #find_each and started delegating all the usual methods down to the resulting ActiveRecord::Relation:

class UnreadInboxMessagesQuery
  def initialize(relation = Message.all)
    @relation = relation
  end

  delegate :find_each, :each, :count, #... etc
    to: :results

  private

  def results
    @relation
      .where(location: 'inbox')
      .where(read: false)
  end
end

Then, I ran into another issue: what if I want to parameterize one of the query’s arguments? I wanted to be able to do something like this:

unread_inbox_messages_from_other_user = UnreadInboxMessagesQuery.new(from: other_user)

I could pass that as an additional param to the initialize, but then how do I initialize the default relation? I could try and pass that down to results, but then I cannot delegate methods down to results anymore. How could I make these query objects chainable so that I can reuse some of the logic a-la-Rails-scopes?

A Different Approach

After some thought, I came to the conclusion that passing the ActiveRecord::Relation down to the #results method instead of doing it in initialization would better fit my requirements, so I ended up with this (note that now I’m calling it scope instead of relation):

class UnreadInboxMessagesQuery
  def results(scope = Message.all)
    scope
      .where(location: 'inbox')
      .where(read: false)
  end
end

And you can use it like this:

unread_inbox_messages = UnreadInboxMessagesQuery.new.results

This makes it so that the outcome of the UnreadInboxMessagesQuery is a plain old ActiveRecord::Relation which makes it easy to access find_each, each, count, etc.

Then, if we want to achieve this:

unread_inbox_messages_from_other_user = UnreadInboxMessagesQuery.new(from: other_user).results

We can change the query object to:

class UnreadInboxMessagesQuery
  def initialize(from: nil)
    @from = from
  end

  def results(scope = Message.all)
    messages_from_user(scope)
      .where(location: 'inbox')
      .where(read: false)
  end

  private

  attr_reader :from

  def messages_from_user(scope)
    from ? scope.where(from: from) : scope
  end
end

Chainable Query Objects!

A nice consequence of this approach is that you can chain these query objects to achieve what ActiveRecord scopes give you. If you want to get all the unread inbox messages, then you can:

unread_messages = UnreadMessagesQuery.new
inbox_messages = InboxMessagesQuery.new
unread_inbox_messages = unread_messages.results(inbox_messages.results)

With this in mind, let’s see a new implementation of UnreadInboxMessagesQuery:

class UnreadInboxMessagesQuery
  # initialize...

  def results(scope = Message.all)
    unread_messages = UnreadMessagesQuery.new,
    inbox_messages = InboxMessagesQuery.new,
    messages_from_user = MessagesFromUserQuery.new(from: from)

    inbox_messages_from_user = inbox_messages.results(messages_from_user.results)
    unread_messages.results(inbox_messages_from_user)
  end

  # other private methods...
end

Now, following this approach, you could find yourself chaining a few queries from time to time, which might become a bit verbose. Let’s attempt to make it shorter by expressing the chain of queries as a collection of queries, over which you get the results from one item of the collection and plug it into the next one:

class UnreadInboxMessagesQuery
  # initialize...

  def results(scope)
    queries.inject(scope) do |new_scope, query|
      query.results(new_scope)
    end
  end

  # other private methods...

  def queries
    [
      UnreadMessagesQuery.new,
      InboxMessagesQuery.new,
      MessagesFromUserQuery.new(from: from)
    ]
  end
end

Figuring out how to write this inject every time would be quite the challenge. Let’s wrap that concept in a new QueryChain class that does the magic for us:

class QueryChain
  def initialize(*queries)
    @queries = queries
  end

  def results(scope)
    queries.inject(scope) do |new_scope, query|
      query.results(new_scope)
    end
  end

  private

  attr_reader :queries
end

And now, you can use the QueryChain to define other queries like this:

class UnreadInboxMessagesQuery
  # initialize...

  def results(scope = Message.all)
    queries.results(scope)
  end

  # other private methods...

  def queries
    QueryChain.new(
      UnreadMessagesQuery.new,
      InboxMessagesQuery.new,
      MessagesFromUserQuery.new(from: from)
    )
  end
end

That’s it for now. Enjoy!


Notes

  • The initial version of this blog post was published on 2016-01-26 and used the return value from one query’s #results as the parameter of another query, and then jumped into using a QueryChain as the solution. The current version, updated on 2016-04-10, has a more detailed reasoning for the resulting QueryChain class. Thanks to Ashraf Hanafy for the feedback.