How do rails association methods work?

后端 未结 4 619
自闭症患者
自闭症患者 2020-11-29 04:29

How do rails association methods work? Lets consider this example

class User < ActiveRecord::Base
   has_many :articles
end

class Article < ActiveReco         


        
相关标签:
4条回答
  • 2020-11-29 04:34

    As metioned previously, when doing

    @user.articles.class
    => Array
    

    what you actually get is Array. That is because #class method was undefined, as also mentioned before.

    But how do you get the actual class of @user.articles (which should be proxy)?

    Object.instance_method(:class).bind(@user.articles).call
    => ActiveRecord::Associations::CollectionProxy
    

    And why did you get Array in the first place? Because #class method was delegated to CollectionProxy @target instance through method missin, which is actually an array. You could peek behind the scene by doing something like this:

    @user.articles.proxy_association
    
    0 讨论(0)
  • 2020-11-29 04:43

    When you do an association (has_one, has_many, etc.), it tells the model to include some methods automatically by ActiveRecord. However, when you decided to create an instance method returning the association yourself, you won't be able to make use of those methods.

    The sequence is something like this

    1. setup articles in User model, i.e. do a has_many :articles
    2. ActiveRecord automatically includes convenient methods into model (e.g. size, empty?, find, all, first, etc)
    3. setup user in Article, i.e. do a belongs_to :user
    4. ActiveRecord automatically includes convenient methods into model (e.g. user=, etc)

    Thus, it's clear that when you declare an association, the methods are added automatically by ActiveRecord, which is the beauty as it handled a tremendous amount of work, which will need to be done manually otherwise =)

    you can read more about it here: http://guides.rubyonrails.org/association_basics.html#detailed-association-reference

    hope this helps =)

    0 讨论(0)
  • 2020-11-29 04:47

    How it actually works is that the association object is a "proxy object". The specific class is AssociationProxy. If you look at line 52 of that file, you'll see:

    instance_methods.each { |m| undef_method m unless m =~ /(^__|^nil\?$|^send$|proxy_|^object_id$)/ }
    

    By doing this, methods like class no longer exist on this object. So if you call class on this object, you'll get method missing. So, there a method_missing implemented for the proxy object that forwards the method call to the "target":

    def method_missing(method, *args)
      if load_target
        unless @target.respond_to?(method)
          message = "undefined method `#{method.to_s}' for \"#{@target}\":#{@target.class.to_s}"
          raise NoMethodError, message
        end
    
        if block_given?
          @target.send(method, *args)  { |*block_args| yield(*block_args) }
        else
          @target.send(method, *args)
        end
      end
    end
    

    The target is an Array, so when you call class on this object, it says it's an Array, but that's just because the target is an Array, the actual class is an AssociationProxy, but you can't see that anymore.

    So all the methods that you add, such as of_sector, get added to the association proxy, so they get called directly. Methods like [] and class aren't defined on the association proxy, so they get sent to the target, which is an array.

    To help you see how this is happening, add this to line 217 of that file in your local copy of association_proxy.rb:

    Rails.logger.info "AssociationProxy forwarding call to `#{method.to_s}' method to \"#{@target}\":#{@target.class.to_s}" 
    

    If you don't know where that file is, the command gem which 'active_record/associations/association_proxy' will tell you. Now when you call class on a AssociationProxy, you will see a log message telling you it is sending that to the target, which should make it clearer what is happening. This is all for Rails 2.3.2 and could change in other versions.

    0 讨论(0)
  • 2020-11-29 04:51

    As already mentioned, the active record associations create a metric buttload of convenience methods. Sure, you could write your own methods to fetch everything. But that is not the Rails Way.

    The Rails Way is the culmination of two mottos. DRY (Don't Repeat Yourself) and "Convention over Configuration". Essentially by naming things in a way that makes sense, some robust methods provided by the framework can abstract out all the common code. The code you place in your question is the perfect example of something that can be replaced by a single method call.

    Where these convenience methods really shine are the more complex situations. The kind of thing involving join models, conditions, validations, etc.

    To answer your question when you do something like @user.articles.find(:all, :conditions => ["created_at > ? ", tuesday]), Rails prepares two SQL queries and then merges them into one. where as your version just returns the list of objects. Named scopes do the same thing, but usually don't cross model boundaries.

    You can validate it by checking the SQL queries in the development.log as you call these things in the console.

    So lets talk about Named Scopes for a moment because they give a great example of how rails handles the SQL, and I think they're a simpler way to demonstrate what's going on behind the scenes, as they don't need any model associations to show off.

    Named Scopes can be used to perform custom searches of a model. They can be chained together or even called through associations. You could easily create custom finders that return identical lists, but then you run into the same problems mentioned in the Question.

    class Article < ActiveRecord::Base
      belongs_to :user
      has_many :comments
      has_many :commentators, :through :comments, :class_name => "user"
      named_scope :edited_scope, :conditions => {:edited => true}
      named_scope :recent_scope, lambda do
        { :conditions => ["updated_at > ? ", DateTime.now - 7.days]}
    
      def self.edited_method
        self.find(:all, :conditions => {:edited => true})
      end
    
      def self.recent_method
        self.find(:all, :conditions => ["updated_at > ?", DateTime.now - 7 days])
      end
    end
    
    Article.edited_scope
    =>     # Array of articles that have been flagged as edited. 1 SQL query.
    Article.edited_method
    =>     # Array of Articles that have been flagged as edited. 1 SQL query.
    Array.edited_scope == Array.edited_method
    => true     # return identical lists.
    
    Article.recent_scope
    =>     # Array of articles that have been updated in the past 7 days.
       1 SQL query.
    Article.recent_method
    =>     # Array of Articles that have been updated in the past 7 days.
       1 SQL query.
    Array.recent_scope == Array.recent_method
    => true     # return identical lists.
    

    Here's where things change:

    Article.edited_scope.recent_scope
    =>     # Array of articles that have both been edited and updated 
        in the past 7 days. 1 SQL query.
    Article.edited_method.recent_method 
    => # no method error recent_scope on Array
    
    # Can't even mix and match.
    Article.edited_scope.recent_method
    =>     # no method error
    Article.recent_method.edited_scope
    =>     # no method error
    
    # works even across associations.
    @user.articles.edited.comments
    =>     # Array of comments belonging to Articles that are flagged as 
      edited and belong to @user. 1 SQL query. 
    

    Essentially each named scope creates an SQL fragment. Rails will skillfully merge with every other SQL fragment in the chain to produce a single query returing exactly what you want. The methods added by the association methods work the same way. Which is why they seamlessly integrate with named_scopes.

    The reason for the mix & match didn't work is the same that the of_sector method defined in the question doeso't work. edited_methods returns an Array, where as edited_scope (as well as find and all other AR convenience methods called as part of a chain) pass their SQL fragment onward to the next thing in the chain. If it's the last in the chain it executes the query. Similarly, this won't work either.

    @edited = Article.edited_scope
    @edited.recent_scope
    

    You tried to use this code. Here's the proper way to do it:

    class User < ActiveRecord::Base
       has_many :articles do
         def of_sector(sector_id)
           find(:all, :conditions => {:sector_id => sector_id})
         end
       end
    end
    

    To achieve this functionality you want to do this:

    class Articles < ActiveRecord::Base
      belongs_to :user
      named_scope :of_sector, lambda do |*sectors|
        { :conditions => {:sector_id => sectors} }
      end
    end
    
    class User < ActiveRecord::Base
      has_many :articles
    end
    

    Then you can do things like this:

    @user.articles.of_sector(4) 
    =>    # articles belonging to @user and sector of 4
    @user.articles.of_sector(5,6) 
    =>    # articles belonging to @user and either sector 4 or 5
    @user.articles.of_sector([1,2,3,]) 
    =>    # articles belonging to @user and either sector 1,2, or 3
    
    0 讨论(0)
提交回复
热议问题