I’m trying to migrate my models to be dumb data bags that have some useful state altering methods. I’ve found that using query objects, I was able to reduce the complexity of some of my models and controllers.
The Problem
My User
model is gigantic. It’s well over 600 LOC, and it’s growing at a scary
rate. Testing is becoming a nightmare because of all the methods. A few methods
I have realized, I could remove completely and extract them into query objects.
# app/models/user.rb
class User < ActiveRecord::Base
# ...
def self.search text
where {
(users.email.like "%#{text}%") |
(users.username.like "%#{text}%") |
(users.first_name.like "%#{text}%") |
(users.last_name.like "%#{text}%")
}
end
# ...
end
Now this method isn’t exactly all that complicated but my User
model is
rittled with these methods. They are just there to be used by the controller
like so:
# app/controllers/users_controller.rb
class UsersController < AuthorizedController
def index
@users = User.search(params[:q]).page(params[:page])
end
end
All that method does, is execute a query. It doesn’t alter the state of any model. This could be extracted out into its own class and be isolated enough that testing becomes very simple.
The Solution
The solution is simple, kill The Batman. First, I need a query class. When
creating query classes, keep in mind that you need to interact with
ActiveRecord::Relation
objects. This will allow for the query objects to be
chained and used again later.
# app/queries/user_search_query.rb
class UserSearchQuery
# @param [ActiveRecord::Relation] relation
def initialize relation=User.scoped
@relation = relation
end
# @param [String] text
def search text
@relation.where {
(users.email.like "%#{text}%") |
(users.username.like "%#{text}%") |
(users.first_name.like "%#{text}%") |
(users.last_name.like "%#{text}%")
}
end
# @param [String] text
def self.search text
new.search(text)
end
end
Second I need to replace the call to User.search
from the UsersController
# app/controllers/users_controller.rb
class UsersController < AuthorizedController
def index
@users = UserSearchQuery.search(params[:q]).page(params[:page])
end
end
Now looking at this, it is easy to say, “Well that didn’t change much”, but it
did. I can remove the old method from the User
model, and reduce the
complexity a little bit by doing this.
Chaining
Here is an example of chaining some queries together
# app/queries/new_users_query.rb
class NewUsersQuery
# @param [ActiveRecord::Relation] relation
def initialize relation=User.scoped
@relation = relation
end
def today date=DateTime.now
between(date.beginning_of_day, date.end_of_day)
end
def last_seven_days
date = DateTime.now.begninning_of_day
between(date - 7.days, date.end_of_day)
end
def between starting, ending
@relation.where {
(users.created_at >= starting) &
(users.created_at < ending)
}
end
end
Now this may sound a little dumb, but this is merely just an example.
query = UserSearchQuery.new(NewUsersQuery.today)
query.search('someone').each do |user|
NotificationMailer.welcome(user).deliver
end