Query objects for complex database queries

Sarah Mitchell Feb 2026
2 tabs
class UserSearchQuery
  def initialize(relation = User.all)
    @relation = relation
  end

  def call(params)
    @relation = filter_by_status(params[:status])
    @relation = filter_by_role(params[:role])
    @relation = search_by_term(params[:search])
    @relation = filter_by_date_range(params[:created_after], params[:created_before])
    @relation = sort_results(params[:sort], params[:direction])
    @relation
  end

  private

  def filter_by_status(status)
    return @relation if status.blank?

    @relation.where(status: status)
  end

  def filter_by_role(role)
    return @relation if role.blank?

    @relation.where(role: role)
  end

  def search_by_term(term)
    return @relation if term.blank?

    @relation.where(
      "name ILIKE :term OR email ILIKE :term",
      term: "%#{term}%"
    )
  end

  def filter_by_date_range(start_date, end_date)
    @relation = @relation.where('created_at >= ?', start_date) if start_date.present?
    @relation = @relation.where('created_at <= ?', end_date) if end_date.present?
    @relation
  end

  def sort_results(sort_column, direction)
    return @relation.order(created_at: :desc) if sort_column.blank?

    direction = direction&.downcase == 'asc' ? :asc : :desc
    @relation.order(sort_column => direction)
  end
end

# Usage in controller:
# @users = UserSearchQuery.new.call(search_params)
# @users = UserSearchQuery.new(User.active).call(search_params)
2 files · ruby Explain with highlit

Query objects encapsulate complex database queries in reusable, testable classes. I use query objects when scopes become too complex or require parameters. Query objects compose smaller scopes, handle conditionals, and apply filtering logic. They're initialized with a base relation and parameters, returning an ActiveRecord relation. Query objects keep models clean and queries maintainable. They enable query reuse across contexts—controllers, jobs, services. Testing query objects is straightforward without complex setup. Naming query objects after their purpose improves code clarity. Following composition over inheritance, query objects build complex queries from simple parts. They're essential for applications with sophisticated search and filtering requirements.