Query Object

A Query Object is a design pattern used to encapsulate complex database queries outside of models, making query logic more organized, reusable, and maintainable.

Table of Contents

What is a Query Object in Ruby on Rails?

A Query Object is a plain Ruby object responsible for handling database query logic that would otherwise clutter a model.

As applications grow, models can accumulate numerous scopes and query methods, making them difficult to maintain. Query Objects help by moving complex filtering, searching, sorting, and reporting queries into dedicated classes.

Unlike scopes, which are typically simple and model-specific, Query Objects can handle more advanced query logic involving multiple conditions, joins, and dynamic parameters. For example, a scope might look like:

class User < ApplicationRecord 
  scope :active, -> { where(active: true) }
end

This works well for a single, simple condition. But once a query needs to combine multiple optional filters, joins, or reusable logic across models, a Query Object becomes a better fit (see examples below).

Why Are Query Objects Useful?

Complex queries can make models bloated and difficult to understand. Query Objects help by:

  • Keeping models clean and focused on business logic
  • Centralizing complex query logic in one place
  • Improving readability and maintainability
  • Promoting reuse across controllers and services
  • Simplifying testing of database queries
  • Handling dynamic filtering and searching more effectively

They are especially useful in applications with advanced search, reporting, or filtering requirements.

How Do Query Objects Work?

A Query Object is typically a Ruby class that accepts parameters and returns an Active Record relation or query result.

Key components:

  • Query Class – Holds the query logic
  • Parameters – Inputs used for filtering or searching
  • Active Record Relations – Builds and returns database queries
  • Reusable Methods – Centralizes complex query behavior
  • Separation of Concerns – Keeps query logic separate from models

Example

Scenario 1: Filtering Users by Status Query Object (app/queries/users_query.rb)

class UsersQuery 
  def initialize(relation = User.all) 
    @relation = relation 
  end 
 
  def active 
    @relation.where(active: true) 
  end 
end

Using the Query Object

users = UsersQuery.new.active

Rather than placing this logic in the model or controller, it lives in a dedicated, reusable class.

Scenario 2: Building a Dynamic Search Query Query Object (app/queries/users_query.rb)

class UsersQuery 
  def initialize(relation = User.all) 
    @relation = relation 
  end 
 
  def search(name: nil, city: nil) 
    users = @relation 
 
    users = users.where("name LIKE ?", "%#{ActiveRecord::Base.sanitize_sql_like(name)}%") if name.present? 
    users = users.where(city: city) if city.present? 
 
    users 
  end 
end 

Using the Query Object

users = UsersQuery.new.search( 
  name: "John", 
  city: "Chicago" 
)

This dynamically applies filters based on the provided parameters. Note the use of sanitize_sql_like, which escapes special characters (% and _) in user input so they aren't misinterpreted as SQL wildcards.

Where to Use Query Objects?

  • Advanced search functionality
  • Dynamic filtering and sorting
  • Reporting and analytics queries
  • Queries involving multiple conditions
  • Database operations shared across multiple controllers
  • Applications with large or complex Active Record models

In Summary

A Query Object encapsulates database query logic in dedicated, reusable classes instead of models. It becomes especially useful once queries grow beyond what simple scopes can cleanly handle.