Skip to content

Patterns in Ruby: Query Object #105

@joshmfrankel

Description

@joshmfrankel

The Query Object Pattern

Description

  • Return Something / Change Nothing
  • Request a collection of data
  • File name describes the Query result
  • Always returns an ActiveRecord chain-able result (unless Enumerable)

When to use it

  • You have a complicated ActiveRecord chain
  • You have filters, sorting, pagination, or other configurable arguments
  • You need to use same query in multiple locations
  • You need to utilize raw SQL

When not to use it (Anti-patterns)

  • You need to produce a side-effect
  • You have a simple query that could be expressed as a Model scope

Before

class PostsController < ApplicationController
  def index
    filters = params[:filter]

    posts = current_user.organization.posts
    posts = posts.where(category: filters[:category]) if filters[:category].present?
    posts = posts.where(author: filters[:author]) if filters[:author].present?
    posts = posts.where(publised_at: filters[:publised_at_start]..filters[:publised_at_end]) if filters[:publised_at_start].present? && filters[:publised_at_end].present?
      
    posts = posts.order("#{params[:sort][:column]} #{params[:sort][:direction]}, created_at DESC") if params[:sort].present?

    @pagy, @posts = pagy(posts, page: params[:pagination][:page], per_page: params[:pagination][:per_page])
  end
end

After

class PostsController < ApplicationController
  def index
    @pagy, @posts = FilteredPostsQuery.new(
      scope: current_user.organization.posts, 
      filters: params[:filters],
      sort: params[:sort],
      pagination: params[:pagination]
    ).call
  end
end

class FilteredPostsQuery < ApplicationQuery
  attr_reader :scope, :filters, :sort, :pagination

  def initialize(scope:, filters:, sort:, pagination:)
    @scope = scope
    @filters = filters
    @sort = sort
    @pagination = pagination
  end

  def call
    result = filtered_posts(scope)
    result = sorted_posts(result)

    pagy(
      result,
      page: pagination[:page], 
      per_page: pagination[:per_page]
    )
  end

  private

  def filtered_posts(scope)
    scope = scope.where(category: filters[:category]) if filters[:category].present?
    scope = scope.where(author: filters[:author]) if filters[:author].present?
    scope = scope.where(publised_at: filters[:publised_at_start]..filters[:publised_at_end]) if filters[:publised_at_start].present? && filters[:publised_at_end].present?
    scope
  end

  def sorted_posts(result)
    result.order("#{sort[:column]} #{sort[:direction]}, created_at DESC") if sort.present?
    result
  end
end

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions