Query Object is a Design Pattern you can use to avoid/starting fixing God Classes. A God Class is nothing more than a class that does more than should do and becomes one of the biggest files in your project regarding number lines.
If you ask me, it's barely impossible to not face at least one User model with these characteristics throughout your software developer career 😅
Query objects are most used to clean up your models from queries. Let's jump straight to a real-world example of how you can build one:
class CurrenciesQuery def initialize(relation = Currency.all) @relation = relation.extending(Scopes) end def self.gold Currency.find('XAU') end def relation @relation end module Scopes def all_except(code) self.where.not(code: code) end end end
class ExchangeRatesQuery def initialize(relation = ExchangeRate.all) @relation = relation.extending(Scopes) end def relation @relation end def self.from_gold ExchangeRate.find_by!( base_currency_code: 'BRL', currency_code: CurrenciesQuery.gold.code ) end module Scopes def from(base_currency_code) self.where(base_currency_code: base_currency_code) end def to(currency_code) self.where(currency_code: currency_code) end end end
Let me give a bit of context about these query objects first. It represents two models:
ExchangeRate. Currencies are fiat currencies and gold, and each one has many exchange rates. An exchange rate is a record that says that base currency X is worthed a given amount of currency Y.
I added two query objects to you observe the similarities of how they are built:
- Inside the class, a module
Scopesis defined, containing the queries;
- In the class constructor, we use the ActiveRecord
extendingmethod to add these query methods to the
With that, we can simply run things like
ExchangeRatesQuery.new.relation.from('BRL').to('USD'). That gives us the exchange rate from BRL to USD (how much 1 BRL is worthed in USD). As you can see, is also a personal preference of mine to define fixed params in query objects (e.g.,
CurrencieQuery, to identify which record represents the gold currency).
In my Rails projects, I'm used to having the query objects in
app/queries folder, with one query object per model (when necessary). I like to do so for one reason: totally decoupling queries from models.
You can also decide on creating query objects for one specific query each. With this approach, you probably wanna create query objects just for more complicated queries. For instance, check this one from Hound repository:
class RecentBuildsByRepoQuery NUMBER_OF_BUILDS = 10 static_facade :call def initialize(user:) @user = user end def call Build.find_by_sql([<<-SQL, user_id: @user.id, limit: NUMBER_OF_BUILDS]) WITH user_builds AS ( SELECT builds.id FROM builds INNER JOIN repos ON builds.repo_id = repos.id INNER JOIN memberships ON repos.id = memberships.repo_id WHERE memberships.user_id = :user_id LIMIT 1000 ), recent_builds_by_pull_request AS ( SELECT distinct ON (repo_id, pull_request_number) builds.* FROM builds INNER JOIN user_builds ON user_builds.id = builds.id ORDER BY repo_id, pull_request_number, created_at DESC, id DESC ) SELECT * FROM recent_builds_by_pull_request ORDER BY created_at DESC LIMIT :limit SQL end end
If you look in the models from this same repository, you'll find that simple queries stay there, and that is totally fine once your class isn't too big, messy, and take care of many things.
In the end, the best approach is the one your team agrees to keep and go forward. Query objects aren't a requirement, boot an approach to keep your code more maintainable.
This is it! Feel free to share how you've been using query objects in your project. If you have any comments or suggestions, don't hold back, let me know.