Query objects store complex SQL queries, data aggregation and filtering methods.
The goal of this pattern is to remove code for querying sets of objects from models/controllers and to provide a simple yet powerful interface for complex data aggregation.
In practice
Query objects live in the app/queries
folder.
Their naming convention is similar to that of controllers. Each object should bear the plural of the name of the model it queries suffixed by the word 'Query'. E.g. an object that queries articles should be called ArticlesQuery.
Each object should be passed a relation as an optional argument that it queries the data from. If no relation has been passed it defaults to querying all objects.
# queries all articles
ArticlesQuery.new
# this is the same as the example above
ArticlesQuery.new(Article.all)
# queries only published articles
ArticlesQuery.new(Article.where(published: true))
Query objects can be used in model scopes and relation conditions. They should be accessible and useable from any location in your codebase.
class Article < ActiveRecord::Base
has_many :views, -> { ViewsQuery.new.organic_views }
scope :published, -> { ArticlesQuery.new.published }
# model implementation ...
end
Implementation
Each query object implementation should resemble the following example.
class ArticlesQuery
attr_reader :relation
def initialize(relation = Article.all)
@relation = relation
end
def published
# method implementation ...
end
private
def custom_sql
# custom SQL query ...
end
end
Examples
We have an Article model that has the following fields:
- author_id
- title
- content
- view_count
- published_at
- created_at
- updated_at
It also implements a belongs_to relation 'author' that returns an instance of the User model that has the following fields:
- first_name
- last_name
Bad solution
The usual bad solution is to keep everything either in a controller or in a model, thus making them 'fat'.
Controller solution:
class ArticlesController < ApplicationController
def index
@articles = Article
.joins('LEFT OUTER JOIN users ON users.id = articles.author_id')
.where(published: true)
.where('view_count > ?', params[:min_view_count])
.where('users.first_name LIKE ?', "#{params[:author_name]}%")
end
Model solution:
class Article < ActiveRecord::Base
scope :published, -> { where(published: true) }
def with_view_count_greater_than(min_view_count)
where('view_count > ?', min_view_count)
end
def with_author_with_first_name_like(first_name)
joins('LEFT OUTER JOIN users ON users.id = articles.author_id')
.where('users.first_name LIKE ?', "#{first_name}%")
end
end
It doesn't matter if we put this code in a controller or in a model, it simply doesn't belong there. A model's job isn't to handle querying logic, query logic in a controller isn't reusable and it makes the controller 'fat'.
Good solution
Create a query object in the app/queries
directory.
It's implementation should resemble the following:
class ArticlesQuery
attr_reader :relation
def initialize(relation = Article.all)
@relation = relation
end
def published
relation.where(published: true)
end
def minimal_view_count(view_count)
return relation unless view_count.present?
relation.where('view_count > ?', view_count)
end
def author_first_name_like(first_name)
return relation unless first_name.present?
with_authors
.where('users.first_name LIKE ?', "#{first_name}%")
end
private
def with_authors
relation.joins('LEFT OUTER JOIN users ON users.id = articles.author_id')
end
end
Then you would use it like this
class ArticlesController < ApplicationController
def index
@articles = articles_scope
end
private
def articles_scope
published_articles = ArticlesQuery.new.published
articles_with_view_count = ArticlesQuery.new(published_articles)
.minimal_view_count(params[:view_count])
ArticlesQuery.new(articles_with_view_count)
.author_first_name_like(params[:author_name])
end
end