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: Currency
and 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
Scopes
is defined, containing the queries; - In the class constructor, we use the ActiveRecord
extending
method to add these query methods to the@relation
variable.
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., self.gold
in 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.
Options if you like my content and would like to support me directly (never required, but much appreciated):
BTC address: bc1q5l93xue3hxrrwdjxcqyjhaxfw6vz0ycdw2sg06
Top comments (0)