Rails 关于 inverse_of 的困惑与探究

IChou · April 04, 2015 · Last by wesson.yi replied at February 28, 2019 · 6031 hits

🚨 Rails5 以后,inverse_of 已经做了不少变更,这篇帖子的内容可能已经不在符合现在的情况


最近在使用 关联 的时候,由于一点手误遇到了些问题,于是花了一下午时间来仔细读了 Guide 中关于 Active Record Associations 的部分。在看到 inverse_of 时,感觉自己突然一下就懵了。

问题缘由

我对 inverse_of 的困惑并不是在实际使用中产生的,即使不了解它也能在项目中愉快的玩耍,这似乎又旁证了 Rails 是一个很智能的框架。

不过它的 Guide 文档里这一部分就有些描述不清了(或说自相矛盾?)。关于 inverse_of 的功用倒是没有什么疑惑,只是被这混乱的文档弄得不知道哪些情况下需要去显式的声明 inverse_of , 哪些情况下 inverse_of 又是无效果的。

问题描述

Guide 中关于 inverse_of 的解释:http://guides.rubyonrails.org/association_basics.html#bi-directional-associations

其中有两处说明让我很费解:

其一:

There are a few limitations to inverse_of support:

  1. They do not work with :through associations.
  2. They do not work with :polymorphic associations.
  3. They do not work with :as associations. 4. For belongs_to associations, has_many inverse associations are ignored.

按第四条所说的,has_many 的关联是无效的,但是 Guide 中的栗子便是使用的 has_many, 而且很好的证明了 inverse_of 的效果。

【2016.6.17】这个其实没有疑问,这是指在 has_many / belongs_to 关联的两端都声明 inverse_of,只会按 belongs_to 一方的声明处理

其二:

Every association will attempt to automatically find the inverse association and set the :inverse_of option heuristically (based on the association name). Most associations with standard names will be supported. However, associations that contain the following options will not have their inverses set automatically:

  1. :conditions
  2. :through
  3. :polymorphic
  4. :foreign_key

按这个说法,只要是按约定命名的 关联 会自动加上 inverse_of, 那么,演示用例是按约定命名的吧,也不属于下面声明的四种情况,为什么加与不加是有差别的?

【2016.6.17】这个其实也没有疑问,这是因为 Rails 版本变迁,我当时看得文档没来得及更新导致的。文章后面部分已经解释了这个问题,这里提前把结论贴一下。

动手验证

验证环境 Rails 版本:4.2.1

定义模型:

# a.rb
class A < ActiveRecord::Base
    has_many :b
end

# b.rb
class B < ActiveRecord::Base
    belongs_to :a
end

测试结果:

2.2.1 :001 > a = A.create :name => 'ichou'
 => #<A id: 2, name: "ichou", created_at: "2015-04-04 06:41:41", updated_at: "2015-04-04 06:41:41">

2.2.1 :002 > b1 = B.create :name => 'kindle', :a => a
 => #<B id: 3, name: "kindle", a_id: 2, created_at: "2015-04-04 06:42:45", updated_at: "2015-04-04 06:42:45">

2.2.1 :003 > b2 = B.create :name => 'Air', :a => a
 => #<B id: 4, name: "Air", a_id: 2, created_at: "2015-04-04 06:43:10", updated_at: "2015-04-04 06:43:10">

2.2.1 :004 > a.name.object_id
 => 70264637654120

2.2.1 :005 > b1.a.name.object_id
 => 70264637654120

2.2.1 :006 > b2.a.name.object_id
 => 70264637654120

object_id 全都一样,说明 inverse_of 已经被启用了。事实上,即使严格按照 Guide 的案例来做,你也会发现结果全是 True,而不是 Guide 所说的结果。

结论:在 4.1+ 的 Rails 中,即使不手动声明 inverse_of ,has_many 关联也会自动创建,而且是有效的!

总结与使用

  1. inverse_of 的作用在于关联模型间共用实例,而不是让不同的查询在内存中存在多份 Copies. 实际运用中可以带来两个好处,一是减少数据库查询;二是在对 关联对象 修改数据后写入数据前,保证从任何一方取得的值都是最新的。

  2. 从 4.1 开始,基本的关联类型(has_many, has_one, belongs_to),若按约定命名,不需要再手动设定 inverse_of

  3. (当时的)Guide 的相关用例是有问题的,或者说是适用于老版本的 Rails,而不是当前版本。 因为给 basic associations\* 自动添加 inverse_of 是在 Rails 4.1 加入的特性。 事实上,关于 has_many 的那个例子,在 4.1 及以后的版本中已经不能复现了。

  4. inverse_of 已经支持 has_many 是从 3.2.1 开始的,4.1 开始支持自动添加 inverse_of, 但是 has_many :through 仍然需要显式声明 inverse_of

  5. 使用没有持久化的关联对象时,根据需要使用 inverse_of,否则反向调用会得到 nil 或者还未更新的 对象。详见 topics/6426


感谢 社区 和 stackoverflow 上的大大们,感谢微信群 成都 Ruby 群 里的星哥。

比较新手向的帖子,若有纰漏,烦请指正

参考文章
  1. https://ruby-china.org/topics/8560
  2. https://ruby-china.org/topics/6426
  3. http://stackoverflow.com/questions/9296694/what-does-inverse-of-do-what-sql-does-it-generate
  4. http://stackoverflow.com/questions/14927952/why-would-i-not-want-to-use-inverse-of-everywhere
  5. http://stackoverflow.com/questions/7654184/does-inverse-of-works-with-has-many
  6. http://stackoverflow.com/questions/7436173/activerecord-inverse-of-does-not-work-on-has-many-through-on-the-join-model-on

产生缘由?

"inverse_of 困惑的起源并不是在实际使用中产生的",不是这样的:

# Controller
@user = User.first
@topics = user.topics

# View
@topics.each do |topic|
  topic.user.username # 这行代码
end

上面这样的例子,很常见吧。(先不要纠结这里的写法) 如果没有使用 inverse_of 的话,上面输出用户名,每次都要查询一次数据库,找到 topic.user,而这个对象我们在之前已经取出来,在内存里了。(验证的话,你可以设置 inverse_of: false) 使用 inverse_of 后,你省略这样的查询。

当然,使用它还有其它作用,你懂的。

Guide 文档自身就描述不清(或说自相矛盾?)

这里,我同意你的看法...不过可以借助 reflection,判断是否有必要添加 inverse_of,以及添加后是否会有效。

ModelName.reflections.map do |key, value|
  p "#{key} inverse_of: #{value.has_inverse?}"
end

有的关联会自动推断 inverse_of,所以用不用其实效果一样。而有的关联加上参数后,不起作用,所以即使设置也没用。可以根据上述代码检测 ...

回答你的部分疑问

官方举的例子不恰当。

#1 楼 @leekelby 嗯,关于你说的第一点确实是,由于我比较深入的接触并运用 Rails 是 4.1 以后的版本,基本关联已经自动添加 inverse_of 了,加上平时也没有去深究,所以没有在实际运用的时候遇到问题或感受到它的重要性,囧~

然后是 has_inverse? 这个方法,你不说我到现在都还不知道有,感谢了

官方举的例子不恰当。 是的,官方的例子在 4.1 以后的版本是不能复现的~ 说多了都是泪啊

帖子写得比较乱,自己再看都有点受不了了

liuhui1226 in 请问有哪位能讲讲 inverse_of? mention this topic. 25 Sep 16:46

好贴,官方文档偶尔也坑

@IChou 想加成都 Ruby 群,我也是成都的😀

Reply to monsterooo

你微信多少

最新 Rails v5.2.2 Guide 中,使用 inverse_of 时的具体限制已经更改。就是楼主引用的“其一”,“其二”,已经变更如下:

Active Record will not automatically identify bi-directional associations 
that contain a scope or any of the following options:

:through
:foreign_key
You need to Sign in before reply, if you don't have an account, please Sign up first.