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.