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 Message
s, 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 aQueryChain
as the solution. The current version, updated on 2016-04-10, has a more detailed reasoning for the resultingQueryChain
class. Thanks to Ashraf Hanafy for the feedback.