DEV Community

loading...
Cover image for Design Patterns: Query Objects

Design Patterns: Query Objects

rwehresmann profile image Rodrigo Walter Ehresmann ・3 min read

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
Enter fullscreen mode Exit fullscreen mode
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

Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.

Discussion (0)

Forem Open with the Forem app