Posted on

Query objects are a fairly well known concept in Rails these days. They’re useful to extract complex SQL queries into their own classes, which would otherwise clutter ActiveRecord models. What if you turned a complex scope which is already used all over your application into a query object? Do you have to refactor all of these occurences?

The following ActiveRecord model has a scope returning popular featured videos. Okay, it’s not really that complex, but this way we can keep the focus on the important parts.

class Video < ActiveRecord::Base
  scope :featured_and_popular,
        -> { where(featured: true).where('views_count > ?', 100) }
end

The scope can be easily extracted into a query object:

module Videos
  class FeaturedAndPopularQuery
    def initialize(relation = Video.all)
      @relation = relation
    end

    def featured_and_popular
      @relation.where(featured: true).where('views_count > ?', 100)
    end
  end
end

Instead of calling a scope on the model, now the query object can be called:

Videos::FeaturedAndPopularQuery.new.featured_and_popular

While it’s great to have the logic removed from the Video class, there are dozens of occurrences of Video.featured_and_popular calls scattered across the whole application. It would be great if the interface could remain unchanged, while still having the complex logic handled by the query object.

To find out how this can be done, let’s first take a look at how Rails implements the scope method. Note that this is a bit simplified, I’ve left out the irrelevant parts:

def scope(name, body, &block)
  unless body.respond_to?(:call)
    raise ArgumentError, 'The scope body needs to be callable.'
  end

  # ...

  singleton_class.send(:define_method, name) do |*args|
    scope = all.scoping { body.call(*args) }
    # ...

    scope || all
  end
end

A couple of things catch the attention:

  • The body (usually a proc) of the scope can be any object, as long as it responds to call.
  • The scoping method is used to scope the result of the body to the current scope.

So the scoping method makes sure that the relation being returned by the scope body is scoped to the relation in the current scope. Wait, that sounds confusing. Check out this example:

Video.only_cats.scoping do
  Video.featured_and_popular
end

This would return cat videos which are featured and popular. So scoping scopes the relation inside the block to the scope of Video.only_cats.

We can simply call the query object from the scope. Thanks to scoping, we don’t need to pass the current scope into the query object:

class Video < ActiveRecord::Base
  scope :featured_and_popular,
        -> { Videos::FeaturedAndPopularQuery.new.featured_and_popular }
end

But this looks quite ugly and verbose.

As we’ve seen in the implementation of scope, it expects its body to be an object which responds to call. Right now we’re passing a proc, but this can be replaced with the query object, as long as it responds to call. So we can just turn the query object into a callable object by renaming a method:

module Videos
  class FeaturedAndPopularQuery
    def initialize(relation = Video.all)
      @relation = relation
    end

    def call
      @relation.where(featured: true).where('views_count > ?', 100)
    end
  end
end

The proc can be removed from the scope declaration, and the query object itself becomes the scope body:

class Video < ActiveRecord::Base
  scope :featured_and_popular, Videos::FeaturedAndPopularQuery.new
end

We could take this even further. Since classes are also objects in Ruby, it’s possible to have a class method call, which delegates the call to a new instance of the query object:

module Videos
  class FeaturedAndPopularQuery
    class << self
      delegate :call, to: :new
    end

    def initialize(relation = Video.all)
      @relation = relation
    end

    def call
      @relation.where(featured: true).where('views_count > ?', 100)
    end
  end
end

Now only the class has to be passed in as the body of the scope. Judging by the camelcased name, it’s very obvious we’re dealing with a class here.

class Video < ActiveRecord::Base
  scope :featured_and_popular, Videos::FeaturedAndPopularQuery
end  

The logic of the query has been extracted to a query object, but the scope has been left in place so other parts of the code don’t need to be changed. The ActiveRecord model looks nice and clean, while the scope is essentially a delegation to the query object.


If you like this post, follow me:


Or share this post: