Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Hacking ActiveRecord: add global named scope

I am trying to have a pack of very generic named scopes for ActiveRecord models like this one:

module Scopes
  def self.included(base)
    base.class_eval do
      named_scope :not_older_than, lambda {|interval|
        {:conditions => ["#{table_name}.created_at >= ?", interval.ago]
      }
    end
  end
end
ActiveRecord::Base.send(:include, Scopes)

class User < ActiveRecord::Base
end

If the named scope should be general, we need to specify *table_name* to prevent naming problems if their is joins that came from other chained named scope.

The problem is that we can't get table_name because it is called on ActiveRecord::Base rather then on User.

User.not_older_than(1.week)

NoMethodError: undefined method `abstract_class?' for Object:Class
from /var/lib/gems/1.8/gems/activerecord-2.3.5/lib/active_record/base.rb:2207:in `class_of_active_record_descendant'
from /var/lib/gems/1.8/gems/activerecord-2.3.5/lib/active_record/base.rb:1462:in `base_class'
from /var/lib/gems/1.8/gems/activerecord-2.3.5/lib/active_record/base.rb:1138:in `reset_table_name'
from /var/lib/gems/1.8/gems/activerecord-2.3.5/lib/active_record/base.rb:1134:in `table_name'
from /home/bogdan/makabu/railsware/startwire/repository/lib/core_ext/active_record/base.rb:15:in `included'
from /var/lib/gems/1.8/gems/activerecord-2.3.5/lib/active_record/named_scope.rb:92:in `call'
from /var/lib/gems/1.8/gems/activerecord-2.3.5/lib/active_record/named_scope.rb:92:in `named_scope'
from /var/lib/gems/1.8/gems/activerecord-2.3.5/lib/active_record/named_scope.rb:97:in `call'
from /var/lib/gems/1.8/gems/activerecord-2.3.5/lib/active_record/named_scope.rb:97:in `not_older_than'

How can I get actual table_name at Scopes module?

like image 275
Bogdan Gusiev Avatar asked Sep 30 '10 09:09

Bogdan Gusiev


People also ask

What is an ActiveRecord scope?

Scopes are used to assign complex ActiveRecord queries into customized methods using Ruby on Rails. Inside your models, you can define a scope as a new method that returns a lambda function for calling queries you're probably used to using inside your controllers.

What are ActiveRecord methods?

The active record pattern is an approach to accessing data in a database. A database table or view is wrapped into a class. Thus, an object instance is tied to a single row in the table. After creation of an object, a new row is added to the table upon save.

Can you use ActiveRecord without rails?

ActiveRecord is commonly used with the Ruby-on-Rails framework but you can use it with Sinatra or without any web framework if desired.

What is ActiveRecord in Ruby on Rails?

What is ActiveRecord? ActiveRecord is an ORM. It's a layer of Ruby code that runs between your database and your logic code. When you need to make changes to the database, you'll write Ruby code, and then run "migrations" which makes the actual changes to the database.


4 Answers

Try using the #scoped method inside a class method of ActiveRecord::Base. This should work:

module Scopes
  def self.included(base)
    base.class_eval do
      def self.not_older_than(interval)
        scoped(:conditions => ["#{table_name}.created_at > ?", interval.ago])
      end
    end
  end
end

ActiveRecord::Base.send(:include, Scopes)
like image 193
Sidane Avatar answered Oct 23 '22 03:10

Sidane


Rails 5, ApplicationRecord (Hope it helps others)

# app/models/concerns/not_older_than.rb

module NotOlderThan
  extend ActiveSupport::Concern

  included do
    scope :not_older_than, -> (time, table = self.table_name){ 
      where("#{table}.created_at >= ?", time.ago)
    }
  end
end

# app/models/application_record.rb

class ApplicationRecord < ActiveRecord::Base
  self.abstract_class = true
  include NotOlderThan
end

# app/models/user.rb
class User < ApplicationRecord
  # Code
end

# Usage
User.not_older_than(1.week)

In Rails 5, all models are inherited from ApplicationRecord by default. If you wan to apply this scope for only particular set of models, add include statements only to those model classes. This works for join queries and chained scopes as well.

like image 34
Ashik Salman Avatar answered Oct 23 '22 01:10

Ashik Salman


Additional useful scopes below :

module Scopes
  def self.included(base)
    base.class_eval do
      def self.created(date_start, date_end = nil)
          if date_start && date_end
            scoped(:conditions => ["#{table_name}.created_at >= ? AND #{table_name}.created_at <= ?", date_start, date_end])
          elsif date_start
            scoped(:conditions => ["#{table_name}.created_at >= ?", date_start])
          end
      end
      def self.updated(date_start, date_end = nil)
          if date_start && date_end
            scoped(:conditions => ["#{table_name}.updated_at >= ? AND #{table_name}.updated_at <= ?", date_start, date_end])
          elsif date_start
            scoped(:conditions => ["#{table_name}.updated_at >= ?", date_start])
          end
      end
    end
  end
end

ActiveRecord::Base.send(:include, Scopes)
like image 1
Quentin Rousseau Avatar answered Oct 23 '22 03:10

Quentin Rousseau


Here is an updated, Rails4 compatible solution.
I am told defining global scopes like this can lead to conflicts, caveat emptor and all that, but sometimes you just need a simple scope on all your models, right?

Define a module.

# in /app/models/concerns/global_scopes.rb
module GlobalScopes
  def self.included(base)
    base.class_eval do
      def self.in_daterange(start_date, end_date)
        all.where(created_at: start_date.to_date.beginning_of_day..end_date.to_date.end_of_day)
      end
    end
  end
end

Have the module included in ActiveRecord::Base.

# in /config/initializers/activerecord.rb
ActiveRecord::Base.send(:include, GlobalScopes)

That's it! Notice that in Rails4 you do not have to mess with :scoped, but instead you use :all and chain your query to it.

like image 1
Epigene Avatar answered Oct 23 '22 02:10

Epigene