分享 Rails inverse_of 研究

hw676018683 · 2018年03月31日 · 最后由 oatw 回复于 2020年05月26日 · 6850 次阅读
本帖已被管理员设置为精华贴

作用

class Post < ApplicationRecord
  has_many :comments
end

class Comment < ApplicationRecord
  belongs_to :post
end

测试:

comment = post.comments.first
comment.post

has_many :comments, inverse_of: false

Comment Load (0.2ms)  SELECT  "comments".* FROM "comments" WHERE "comments"."post_id" = $1 ORDER BY "comments"."id" ASC LIMIT $2  [["post_id", 1], ["LIMIT", 1]]
Post Load (0.2ms)  SELECT  "posts".* FROM "posts" WHERE "posts"."id" = $1 LIMIT $2  [["id", 1], ["LIMIT", 1]]

has_many :comments, inverse_of: :post

Comment Load (0.2ms)  SELECT  "comments".* FROM "comments" WHERE "comments"."post_id" = $1 ORDER BY "comments"."id" ASC LIMIT $2  [["post_id", 1], ["LIMIT", 1]]

inverse_of: false时,comment.post多了一条 sql

自动计算 inverse_of

上面在测试没有 inverse_of,为啥要加上inverse_of: false

因为 Rails 会去尝试计算出 inverse_of 的值

相关代码 (v5.2.0.rc2):


# activerecord/lib/active_record/reflection.rb

def automatic_inverse_of
  if can_find_inverse_of_automatically?(self)
    inverse_name = ActiveSupport::Inflector.underscore(options[:as] || active_record.name.demodulize).to_sym

    begin
      reflection = klass._reflect_on_association(inverse_name)
    rescue NameError
      # Give up: we couldn't compute the klass type so we won't be able
      # to find any associations either.
      reflection = false
    end

    if valid_inverse_reflection?(reflection)
      return inverse_name
    end
  end
end

那什么情况 Rails 不去自动计算出 inverse_of 的值呢?

相关代码 (v5.2.0.rc2):

# activerecord/lib/active_record/reflection.rb

def can_find_inverse_of_automatically?(reflection)
  reflection.options[:inverse_of] != false &&
    VALID_AUTOMATIC_INVERSE_MACROS.include?(reflection.macro) &&
    !INVALID_AUTOMATIC_INVERSE_OPTIONS.any? { |opt| reflection.options[opt] } &&
    !reflection.scope
end

有四种情况:

  1. 主动设置 inverse_of 为false,也就是上面我们测试的情况
  2. 关系类型不是 [:has_many, :has_one, :belongs_to] 其中一种,就是去除了has_and_belongs_to_many
  3. 关系类型的 options 不能包含 [:conditions, :through, :foreign_key]
    • conditions: 老版本的 Rails 才有的,不用管
    • through: 通过第三表关联的,也不用去操心
    • foreign_key: 指定了外键的
  4. 设置了 scope,比如 has_many :comments, -> { order(id: :asc) }, Rails 的解释如下(不是很明白):

Anything with a scope can additionally ruin our attempt at finding an inverse, so we exclude reflections with scopes.

Rails 是根据 class 的 name(去除了 namespace) 或者 as option(多态),去计算 inverse_of inverse_name = ActiveSupport::Inflector.underscore(options[:as] || active_record.name.demodulize).to_sym

最后

最近遇到 Rails 的一个 bug,所以研究了一下 inverse_of

更新nested attributes时,touch: true没有生效

这个 bug 还未解决,但可以通过加 inverse_of 解决,相关 https://github.com/rails/rails/pull/29132

以上研究希望能帮助大家更理解 inverse_of,如有不对,望能指出,共同进步

huacnlee 将本帖设为了精华贴。 04月02日 10:15

加了 scope 也可以的

xinxin 回复

inverse_of 主要作用是避免 sql 查询,会直接从内存中读取数据

seven.lee 回复

避免 sql 查询只是 inverse_of 的作用之一,它更重要的意义是保证上下文中 post 实例是同一个对象,如果它在某处被修改了,不会与 post 的另一份实例形成竞争关系

IChou 回复

恍然大悟,突然想起来之前遇到的一个 bug...

补充一下,按照 Rails 6.0.3.1 源码里的写法,如果是 polymorphic 类型的 belongs_to associations 也不能自动计算 inverse_of。 @hw676018683 楼主有没有兴趣再翻翻源码,我最近经常自我怀疑,不太相信自己的判断~😂

def can_find_inverse_of_automatically?(_)
  !polymorphic? && super
end

https://github.com/rails/rails/blob/v6.0.3.1/activerecord/lib/active_record/reflection.rb#L735

oatw 回复

已经转 go 了,对 rails 没啥兴趣了,你加油,相信自己

ps: ruby 的动态性+rails 的抽象性 -> 研究 rails 源码真是一件掉头发的事情😂

hw676018683 回复

哈~哈哈哈~大哥你也加油。

ps:拿出手机,打开前置摄像头,掀开刘海儿~偷偷照一照我的发际线。

需要 登录 后方可回复, 如果你还没有账号请 注册新账号