Query Object Pattern in Rails
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.