Rails 关联方法如何工作?

发布于 2024-08-06 20:28:50 字数 1078 浏览 10 评论 0原文

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

class User < ActiveRecord::Base
   has_many :articles
end

class Article < ActiveRecord::Base
   belongs_to :user
end

现在我可以做类似的事情,

@user = User.find(:first)
@user.articles

This fetches属于该用户的文章。到目前为止,一切都很好。

现在,我可以在一些条件下继续查找这些文章。

@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

And do

@user.articles.of_sector(3)

现在我的问题是,这个 find 如何在使用关联方法获取的 ActiveRecord 对象数组上工作?因为如果我们实现自己的名为 articlesUser 实例方法,并编写我们自己的实现,它会为我们提供与关联方法完全相同的结果,即在 fetch 数组上查找ActiveRecord 对象将无法工作。

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

How do rails association methods work? Lets consider this example

class User < ActiveRecord::Base
   has_many :articles
end

class Article < ActiveRecord::Base
   belongs_to :user
end

Now I can do something like

@user = User.find(:first)
@user.articles

This fetches me articles belonging to that user. So far so good.

Now further I can go ahead and do a find on these articles with some conditions.

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

Or simply declare and associations method like

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

And do

@user.articles.of_sector(3)

Now my question is, how does this find work on the array of ActiveRecord objects fetched using the association method? Because if we implement our own User instance method called articles and write our own implementation that gives us the exact same results as that of the association method, the find on the fetch array of ActiveRecord objects wont work.

My guess is that the association methods attach certain properties to the array of fetched objects that enables further querying using find and other ActiveRecord methods. What is the sequence of code execution in this this case? How could I validate this?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(4

挖鼻大婶 2024-08-13 20:28:50

它实际上是如何工作的,关联对象是一个“代理对象”。具体类是 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 ' 会告诉你。现在,当您在 AssociationProxy 上调用 class 时,您将看到一条日志消息,告诉您它正在将其发送到目标,这应该可以让您更清楚地了解发生了什么。这全部适用于 Rails 2.3.2,在其他版本中可能会发生变化。

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.

拒绝两难 2024-08-13 20:28:50

正如已经提到的,活动记录关联创建了一系列便捷方法的度量标准。当然,您可以编写自己的方法来获取所有内容。但这不是 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无缝集成。

混合的原因& match 不起作用与问题中定义的 of_sector 方法不起作用相同。 edited_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

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
烟若柳尘 2024-08-13 20:28:50

正如前面提到的,当你做的时候,

@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

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
撩动你心 2024-08-13 20:28:50

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

序列类似于

  1. User 模型中的设置 articles,即执行 has_many :articles
  2. ActiveRecord 自动将方便的方法包含到模型中(例如 >大小空?查找全部第一个等)
  3. 设置Article 中的 user,即执行一个 belongs_to :user
  4. ActiveRecord 自动将方便的方法包含到模型中(例如 user= 等),

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

您可以在此处阅读更多相关信息:< a href="http://guides.rubyonrails.org/association_basics.html#detailed-association-reference" rel="nofollow noreferrer">http://guides.rubyonrails.org/association_basics.html#detailed-association-reference

希望这有帮助 =)

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

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文