Lambda, procs and ActiveRecord scopes - Part 2

Tags: ActiveRecord, ruby, rails, lambda
Publish Date: 2016-06-19

As mentioned in my previous post, there are many "Rails Programmers" who simply follow the examples without truly understanding the Rails API. So as a folllow up to my previous post, here is a look at what the scope method does within ActiveRecord.

 

scope method within ActiveRecord(::Scoping::Named::ClassMethods)

Very often, when we use the scope method within ActiveRecord, we only provide 2 arguments: The name of the scope and a lambda.

Let's dig into the Rails API and see what the scope method does with the arguments you pass into it.

# File activerecord/lib/active_record/scoping/named.rb, line 141
def scope(name, body, &block)
  unless body.respond_to?(:call)
    raise ArgumentError, 'The scope body needs to be callable.'
  end

  if dangerous_class_method?(name)
    raise ArgumentError, "You tried to define a scope named \"#{name}\" "                "on the model \"#{self.name}\", but Active Record already defined "                "a class method with the same name."
  end

  extension = Module.new(&block) if block

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

    scope || all
  end
end

First, it performs some checks: in line 3-5 above, it checks whether the second argument responds to the method call. So technically, we could also use a Proc.new for our second argument... but it's better to keep it with lambda (explained in the next post). Then In line 7-9, it checks whether the name of your scope clashes with class methods that are already defined by ActiveRecord.

Line 13-15 is where the scope definition happens: it defines a class method named after the first argument, its method parameters are the block parameters from the lambda and the lambda's body becomes the method body. The returned value (captured by local variable scope) is an instance of ActiveRecord::Relation. 

If you provide a block to the scope method, the block's body will be used to define a anonymous module (line 11), which would be extended by the ActiveRecord::Relation object in line 15.

In case the above was confusing, let's take the classical BlogPost class as example

 

BlogPost <ActiveRecord::Base example

We could define some scopes to filter out posts, so we might have the following:

class BlogPost < ActiveRecord::Base
  scope :title_matches, lambda{ |str| where("title LIKE ?", "%#{str}%") }
  scope :published, lambda{ where(published: true) }
end

Behind the scenes, what the scope invocation above do, is the following:

class BlogPost < ActiveRecord::Base
  def self.title_matches(str)
    where("title LIKE ?", "%#{str}%")
  end
  
  def self.published
    where(published: true)
  end
end
 
Extending scope by adding a block

For the BlogPosts returned from :published, we might want to apply further scoping, which wouldn't make sense to the BlogPosts returned from other scopes (e.g. :title_matches). In that case, we could provide a block to the scope method as a third argument.

Here is a scenario where an additional block to the scope method would come in handy: Let's say we've applied tagging to our BlogPost class and for setting/getting its tags, we use the ActAsTaggableOn gem. By doing so, each instance of BlogPost responds to the :tag_list method, which would return an array of strings, which are the tags of a BlogPost.

Now, among the published post, we would like to count how many times each tag has been used. For that, we define a tags_count method within the scope of :published. See below:

class Post < ActiveRecord::Base
  acts_as_taggable # Specific for the ActAsTaggableOn gem

  scope :published, lambda{ where(published: true) } do
    def tags_count
      map(&:tag_list).inject({}) do |hash, arr|
        arr.each do |elem|  
          hash[elem] ? hash[elem] += 1 : hash[elem] = 1
        end
        hash
      end
    end
  end
end

BlogPost.published.tags_count
# => {"ruby"=>1, "javascript"=>2}

The code above actually produces the following:

class BlogPost < ActiveRecord::Base
  # Define an anonymous Module:
  extensions = Module.new do
    def tags_count
      map(&:tag_list).inject({}) do |hash, arr|
        arr.each do |elem|  
          hash[elem] ? hash[elem] += 1 : hash[elem] = 1
        end
        hash
      end      
    end
  end

  # Then define a class method and extend the ActiveRecord::Relation object with the anonymous Module from above
  def self.published
    scope = where(published: true)
    scope.extend extensions
    scope
  end
end

So in a nutshell, by extending the ActiveRecord::Relation object with the newly created anonymous module, all methods defined within this module now becomes available to the ActiveRecord::Relation object which was returned by the original scope.

In case the example with inject({}) was confusing, I would recommend you to check my earlier post for an example. 

As a continuation of this topic, in the next post, I will explain what the differences are between a Proc and a Lambda and why we should invoke the scope method with a lambda rather than a Proc.