How can an ActiveRecord::Relation object call class methods

前端 未结 4 1372
礼貌的吻别
礼貌的吻别 2020-12-04 22:15

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

class Project < ActiveRecord::Base
  has_many :tasks
end

class Task < ActiveRecord::Base
         


        
相关标签:
4条回答
  • 2020-12-04 22:45

    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.

    0 讨论(0)
  • 2020-12-04 22:58

    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.

    0 讨论(0)
  • 2020-12-04 23:05

    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
    0 讨论(0)
  • 2020-12-04 23:06

    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 :)

    0 讨论(0)
提交回复
热议问题