Under the hood how named_scope works

Neeraj Singh

By Neeraj Singh

on October 17, 2008

Following code was tested with ruby 1.8.7 and Rails 2.x .

Rails recently added named_scope feature and it is a wonderful thing. If you don't know what named_scope is then you can find out more about it here .

This article is not about how to use named_scope. This article is about how named_scope does what it does so well.

Understanding with_scope

ActiveRecord has something called with_scope which is not associated with named_scope. The two are entirely separate thing. However named_scope relies on the workings on with_scope to do its magic. So in order to understand how named_scope works first let's try to understand what with_scope is.

with_scope let's you add scope to a model in a very extensible manner.

1def self.all_male
2  with_scope(:find => {:conditions => "gender = 'm'"}) do
3    all_active
4  end
5end
6
7def self.all_active
8  with_scope(:find => {:conditions => "status = 'active'"}) do
9    find(:first)
10  end
11end
12
13# User.all_active
14# SELECT * FROM "users" WHERE (status = 'active') LIMIT 1
15
16# User.all_male
17# SELECT * FROM "users" WHERE ((gender = 'm') AND (status = 'active')) LIMIT 1

We can see that when User.all_male is called, it internally calls all_active method and the final sql has both the conditions.

with_scope allows nesting and all the conditions nested together are used to form one single query. And named_scope uses this feature of with_scope to form one single query from a lot of named scopes.

Writing our own named_scope called mynamed_scope

The best way to learn named_scope is by implementing the functionality of named_scope ourselves. We will build this functionality incrementally. To avoid any confusion we will call our implementation mynamed_scope.

To keep it simple in the first iteration we will not support any lambda operation. We will support simple conditions feature. Here is a usage of mynamed_scope .

1class User < ActiveRecord::Base
2  mynamed_scope :active, :conditions => {:status =>  'active'}
3  mynamed_scope :male, :conditions => {:gender => 'm'}
4end

We expect following queries to provide right result.

1User.active
2User.male
3User.active.male
4User.male.active

Let's implement mynamed_scope

At the top of user.rb add the following lines of code

1module ActiveRecord
2  module MynamedScope
3    def self.included(base)
4      base.extend ClassMethods
5    end
6
7    module ClassMethods
8      def mynamed_scope(name,options = {})
9        puts "name is #{name}"
10      end
11    end
12
13  end
14end
15ActiveRecord::Base.send(:include, ActiveRecord::MynamedScope)

Now in script/console if we do User then the code will not blow up.

Next we need to implement functionalities so that mynamed_scope creates class methods like active and male.

What we need is a class where each mynamed_scope could be stored. If 7 mynamed_scopes are defined on User then we should have a way to get reference to all those mynamed_scopes. We are going to add class level attribute myscopes which will store all the mynamed_scopes defined for that class.

1def myscopes
2  read_inheritable_attribute(:myscopes) || write_inheritable_attribute(:myscopes, {})
3end

This discussion is going to be tricky.

We are storing all mynamed_scope information in a variable called myscopes. This will contain all the mynamed_scopes defined on User.

However we need one more way to track the scoping. When we are executing User.active then the active mynamed_scope should be invoked on the User. However when we perform User.male.active then the mynamed_scope active should be performed in the scope of User.male and not directly on User.

This is really crucial. Let's try one more time. In the case of User.active the condition that was supplied while defining the mynamed_scope active should be acted on User directly. However in the case of User.male.active the condition that was supplied while defining mynamed_scope active should be applied on the scope that was returned by User.male .

So we need a class which will store proxy_scope and proxy_options.

1class Scope
2  attr_reader :proxy_scope, :proxy_options
3  def initialize(proxy_scope, options)
4    @proxy_scope, @proxy_options = proxy_scope, options
5  end
6end # end of class Scope

Now the question is when do we create an instance of Scope class. The instance must be created at run time. When we execute User.male.active, until the run time we don't know the scope object active has to work upon. It means that User.male should return a scope and on that scope active will work upon.

So for User.male the proxy_scope is the User class. But for User.male.active, mynamed_scope 'active' gets (User.male) as the proxy_scope.

Also notice that proxy_scope happens to be the value of self.

Base on all that information we can now write the implementation of mynamed_scope like this.

1def mynamed_scope(name,options = {})
2  name = name.to_sym
3  myscopes[name] = lambda { |proxy_scope| Scope.new(proxy_scope,options) }
4
5  (class << self; self end).instance_eval do
6    define_method name do
7      myscopes[name].call(self)
8    end
9  end
10end

At this point of time the overall code looks like this.

1module ActiveRecord
2  module MynamedScope
3    def self.included(base)
4      base.extend ClassMethods
5    end
6
7    module ClassMethods
8
9      def myscopes
10        read_inheritable_attribute(:myscopes) || write_inheritable_attribute(:myscopes, {})
11      end
12
13      def mynamed_scope(name,options = {})
14        name = name.to_sym
15        myscopes[name] = lambda { |proxy_scope| Scope.new(proxy_scope,options) }
16
17        (class << self; self end).instance_eval do
18          define_method name do
19            myscopes[name].call(self)
20          end
21        end
22      end
23
24      class Scope
25        attr_reader :proxy_scope, :proxy_options
26        def initialize(proxy_scope, options)
27          @proxy_scope, @proxy_options = proxy_scope, options
28        end
29      end # end of class Scope
30
31    end # end of module ClassMethods
32
33  end # endof module MynamedScope
34end
35ActiveRecord::Base.send(:include, ActiveRecord::MynamedScope)
36
37
38class User < ActiveRecord::Base
39  mynamed_scope :active, :conditions => {:status =>  'active'}
40  mynamed_scope :male, :conditions => {:gender => 'm'}
41end

On script/console

1>> User.active.inspect
2  SQL (0.000549)    SELECT name
3 FROM sqlite_master
4 WHERE type = 'table' AND NOT name = 'sqlite_sequence'
5
6=> "#<ActiveRecord::MynamedScope::ClassMethods::Scope:0x203201c @proxy_scope=User(id: integer, gender: string, status: string, created_at: datetime, updated_at: datetime), @proxy_options={:conditions=>{:status=>"active"}}>"
7>>

What we get is an instance of Scope. What we need is a way to call sql statement at this point of time.

But calling sql can be tricky. Remember each scope has a reference to the proxy_scope before it. This is the way all the scopes are chained together.

What we need to do is to start walking through the scope graph and if the previous proxy_scope is an instance of scope then add the condition from the scope to with_scope and then go to the previous proxy_scope. Keep walking and keep nesting the with_scope condition until we find the end of chain when proxy_scope will NOT be an instance of Scope but it will be a sub class of ActiveRecord::Base.

One way of finding if it is an scope or not is to see if it responds to find(:all). If the proxy_scope does not respond to find(:all) then keep going back because in the end User will be able to respond to find(:all) method.

1# all these two methods to Scope class
2def inspect
3  load_found
4end
5
6def load_found
7  find(:all)
8end

Now in script/console you will get undefined method find. That is because find is not implemented by Scope.

Let's implement method_missing.

1def method_missing(method, *args, &block)
2  if proxy_scope.myscopes.include?(method)
3    proxy_scope.myscopes[method].call(self)
4  else
5    with_scope :find => proxy_options do
6      proxy_scope.send(method,*args)
7    end
8  end
9end

Statement User.active.male invokes method 'male' and since method 'male' is not implemented by Scope, we don't want to call proxy_scope yet since this method 'male' might be a mynamed_scope. Hence in the above code a check is done to see if the method that is missing is a declared mynamed_scope or not. If it is not a mynamed_scope then the call is sent to proxy_scope for execution. Pay attention to with_scope. Because of this with_scope all calls to proxy_scope are nested.

However Scope class doesn't implement with_scope method. However the first proxy_scope ,which will be User in our case, implements with_scope method. So we can delegate with_scope method to proxy_scope like this.

1delegate :with_scope,  :to => :proxy_scope

At this point of time the code looks like this

1module ActiveRecord
2  module MynamedScope
3    def self.included(base)
4      base.extend ClassMethods
5    end
6
7    module ClassMethods
8
9      def myscopes
10        read_inheritable_attribute(:myscopes) || write_inheritable_attribute(:myscopes, {})
11      end
12
13      def mynamed_scope(name,options = {})
14        name = name.to_sym
15        myscopes[name] = lambda { |proxy_scope| Scope.new(proxy_scope,options) }
16
17        (class << self; self end).instance_eval do
18          define_method name do
19            myscopes[name].call(self)
20          end
21        end
22      end
23
24      class Scope
25        attr_reader :proxy_scope, :proxy_options
26        delegate :with_scope,  :to => :proxy_scope
27        def initialize(proxy_scope, options)
28          @proxy_scope, @proxy_options = proxy_scope, options
29        end
30
31        def inspect
32          load_found
33        end
34
35        def load_found
36          find(:all)
37        end
38
39        def method_missing(method, *args, &block)
40          if proxy_scope.myscopes.include?(method)
41            proxy_scope.myscopes[method].call(self)
42          else
43            with_scope :find => proxy_options do
44              proxy_scope.send(method,*args)
45            end
46          end
47        end
48
49      end # end of class Scope
50
51    end # end of module ClassMethods
52
53  end # endof module MynamedScope
54end
55ActiveRecord::Base.send(:include, ActiveRecord::MynamedScope)
56
57
58class User < ActiveRecord::Base
59  mynamed_scope :active, :conditions => {:status =>  'active'}
60  mynamed_scope :male, :conditions => {:gender => 'm'}
61end

Let's checkout the result in script/console

1>> User.active
2SELECT * FROM "users" WHERE ("users"."status" = 'active')
3>> User.male
4SELECT * FROM "users" WHERE ("users"."gender" = 'm')
5>> User.active.male
6SELECT * FROM "users" WHERE (("users"."gender" = 'm') AND ("users"."status" = 'active'))
7>> User.male.active
8SELECT * FROM "users" WHERE (("users"."status" = 'active') AND ("users"."gender" = 'm'))
9
10# you can also see count
11>> User.active.count
12SELECT count(*) AS count_all FROM "users" WHERE ("users"."status" = 'active')
13=> 2

named_scope supports a lot more things than what we have shown. named_scope supports passing lambda instead of conditions and it also supports joins and extensions.

However in the process of building mynamed_scope we got to see the workings of the named_scope implementation.

Stay up to date with our blogs. Sign up for our newsletter.

We write about Ruby on Rails, ReactJS, React Native, remote work,open source, engineering & design.