An Interest In:
Web News this Week
- April 19, 2024
- April 18, 2024
- April 17, 2024
- April 16, 2024
- April 15, 2024
- April 14, 2024
- April 13, 2024
Design Patterns with Ruby on Rails part 2: Query Object
This post is the second part of a series of posts about design patterns with Ruby on Rails.
See other parts here:
Part 1
Query Object
The query object is a pattern that helps to make Rails models slim by extracting
complex SQL queries or scopes into the separated classes that are easy to reuse and test.
Naming
Usually located in app/queries
directory, and of course, can be organized into multiple namespaces if needed.
Typical file name has _query
suffix, class name has Policy
suffix and meaningful name that tells us what is the purpose of the class.
e.g.:Users::ListActiveUsersQuery
module Users class ListActiveUsersQuery def self.call User.where(status: 'active', deleted_at: nil).where.not(email: nil) end endend
Why use a class method instead of an instance? because it is more practical and easier to stub.
There is a way to divide this query into pieces and become even more reusable? Yes, chaining methods.
class UsersQuery def initialize(users = User.all) @users = users end def active @users.where(active: true, pending: false) end def pending @users.where(pending: true, active: false) end def deleted @users.with_deleted endend
This way:
query = UsersQuery.newquery.deleted.pending
Query objects and model scopes
If you have the following scope defined:
class User < ActiveRecord::Base scope :active, -> { where(role: ['moderator', 'guest'], status: 'active', deleted_at: nil) }end
and you want to extract this query and keep this behavior with User.active
, how can you do this?
The solution may seen obvious:
class User < ActiveRecord::Base def self.active ListActiveUsersQuery.call endend
but it adds more code and it is no longer visible in the scope definition.
Using a scope with query objects
Don't forget that your query object has to return relation.
class User < ActiveRecord::Base scope :active, ListActiveUsersQueryend
Let's design our query object class:
class ListActiveUsersQuery class << self delegate :call, to: :new end def initialize(relation = User) @relation = relation end def call @relation.where.not(email: nil).where(status: 'active', deleted_at: nil) endend
Now the query still available via User.active
scope.
Refactoring
Let's take a look at how to use query objects in practice when refactoring larger queries.
class UsersController < ApplicationController def index @users = User.where(active: true, deleted_at: nil) .joins(:emails).where(emails: { active: true }) endend
if you want to write tests for this action it won't be possible without hitting the database.
It gets easier after extracting this query to a separated class:
class ListActiveUsersWithEmailQuery attr_reader :relation def self.call(relation = User) new(relation).call end def new(relation = User) @relation = relation end def call active.with_email end def active relation.where(active: true, deleted_at: nil) end def with_email relation.joins(:emails).where(emails: { active: true }) endend
and the controller can be now updated:
class UsersController < ApplicationController def index @users = ListActiveUsersWithEmailQuery.call endend
Testing
The query's not a controller concern anymore, it simplifies your test, now you can simply call allow(ListActiveUsersWithEmailQuery).to receive(:call).and_return(...)
in your spec.
Conclusion
At this point, you are familiarized with query object pattern and you should be able to start using in your Rails
application.
It's important to remember that moving to a separated class is not always a good idea, it adds a complexity layer
to your code and has a maintainability cost, use this pattern with caution, and enjoy more readable and extendable code.
References
[book] Rails Patterns by Pawel Dabrowski
Original Link: https://dev.to/renatamarques97/design-patterns-with-ruby-on-rails-part-2-query-object-1h65
Dev To
An online community for sharing and discovering great ideas, having debates, and making friendsMore About this Source Visit Dev To