rails关联方法如何工作?

时间:2009-10-07 05:14:00

标签: ruby-on-rails activerecord model-associations

rails关联方法如何工作?让我们考虑这个例子

class User < ActiveRecord::Base
   has_many :articles
end

class Article < ActiveRecord::Base
   belongs_to :user
end

现在我可以做类似

的事情了
@user = User.find(:first)
@user.articles

这会抓取属于该用户的文章。到目前为止一切都很好。

现在我可以继续在某些条件下对这些文章进行查找。

@user.articles.find(:all, :conditions => {:sector_id => 3})

或者简单地声明和关联方法,如

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

并且

@user.articles.of_sector(3)

现在我的问题是,这个find如何处理使用关联方法获取的ActiveRecord个对象数组?因为如果我们实现自己的User实例方法名为articles并编写我们自己的实现,它给出了与关联方法完全相同的结果,那么在ActiveRecord的获取数组上找到对象不会工作。

我的猜测是,关联方法会将某些属性附加到已获取对象的数组,从而可以使用find和其他ActiveRecord方法进一步查询。在这种情况下,代码执行的顺序是什么?我怎么能验证这个?

4 个答案:

答案 0 :(得分:21)

它的实际工作原理是关联对象是一个“代理对象”。具体的班级是AssociationProxy。如果你看一下该文件的第52行,你会看到:

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

通过这样做,此对象上不再存在class等方法。因此,如果您在此对象上调用class,则会丢失方法。因此,为代理对象实现了method_missing,将方法调用转发给“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

目标是一个数组,所以当你在这个对象上调用class时,它说它是一个数组,但那只是因为目标是一个数组,实际的类是一个AssociationProxy,但你不能再见了。

因此,您添加的所有方法(例如of_sector)都会添加到关联代理中,因此可以直接调用它们。像[]class这样的方法没有在关联代理上定义,所以它们被发送到目标,这是一个数组。

为了帮助您了解这种情况,请将其添加到association_proxy.rb本地副本中该文件的第217行:

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

如果您不知道该文件的位置,命令gem which 'active_record/associations/association_proxy'会告诉您。现在,当您在AssociationProxy上调用class时,您将看到一条日志消息,告诉您它正在向目标发送该消息,这样可以更清楚地了解正在发生的事情。这完全适用于Rails 2.3.2,并且可能会在其他版本中发生变化。

答案 1 :(得分:9)

如前所述,活动记录关联创建了一个方便性的方便性方法。当然,你可以编写自己的方法来获取所有内容。但这不是Rails方式。

Rails Way是两个格言的顶点。干(不要重复自己)和“约定优于配置”。本质上,通过以有意义的方式命名事物,框架提供的一些强大方法可以抽象出所有常见代码。您在问题中放置的代码是可以通过单个方法调用替换的完美示例。

这些便利方法真正发挥作用的是更复杂的情况。涉及联接模型,条件,验证等的事情。

要在执行@user.articles.find(:all, :conditions => ["created_at > ? ", tuesday])之类的操作时回答您的问题,Rails会准备两个SQL查询,然后将它们合并为一个。您的版本只返回对象列表。命名范围执行相同的操作,但通常不跨越模型边界。

您可以通过在控制台中调用这些内容时检查development.log中的SQL查询来验证它。

所以我们暂时谈谈Named Scopes,因为它们给出了一个关于rails如何处理SQL的很好的例子,我认为它们是一种更简单的方式来演示幕后发生的事情,因为它们不是需要任何模型关联来展示。

命名范围可用于执行模型的自定义搜索。它们可以链接在一起,甚至可以通过关联来调用。您可以轻松创建返回相同列表的自定义查找程序,但之后会遇到问题中提到的相同问题。

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.

这是事情发生变化的地方:

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. 

基本上每个命名范围都会创建一个SQL片段。 Rails将巧妙地与链中的每个其他SQL片段合并,以生成单个查询,完全按照您的需要进行返回。关联方法添加的方法以相同的方式工作。这就是他们与named_scopes无缝集成的原因。

混合的原因&amp;匹配不起作用与问题中定义的of_sector方法不起作用相同。 edited_methods返回一个数组,其中,edit_scope(以及查找和所有其他AR便捷方法称为链的一部分)将其SQL片段向前传递给链中的下一个事物。如果它是链中的最后一个,则执行查询。同样,这也不起作用。

@edited = Article.edited_scope
@edited.recent_scope

您尝试使用此代码。这是正确的方法:

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

要实现此功能,您需要执行此操作:

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

然后你可以这样做:

@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

答案 2 :(得分:1)

如前所述,在做

@user.articles.class
=> Array

你实际得到的是Array。这是因为#class方法未定义,如前所述。

但是你怎么得到@ user.articles的实际类(应该是代理)?

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

为什么你首先得到Array?因为#class方法通过方法missin委托给CollectionProxy @target实例,这实际上是一个数组。 你可以通过做这样的事情来窥探场景:

@user.articles.proxy_association

答案 3 :(得分:0)

当您进行关联(has_onehas_many等)时,它会告诉模型ActiveRecord自动包含一些方法。但是,当您决定创建一个自己返回关联的实例方法时,您将无法使用这些方法。

序列是这样的

  1. articles模型中设置User,即执行has_many :articles
  2. ActiveRecord会自动将方便的方法包含在模型中(例如sizeempty?findallfirst等)
  3. user中设置Article,即执行belongs_to :user
  4. ActiveRecord会自动将方便的方法包含在模型中(例如user=等)
  5. 因此,很明显,当你声明一个关联时,这些方法是由ActiveRecord自动添加的,这是处理大量工作的美妙之处,需要手动完成,否则=)

    您可以在此处详细了解:http://guides.rubyonrails.org/association_basics.html#detailed-association-reference

    希望这有助于=)