Design patterns in large Ruby on Rails web applications: constructing a Query Object class that is responsible for elegantly querying a database. Read our blog post to find out how to make a simple and easy to test Query Object implementation within a Rails application.

Query Objects are classes specifically responsible for handling complex SQL queries, usually with data aggregation and filtering methods, that can be applied to your database. We use this design pattern in large Ruby on Rails web applications, to improve an app’s maintainability and scalability - which makes programmers’ lives a bit easier along the way and allows for faster development times.

Oftentimes, the code responsible for querying the database is placed within scopes in models, or gets mixed in with other logic. It is usually the same code in many places throughout the app, which goes against the principles of DRY programming (Don’t Repeat Yourself!). This leads to a lengthy development process and makes the code more prone to errors - causing a lot of irritation, especially when there is a need to refactor code or make changes to the database itself during any stage of the query.

Example of a poorly designed query function

So what does a poorly designed query function look like and what troubles can it cause? Have a look at the example below - which is a combination of a few actions, including queries to the database - and check for yourself how hard it is to test it without hitting the database with the real records:

def popular_sport_posts
  popular_posts = []
  if user.posts.where(category: 'sport').any?
    sport_posts = user.posts.where(category: 'sport')
    return sport_posts if sport_posts.size == 1
    if sport_posts.size > 1 && sport_posts.size < 11
      sport_posts.each do |post|
        popular_posts << post if post.comments.where(published: true).present?
      end
    end
  end
  if sport_posts.size > 10
    sport_posts.each do |post|
      if post.comments.where(published: true).present?
        post.comments.each do |comment|
          if comment.created_at < 30.days.ago
            popular_posts << post
            break
          end
        end
      end
    end
  end

  popular_posts
end

A best programming practices solution for the above? We can simply extract our database queries in Rails in the above code to create Policy Objects and Query Objects. This will make each section of code more isolated - and thus far easier to test.

Construction of a Query Object class

There is no significant difference between a standard class and a Query Object class. Still, like any other design pattern, this one also has its own specific set of rules.

In terms of initializing the class, it is always clever to pass the scope. If the scope is not given, then we use the default one. Thanks to this approach, we can always pass a scope and use pre-filtered results, or create more complex queries by composing multiple query objects. Here is a simple example:

module Products
  class SportsQuery
    def initialize(scope = Post.all)
      @scope = scope
    end

    def products
      scope.where(category: 'sport')
    end
  end
end

Other benefits of this design pattern

When creating a Query Object class, we make our models slimmer and our logic more decoupled. This means we can create more meaningful and faster tests that focus only on that specific part of the code.

Let’s get back again to the popular_sport_posts method presented above and refactor it using a Query Object.

class SportsQuery
  def initialize(scope = Post.all)
    @scope = scope
  end

  def sport_posts
    scope.where(category: 'sport')
  end

  def sport_posts_count
    sport_posts.count
  end

  def sport_posts_with_published_comments
    sport_posts.joins(:comments).where(comments: { published: true })
  end

  def sport_posts_with_recent_published_comments
    sport_posts_with_published_comments.where('DATE(comments.created_at) > ?', 30.days.ago)
  end
end


def popular_sport_posts(user, sports_query = SportsQuery.new(user.posts))
  sport_posts_count = sports_query.sport_posts_count

  if sport_posts_count < 2
    sports_query.sport_posts
  elsif sport_posts_count < 11
    sports_query.sport_posts_with_published_comments
  else
    sports_query.sport_posts_with_recent_published_comments
  end
end

To test this class we are not forced to create real records in the database in order to check behavior. Tests are much faster and the code itself is far more readable. Thanks to the implemented design pattern, we can effortlessly separate database query logic and then just stub the Query Object, testing database communication in an abstracted test class.

See below for yourself:

describe '#popular_sport_posts' do
  context 'when sports posts count is less than 2' do
    it 'returns sport posts' do
      posts = [instance_double(Post)]
      sports_query = instance_double(SportsQuery,
                                     sport_posts_count: 1,
                                     sport_posts: posts)
      result = described_object.popular_sport_posts(sports_query)
      expect(result).to eq(sport_posts)
    end
  end

  context 'when sports posts count is less than 11' do
    it 'returns sport posts with published comments' do
      posts = [instance_double(Post)]
      sports_query = instance_double(SportsQuery,
                                     sport_posts_count: 9,
                                     sport_posts_with_published_comments: posts)
      result = described_object.popular_sport_posts(sports_query)
      expect(result).to eq(posts)
    end
  end

  context 'when sports posts count is more than 10' do
    it 'returns sports posts with recent published comments' do
      posts = [instance_double(Post)]
      sports_query = instance_double(SportsQuery,
                                     sport_posts_count: 12,
                                     sport_posts_with_recent_published_comments: posts)
      result = described_object.popular_sport_posts(sports_query)
      expect(subject).to eq(posts)
    end
  end
end

Query Object and Builder pattern - the perfect pairing

It is worth mentioning that the Query Object pattern works fantastically with the Builder pattern, especially in cases where a scope object is too complicated to just pass it to the initializer. In such an instance it is better to create a base class, which will expose our code for queries.

Usually we keep our Query Objects under /lib/query_objects/products/sport_query.rb, where products is the name of the database table and the file name, sport_query.rb, is a combination of the query suffix and the type of data we are fetching when using this class.

Intrigued by our solution to construct easy-to-test Query Object classes responsible for elegant communication with your database? Want to check out our IT skills and knowledge in practice? Make sure to get in contact with us at iRonin - we can help you with your Ruby on Rails web applications.