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
方法进一步查询。在这种情况下,代码执行的顺序是什么?我怎么能验证这个?
答案 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_one
,has_many
等)时,它会告诉模型ActiveRecord自动包含一些方法。但是,当您决定创建一个自己返回关联的实例方法时,您将无法使用这些方法。
序列是这样的
articles
模型中设置User
,即执行has_many :articles
size
,empty?
,find
,all
,first
等)user
中设置Article
,即执行belongs_to :user
user=
等)因此,很明显,当你声明一个关联时,这些方法是由ActiveRecord自动添加的,这是处理大量工作的美妙之处,需要手动完成,否则=)
您可以在此处详细了解:http://guides.rubyonrails.org/association_basics.html#detailed-association-reference
希望这有助于=)