Rails Mongoid inverse_of not work as well as active_records

qinfanpeng · June 14, 2016 · Last by plpl123456 replied at August 10, 2016 · 4795 hits
Topic has been selected as the excellent topic by the admin.

原文地址:Mongoid inverse_of not work as well as active_records

Rails 4.1 以后,大多数情况下它都能自动帮我们加上inverse_of。大概也是由于这个原因,现在我们少有提到inverse_of了,但是某些情况下 Rails 不会自动帮我们加的,需要我们自己留意:

# activerecord-4.2.5.1/lib/active_record/reflection.rb:553
VALID_AUTOMATIC_INVERSE_MACROS = [:has_many, :has_one, :belongs_to]
INVALID_AUTOMATIC_INVERSE_OPTIONS = [:conditions, :through, :polymorphic, :foreign_key]

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

下面先来温习一下inverse_of相关作用。

Memory Optimization Associated Object

class Dungeon < ActiveRecord::Base
  has_many :traps
end

class Trap < ActiveRecord::Base
  belongs_to :dungeon, foreign_key: :my_dungeon_id
end    

这里之所以要加上foreign_key: :my_dungeon_id是为了阻止 Rails自动帮我们加上inverse_of

dungeon = Dungeon.first
trap = dungeon.traps.first

trap.dungeon # 此处还会去查询数据库(有点不合理,对吧)

trap.dungeon == dungeon     # => true
trap.dungeon.equal? dungeon # => false(说明它俩虽然内容相同,但在内存中却不是同一个对象)

dungeon.level == trap.dungeon.level # => true
dungeon.level = 10
dungeon.level == trap.dungeon.level # => false(也有点不合理,对吧)

下面我们移除foreign_key: :my_dungeon_id,如此一来 Rails便会自动帮我们加上inverse_of的:

class Trap < ActiveRecord::Base
  belongs_to :dungeon
end    

dungeon = Dungeon.first
trap = dungeon.traps.first

trap.dungeon # 此处不会再去查询数据库了

trap.dungeon == dungeon     # => true
trap.dungeon.equal? dungeon # => true(这才合情理嘛)

dungeon.level == trap.dungeon.level # => true
dungeon.level = 10
dungeon.level == trap.dungeon.level # => true

Creating associated objects across a has_many :through

class Post < ActiveRecord::Base 
  has_many :taggings 
  has_many :tags, :through => :taggings 
end

class Tag < ActiveRecord::Base 
  has_many :taggings 
  has_many :posts, :through => :taggings 
end

class Tagging < ActiveRecord::Base 
  belongs_to :tag, foreign_key: :my_tag_id
  belongs_to :post
end

同样加foreign_key: :my_tag_id是为了避免 Rails 自动帮我们加上inverse_of

post = Post.first
tag = post.tags.build name: "ruby"
tag.save

tag.taggings # => []
tag.posts    # => []

造成最后两行返回空的本质原因是没保存对应的关联的数据tagging。现在如果移除foreign_key: :my_tag_id,就相当于加上了inverse_of: :taggings(Rails 自动加的)变成了下面这样:

class Tagging < ActiveRecord::Base 
  belongs_to :tag, inverse_of: :taggings
  belongs_to :post, inverse_of: :taggings
end

post = Post.first
tag = post.tags.build name: 'ruby'
tag.save

tag.taggings # => [... ]
tag.posts    # => [post]

说明 Rails 在保存关联数据时,需要知道反向的关联关系。

Automatically assign associated object

class Dungeon < ActiveRecord::Base
  has_many :traps
end

class Trap < ActiveRecord::Base
  belongs_to :dungeon, foreign_key: :my_dungeon_id

  validates_presence_of :dungeon
end

dungeon = Dungeon.new
trap = dungeon.traps.build

trap.valid?  # => false 这是因为 trap.dungeon 是nil。

# 加上inverse_of

class Trap < ActiveRecord::Base
  belongs_to :dungeon # 此时会自动加上 inverse_of: :dungeon
  validates_presence_of :dungeon
end

trap.valid?  # => false

Inverse_of in mongoid

初步看起来mongoid中的inverse_of只是起到自定义关联名称的作用,并不具备上面提到的那些功效:

class Lush
  include Mongoid::Document
  has_many :whiskeys, class_name: "Drink", inverse_of: :alcoholic
end

class Drink
  include Mongoid::Document
  belongs_to :alcoholic, class_name: 'Lush', inverse_of: :whiskeys
end

alcoholic = Lush.first
whiskey = alcoholic.whiskeys.first

whiskey.alcoholic == alcoholic     # => true
whiskey.alcoholic.equal? alcoholic # => false(对此貌似没有好的方式可以解决)

# 会导致 N+1 问题
alcoholic.whiskeys.each { |whiskey| whiskey.some_method_which_use_alcoholic }

# 不太优雅的解决方案一(会再查一次alcoholic,但不会有N+1)
alcoholic.whiskeys.includes(:alcoholic).each { |whiskey| whiskey.some_method_which_use_alcoholic }

# 不太优雅的解决方案二(不会再查询alcoholic)
alcoholic.whiskeys.each do |whiskey|
  whiskey.alcoholic = alcoholic
  whiskey.some_method_which_use_alcoholic 
end

参考资料

  1. activerecord-4.2.5.1/lib/active_record/associations.rb
  2. Exploring the inverse of option on rails model associations

上次看到 inverse_of 的例子就觉得有点绕。。。当时明白了,过两天又忘了。

#1 楼 @catherine Memory Optimization Associated Object 这点好理解点。

也就是 inverse_of 在 mongoid 起不到作用了?

#3 楼 @pathbox 基本没啥用得。

jasl mark as excellent topic. 15 Jun 00:36

#4 楼 @qinfanpeng 这是否是 非关系数据库的原因?

#6 楼 @pathbox 个人感觉不是,mongoid 为啥这样设计,我是没想明白。

哈~~ 原来是这么一回事 去年的 4 月 也遇到这个问题,当时怎么看都跟 rails 的文档冲突,问了很多人都无解 最后我也不记得怎么就把自己糊弄过去了(逃

造成最后两行返回空的本质原因是没保存对应的关联的数据 tagging。现在如果移除 foreign_key: :my_tag_id,就相当于加上了 inverse_of: :taggings(Rails 自动加的)变成了下面这样:

class Tagging < ActiveRecord::Base 
  belongs_to :tag, inverse_of: :taggings
  belongs_to :post, inverse_of: :taggings
end

post = Post.first
tag = post.tags.build name: 'ruby'
tag.save

tag.taggings # => [... ]
tag.posts    # => [post]

关于这一段,我记得在 has_many :through 型的关联中,如果不手动指定 inverse_of,through 表不会有记录的 演示代码中,你显式的声明了 inverse_of,并不能证明

移除 foreign_key: :my_tag_id,就相当于加上了 inverse_of: :taggings(Rails 自动加的)

也不知道这个在后面的版本中改过了没有,有空去试一下

小王子棒👍 👍

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