This pattern allows us to create complex database queries for a specific domain. It helps us keep our models and controllers thiner by moving all database logic into a designated class.

Example:

# frozen_string_literal: true

class FindCars
  attr_reader :cars

  def initialize(cars = initial_scope)
    @cars = cars
  end

  def call(params = {})
    scoped = cars
    scoped = filter_by_brand(scoped, params[:brand])
    scoped = filter_by_model(scoped, params[:model])
    sort(scoped, params[:sort_field], params[:sort_direction])
  end

  private

  def initial_scope
    Car.all
  end

  def filter_by_brand(scoped, brand)
    return scoped unless brand

    scoped.where(brand: brand)
  end

  def filter_by_model(scoped, model)
    return scoped unless model

    scoped.where(model: model)
  end

  def sort(scoped, sort_field, sort_direction)
    scoped.order(sort_fields(sort_field, sort_direction))
  end

  def sort_fields(field, direction)
    sort_direction = direction == 'asc' ? 'asc' : 'desc'

    {
      brand: "brand #{sort_direction}",
      model: "model #{sort_direction}",
    }.fetch(field&.to_sym, 'created_at desc')
  end
end

To use it, just:

FindCars.new.call({ brand: 'Dacia' })

As you can see, this is great for complex pages where we have multiple filters, like for example an e-commerce product listing page. With the Query Object pattern we avoid adding multiple conditions in our controller or adding scopes/queries in our models.

Remember to not over-engineering and use this pattern only in places with certain complexity.