Ram Patidar
Ram Patidar

Reputation: 666

How can an ActiveRecord::Relation object call class methods

How can an ActiveRecord::Relation object call class methods?

class Project < ActiveRecord::Base
  has_many :tasks
end

class Task < ActiveRecord::Base
  belongs_to :project

  def self.initial_tasks # class methods
   # here return initial tasks
  end
end

Now we can call :

Project.first.tasks.initial_tasks # how it works

initial_tasks is a class method and we can't call class methods on an object.

Project.first.tasks returns an ActiveRecord::Relation object, so how could it be able to call initial_tasks?

Please explain.

Upvotes: 32

Views: 17568

Answers (4)

L.Youl
L.Youl

Reputation: 382

For anyone struggling to actually access a filtered list of records inside a class method you can simply call all which will return you the list you are expecting rather than all of the records in that table which is accessed with relation.

Upvotes: 2

Nikita Shilnikov
Nikita Shilnikov

Reputation: 1204

It's extremely easy to explore. You just do so:

class Project < ActiveRecord::Base
  has_many :tasks
end

class Task < ActiveRecord::Base
  belongs_to :project

  def self.initial_tasks  #class methods
    1 / 0
  end
end

Then call Project.first.tasks.initial_tasks and you get:

  Division by zero
  ...
  .../gems/activerecord-4.1.0/lib/active_record/relation/delegation.rb:70:in `block in re
  .../gems/activerecord-4.1.0/lib/active_record/associations/collection_proxy.rb:872:in `
  .../gems/activerecord-4.1.0/lib/active_record/relation.rb:286:in `scoping'",
  .../gems/activerecord-4.1.0/lib/active_record/associations/collection_proxy.rb:872:in `
  .../gems/activerecord-4.1.0/lib/active_record/relation/delegation.rb:70:in `initial_tasks'",

And that's all you need basically. Easy to explore but not so easy to understand.

Now I'll explain what that means. When you call Project#tasks method it does not return you ActiveRecord::Relation object. Actually it returns you an instance of runtime-created class named Task::ActiveRecord_Associations_CollectionProxy inherited from ActiveRecord::Associations::CollextionProxy that in turn inherited from ActiveRecord::Relation. This runtime-created class is linked with Task class and contains dynamically-defined (via method_missing) proxy-methods that delegate calls to Task class methods and merge association scope with class-defined scope returned by class-level methods.

How it works (really non-trivial):

  1. There is ActiveRecord::Base class. Class object extended by ActiveRecord::Delegation::DelegateCache module here.
  2. DelegateCache has DelegateCache.inherited callback that defines @relation_delegate_cache attribute every time you inherit ActiveRecord::Base. It means all AR::Base descendant classes will have such attribute. The callback calls DelegateCache#initialize_relation_delegate_cache method which in order fills cache attribute with runtime-created classes:

    [
      ActiveRecord::Relation,
      ActiveRecord::Associations::CollectionProxy,
      ActiveRecord::AssociationRelation
    ].each do |klass|
      delegate = Class.new(klass) {
        include ClassSpecificRelation
      }
      const_set klass.name.gsub('::', '_'), delegate
      cache[klass] = delegate
    end
    

    Here these classes get unusual names a-la Task::ActiveRecord_Associations_CollectionProxy mentioned earlier.

  3. Ok, now our Task model has links to runtime-defined relation classes. These classes have some concern called ClassSpecificRelation (included here). The concern adds method_missing method that detects your class-method calls on a relation object (for instance calling #initial_tasks on Project.tasks). On such call it dynamically defines new runtime-class instance methods that delegate to class-level methods. Now you have Task class linked to Task::ActiveRecord_Associations_CollectionProxy class containing all instance-level methods that proxy calls to Task class-level methods get scope result and merge it with current association scope (here).

That's how AR prefers dynamically-defined methods on runtime-created classes over using inefficient method_missing calls on ActiveRecord::Relation.

I think it's OK if you do not understand all this stuff. Just call class-level methods on associations :)

Upvotes: 31

Kanti
Kanti

Reputation: 1082

Here in ActiveRecord::Relation, Relation is representing whole table and your class Post is map with table,

So ActiveRecord::Relation is array or single record it can access class method.

Upvotes: 0

zeantsoi
zeantsoi

Reputation: 26203

There's not much documentation on the application on class methods to ActiveRecord::Relation objects, but we can understand this behavior by taking a look at how ActiveRecord scopes work.

First, a Rails model scope will return an ActiveRecord::Relation object. From the docs:

Class methods on your model are automatically available on scopes. Assuming the following setup:

class Article < ActiveRecord::Base
  scope :published, -> { where(published: true) }
  scope :featured, -> { where(featured: true) }

  def self.latest_article
    order('published_at desc').first
  end

  def self.titles
    pluck(:title)
  end
end

First, invoking a scope returns an ActiveRecord::Relation object:

Article.published.class
#=> ActiveRecord::Relation

Article.featured.class
#=> ActiveRecord::Relation

Then, you can operate on the ActiveRecord::Relation object using the respective model's class methods:

Article.published.featured.latest_article
Article.featured.titles

It's a bit of a roundabout way of understanding the relationship between class methods and ActiveRecord::Relation, but the gist is this:

  1. By definition, model scopes return ActiveRecord::Relation objects
  2. By definition, scopes have access to class methods
  3. Therefore, ActiveRecord::Relation objects have access to class methods

Upvotes: 41

Related Questions