DEV Community

loading...

Refactoring towards query objects in Rails

Victor Hazbun
Founder at @BonsaiLabs
・1 min read

Query Objects

They store complex SQL queries, data aggregation, and filtering methods.

The problem

Say you have the following view.

class PostsController < ApplicationController
  def index
    @posts = Post
      .joins('LEFT OUTER JOIN users ON users.id = posts.author_id')
      .where(published: true)
      .where('view_count > ?', params[:min_view_count])
      .where('users.first_name LIKE ?', "#{params[:author_name]}%")
end
Enter fullscreen mode Exit fullscreen mode

It's a lot of code for a controller, so let's refactor it to use a Query Object.

The PostsQuery object

class PostsQuery
  def self.call(params = {}, relation = Post.all)
    relation = published(relation)
    relation = minimal_view_count(relation, params[:view_count])
    relation = author_first_name_like(relation, params[:first_name])
    relation
  end

  def self.published(relation)
    relation.where(published: true)
  end

  def self.minimal_view_count(relation, view_count)
    return relation unless view_count.present?
    relation.where('view_count > ?', view_count)
  end

  def self.author_first_name_like(relation, first_name)
    return relation unless first_name.present?
    with_authors(relation)
      .where('users.first_name LIKE ?', "#{first_name}%")
  end

  def self.with_authors(relation)
    relation.joins('LEFT OUTER JOIN users ON users.id = posts.author_id')
  end
end
Enter fullscreen mode Exit fullscreen mode

The implementation

Nice, let's see how the controller looks now.

class PostsController < ApplicationController
  def index
    @posts = PostsQuery.call(params)
  end
end
Enter fullscreen mode Exit fullscreen mode

Final thoughts

Not only looks better, testing this approach is much simpler.

This does not only applies for Rails apps, this pattern could be used anywhere in Ruby.

Discussion (0)