---
title: Um Caso para Objetos de Consulta no Rails
teaser: 'Quando usar objetos de consulta e como eles podem ser estruturados?

  '
tags: rails,ruby,query objects
author: Thiago Araújo Silva
published_on: 2022-05-13
---

Você provavelmente já ouviu falar em objetos de consulta, popularmente
conhecidos como "query objects": o principal propósito desse tipo de
objeto é encapsular uma consulta de banco de dados de forma
reutilizável, potencialmente combinável e parametrizável. Quando usar
esse tipo de objeto e como eles podem ser estruturados? Vamos explorar
esse tópico a seguir.

## Reutilizando filtros

No Rails, é fácil criar consultas de banco de dados. Com um modelo
Active Record, podemos facilmente construir uma consulta de maneira
_ad-hoc_:

```ruby
ServiceOffering
  .where(state: "CA")
  .joins(:vendor)
  .where(vendors: {education_level: "Kindergarten"})
```

Se espalharmos consultas como essa por nossa aplicação mas surgir a
necessidade de mudar o filtro "education level", teremos muitos
lugares no código para atualizar. A forma mais simples de corrigir
esse problema é criando métodos de classe para representar os filtros,
o que os torna reutilizáveis:

```ruby
class ServiceOffering < ApplicationRecord
  def self.by_state(state)
    where(state: state)
  end

  def self.by_education_level(education_level)
    joins(:vendor)
      .where(vendors: {education_level: education_level})
  end

  # ...
end
```

E podemos chamar a consulta acima da seguinte maneira:

```ruby
ServiceOffering
  .by_state("CA")
  .by_education_level("Kindergarten")
```

## Lidando com filtros opcionais

E se tivermos filtros opcionais? Vamos tornar `by_state` um deles:

```ruby
def self.by_state(state)
  where(state: state) if state.present?
end
```

Infelizmente isso quebra o encadeamento de filtros. No exemplo abaixo,
teremos um erro se `params[:state]` for nulo:

```ruby
# undefined method `by_education_level' for nil:NilClass
ServiceOffering
  .by_state(params[:state])
  .by_education_level(params[:education_level])
```

E a solução são os [Escopos do Active Record], que são indulgentes
para com valores nulos de forma a preservar a concatenabilidade dos
escopos quando um dos elos retornar `nil`:

[Escopos do Active Record]: https://guiarails.com.br/active_record_querying.html#scopes

```ruby
class ServiceOffering < ApplicationRecord
  scope :by_state, ->(state) { where(state: state) if state.present? }

  scope :by_education_level, ->(education_level) do
    if education_level.present?
      joins(:vendor)
        .where(vendors: {education_level: education_level})
    end
  end

  # ...
end
```

## Um caso para objetos de consulta no Rails: consultas de domínio

Nossa situação melhorou em relação ao primeiro snippet de código, mas
a última versão tem [_code smells_] bem sutis.

[_code smells_]: https://coodesh.com/blog/dicionario/o-que-e-code-smell/

1. Por se tratar de uma consulta com filtros encadeáveis, é bem
   possível que os filtros sejam utilizados sempre em conjunto;
   queremos ter certeza que eles serão [testados nas mesmas
   combinações] em que se encontram no ambiente de produção, mas não
   existe encapsulamento adequado para facilitar os testes;

2. Nossos filtros são opcionais; a lógica para pular um filtro é bem
   específica e pode não fazer sentido no contexto geral do modelo
   `ServiceOffering`. Ao utilizar um escopo como esse, podemos
   acidentalmente introduzir um bug em nossa aplicação se não
   contarmos com a possibilidade de filtros vazios.

3. Estamos fazendo `JOIN` com outras tabelas, o que não faz parte da
   responsabilidade central do nosso modelo. Sempre que uma consulta
   engloba mais de uma tabela ou atinge uma certo grau de
   complexidade, é sinal que pode ser representada através de um
   objeto de consulta.

[testados nas mesmas combinações]: https://github.com/thoughtbot/guides/pull/643#issuecomment-923151377

Um dos problemas com o _mindset_ do Rails é que novos filtros tendem a
ser representados através de métodos adicionais nos modelos, sendo
essa a solução padrão oferecida pelo framework. Com o passar do tempo,
os modelos tendem a ficar poluídos com filtros inexpressivos, e o
código de cada modelo a não formar um todo coerente.

Muitas vezes, um subconjunto de filtros é circunscrito a um subdomínio
particular da aplicação, então pode fazer sentido agrupá-los em uma
unidade coerente e de propósito único, de forma a preservar a
integridade dos modelos. E melhor ainda se usarmos nomes que façam
sentido dentro do domínio da nossa aplicação!

## Construindo um objeto de consulta

Dado que as "ofertas de serviço" (service offerings) pertencem ao
contexto de "mercado" (marketplace) da nossa aplicação, podemos muito
bem criar uma classe `MarketplaceItems` para representá-las. Hoje,
`MarketplaceItems` retorna objetos da classe `ServiceOffering`, mas
amanhã pode retornar outro tipo -- então abstrair nossa operação como
uma entidade de domínio certamente tem seus benefícios.

```ruby
# Por motivos de simplicidade, não estamos aplicando um namespace
# nessa classe
class MarketplaceItems
  def self.call(filters)
    scope = ServiceOffering.all
    
    if filters[:state].present?
      scope = scope.where(state: filters[:state])
    end

    if filters[:education_level].present?
      scope = scope
        .joins(:vendor)
        .where(vendors: {education_level: filters[:education_level]})
    end

    scope
  end
end
```

Chamar esse objeto de consulta ficou bem fácil:

```ruby
MarketplaceItems.call(state: "CA", education_level: "Kindergarten")
```

O nosso objeto funciona, mas não é muito escalável. Reatribuir uma
variável local repetidamente dentro de um `if` é cansativo e polui o
aspecto visual do código, especialmente ao lidar com um número
considerável de filtros. Será que métodos privados poderiam melhorar a
nossa semântica? Vamos ver:

```ruby
class MarketplaceItems
  class << self
    def call(filters)
      scope = ServiceOffering.all
      scope = by_state(scope, filters[:state])
      scope = by_education_level(scope, filters[:education_level])
      scope
    end

    private

    def by_state(scope, state)
      return scope if state.blank?

      scope.where(state: state)
    end

    def by_education_level(scope, education_level)
      return scope if education_level.blank?

      scope
        .joins(:vendor)
        .where(vendors: {education_level: filters[:education_level]})
    end
  end
end
```

Nem tanto. Ainda é necessário passar `scope` para os métodos privados
e gerenciar o estado da mesma variável. Isso não é tão ruim, mas
existe uma forma melhor de resolver o problema.

## Um objeto de consulta melhorado

A nossa sorte é que o Ruby é uma linguagem dinâmica, e ao contrário do
que alguns acreditam, estender um objeto (com `extend`) em tempo de
execução não é custoso. O Rails nos dá o método [`extending`] para
estender um objeto do tipo `ActiveRecord::Relation` em tempo de
execução, o que podemos usar em benefício próprio. Com tal truque em
mente, podemos refatorar o nosso objeto de consulta:

[`extending`]: https://apidock.com/rails/ActiveRecord/QueryMethods/extending

```ruby
class MarketplaceItems
  module Scopes
    def by_state(state)
      return self if state.blank?
      
      where(state: state)
    end

    def by_education_level(education_level)
      return self if education_level.blank?
      
      joins(:vendor)
        .where(vendors: {education_level: education_level})
    end
  end

  def self.call(filters)
    Marketplace
      .extending(Scopes)
      .by_state(filters[:state])
      .by_education_level(filters[:education_level])
  end
end
```

Perceba como a nossa classe está mais limpa! A única responsabilidade
do método `call` é construir a consulta e concatenar os filtros, e os
escopos encontram-se isolados dentro de um módulo. Ainda é necessário
retornar `self` nos escopos para não quebrar os filtros opcionais, no
entanto isso não é um demérito à solução que conseguimos construir até
agora.

Essa solução se destaca ao lidar com um número considerável de
filtros.

## Formas de estruturar um objeto de consulta

Existem muitas formas de estruturar um objeto de consulta, por exemplo:

- Injetar um escopo no construtor para dar mais flexibilidade ao objeto
  cliente:

```ruby
class MarketplaceItems
  def self.call(scope, filters)
    new(scope).call(filters)
  end

  def initialize(scope = ServiceOffering.all)
    @scope = scope
  end

  def call(filters = {})
    # ...
  end
end

# Aqui estamos limitando os itens do mercado a um
# vendedor em particular
MarketplaceItems.call(vendor.service_offerings, filters)
```

- Fazer o objeto retornar dados brutos ao invés de escopos Active
  Record, o que é útil em queries com requerimentos de performance:

```ruby
class MarketplaceItems
  COLUMNS = [:title]

  # Uma alternativa é ter um segundo método para retornar
  # dados brutos, além do método que retorna
  # ActiveRecord::Relation
  def self.call(filters)
    Marketplace
      .extending(Scopes)
      .by_state(filters[:state])
      .by_education_level(filters[:education_level])
      .pluck(*COLUMNS)
      .map { |row| COLUMNS.zip(row).to_h }
  end
end
```

- Construir a query com uma string SQL ao invés do construtor de
  queries do Active Record.

E muitas outras! O importante é que cada alternativa seja utilizada
com um real propósito que satisfaça as necessidades da aplicação sem
_overengineering_, a não ser que uma convenção particular exista no
projeto.

## Bonus: Escopos com comportamento Rails

Te incomoda retornar `self` ao lidar com filtros vazios? Se sim, é
possível melhorar a situação com um módulo `Scopeable`:

```ruby
module Scopeable
  def scope(name, body)
    define_method name do |*args, **kwargs|
      relation = instance_exec(*args, **kwargs, &body)
      relation || self
    end
  end
end
```

E agora podemos incluir (com `extend`) o `Scopeable` dentro do módulo
`Scopes`:

```ruby
module Scopes
  extend Scopeable

  scope :by_state, ->(state) { state && where(state: state) }

  scope :by_education_level, ->(education_level) do
    education_level && joins(:vendor)
      .where(vendors: {education_level: education_level})
  end
end
```

Não sou muito fã dessa DSL (e de escopos Active Record no geral)
porque ela é capaz de [prejudicar a descoberta de métodos] dentro do
código, então tendo a preferir métodos normais do Ruby -- e acho ainda
melhor marcar explicitamente os métodos que desejo transformar em
escopos, da seguinte maneira:

[prejudicar a descoberta de métodos]: https://github.com/thoughtbot/guides/pull/643#issuecomment-923151377

```ruby
module Scopes
  extend Scopeable

  def by_state(state)
    state.present? && where(state: state)
  end
  scope :by_state # Redefina by_state para que se comporte como um escopo

  # ...
end
```

Mas isso fica como um exercício para o leitor.

## Conclusão

Sou fã de DDD (domain-driven design) e objetos de propósito único,
então a minha preferência é manter os modelos livres de
responsabilidades não relacionadas às entidades representadas pelos
mesmos.

Modelos Active Record com métodos ou escopos de consulta são
toleráveis ao lidar com filtros genéricos e não encadeáveis, caso
contrário um objeto de consulta bem nomeado é uma excelente
alternativa a considerar!
