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.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.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|