This pattern is used to encapsulate complex database queries in an object.
It helps on maintaining Rails code simple by separating algorithms from how the data needs to be gathered.
All query objects are POROs placed in the /app/queries/
folder.
The actual object that encapsulates all the logic.
# frozen_string_literal: true
class SearchUsers # (1)
def initialize(scope = User.all) # (2)
@scope = scope
end
def call(filters: {}) # (3)
filter_by_email(filters[:email]) if filters[:email]
filter_by_language(filters[:language]) if filters[:language]
@scope.order(created_at: :desc)
end
private
def filter_by_email(email)
@scope = @scope.where('email ILIKE %?%', email)
end
def filter_by_language(language)
@scope = @scope.joins(:profile).where(profile: { language: language })
end
end
- (1) The name for that object explains its concrete use case. There's no need to add
Query
as prefix. - (2) The initializer takes an scope to start from. This allows to compose it with other query objects and/or regular ActiveRecord methods.
- (3) The class has a single call method which takes the additional parameters needed to perform the query, if any. The method calls a number of private methods to incrementally build the query in small steps. In the end it adds an order clause to make sure the query always returns the same results having the same data in the DB.
Can be used from all parts of the code that usually need to interact with the DB, like controllers, services or the model itself.
users = SearchUsers.new.call(language: :en, email: "baker221b@example.com")
# frozen_string_literal: true
class UsersController < ApplicationController
def index
@users = SearchUsers.new(User.active).call(search_params) # (1)
end
private
def search_params
params.require(:search).permit(:email, :language)
end
end
- (1)
call
allways receives a previously filtered list of params
No. Scopes usually contain easy queries, semantically useful across all the project. Query objects encapsulate complex queries that may combine multiple tables, specific performance tweaks; or their behavior is very specific to concrete use cases and are complex enough to need to be encapsulated individually.