20

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对象的 fetch 数组上的查找将不起作用。

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

4

4 回答 4

22

它的实际工作方式是关联对象是“代理对象”。具体的类是AssociationProxy。如果您查看该文件的第 52 行,您将看到:

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

通过这样做,class该对象上不再存在类似的方法。所以如果你调用class这个对象,你会丢失方法。因此,method_missing代理对象实现了将方法调用转发到“目标”:

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'会告诉您。现在,当您调用classAssociationProxy 时,您将看到一条日志消息,告诉您它正在将其发送到目标,这应该更清楚正在发生的事情。这是 Rails 2.3.2 的全部内容,可能会在其他版本中更改。

于 2009-10-12T14:33:28.093 回答
10

如前所述,活动记录关联创建了一系列便利方法的度量标准。当然,您可以编写自己的方法来获取所有内容。但这不是 Rails 方式。

Rails Way 是两个座右铭的结晶。DRY(不要重复自己)和“约定优于配置”。本质上,通过以一种有意义的方式命名事物,框架提供的一些健壮的方法可以抽象出所有公共代码。您在问题中放置的代码是可以通过单个方法调用替换的完美示例。

这些便捷方法真正发挥作用的地方是更复杂的情况。涉及连接模型、条件、验证等的东西。

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

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

所以让我们暂时讨论一下命名范围,因为它们给出了一个很好的例子来说明 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 无缝集成的原因。

mix & match 不起作用的原因与问题中定义的 of_sector 方法不起作用的原因相同。edit_methods 返回一个数组,其中edited_scope(以及作为链的一部分调用的 find 和所有其他 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
于 2009-10-12T09:23:44.713 回答
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
于 2012-08-16T14:26:00.020 回答
0

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

序列是这样的

  1. articles在模型中设置User,即做一个has_many :articles
  2. ActiveRecord 自动将方便的方法包含到模型中(例如size, empty?, find, all, first, 等)
  3. 设置userArticle即做一个belongs_to :user
  4. ActiveRecord 自动将方便的方法包含到模型中(例如user=,等)

因此,很明显,当您声明关联时,ActiveRecord 会自动添加方法,这很美妙,因为它处理了大量工作,否则需要手动完成 =)

你可以在这里阅读更多信息:http: //guides.rubyonrails.org/association_basics.html#detailed-association-reference

希望这会有所帮助=)

于 2009-10-07T06:22:59.547 回答