關於ActiveRecord 的 inverse_of

在看spree的source code 的時候發現他在很多model的關聯裡都會加上inverse_of的屬性,好奇地找了一下資料才發現原來是個好物啊!接下來就來說一下inverse_of這個東西。

inverse_of 是什麼?

簡單的來說就是兩點
1.讓兩個互相關聯的model可以指到同一個instance物件,在model的資料有更新時讓其他有關聯的model可以即時更新對方的資料。
2.減少資料重複查詢。
這樣講應該很難了解實際上到底是幹麼用的,下面來舉個例子說明一下。

Example

models
class Parent < ActiveRecord::Base
  has_many :sons
end

class Son < ActiveRecord::Base
  belongs_to :parent
end

在沒有inverse_of的情況下

controller
parent = Parent.first
parent.name # => "john cena"

son = parent.sons.first
parent.name == son.parent.name # => true

parent.name = "sean"
parent.name == son.parent.name # => false

parent.name # => "sean"

son.parent.name # => "john cena"

從上面的程式可以看到在沒有inverse_of的情況下parent model更改資料後,再用son model反關連回去竟然得到的是舊的資料,也就是說parent在更新資料後son裡面的parent並沒有跟著一起更新,所以資料還會是舊的(指的是他們其實並不是指向同一個instance),所以這時候就要加上inverse_of的屬性使兩個互相關聯的model可以指向同一筆資料(同一個instance)

models
class Parent < ActiveRecord::Base
  has_many :sons , inverse_of: :parent
end

class Son < ActiveRecord::Base
  belongs_to :parent, inverse_of: :sons 
  #如果這邊是被用has_many關聯的話後面得model名稱就會是複數,所以是sons,如果是用has_one的話就會是son。

end

等等?!我跟著你的方法照著做但出來的結果怎麼會不一樣?

如果照著上面的方式做你會發現其實有沒有加inverse_of比對出來的結果都會是true

controller
parent = Parent.first
parent.name # => "john cena"

son = parent.sons.first
parent.name == son.parent.name # => true

parent.name = "sean"
parent.name == son.parent.name # => true

parent.name # => "sean"

son.parent.name # => "sean"

那上面講的是唬爛的嗎?當然不是!!其實上面的情況是在rails 4.1之前會發生的情況,在rubyonrails guides association_basics裡有講到

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:
:conditions
:through
:polymorphic
:foreign_key

除了上面conditions,through,polymorphic,foreign_key的情況外,預設的association會幫你自動加上:inverse_of,在Exploring the :inverse_of Option on Rails Model Associations這篇文章的最下面也有講到

Good News
As of version 4.1, Rails will try to automatically set the :inverse_of option for you (pull request). Given our example with Criminal having a :belongs_to association with Prison, it will attempt to derive the inverse from the class name -- in this case :criminals based on the class Criminal. Obviously, this falls apart when the names do not match up. For example, when using :class_name or :foreign_key options on your associations. In that case, :inverse_of has to be explicitly set to the correct names, so it's still worth knowing how :inverse_of works.

但其實還有個情況是上面沒有提到的(不會自動加上inverse_of),那就是在accepts_nested_attributes_for的情況下。

accepts_nested_attributes_for情況下

在有加入accepts_nested_attributes_for的情況下的話就會發生像上面提到的一樣,更新parent.name後son.parent.name不會跟著一起更新。

models
class Parent < ActiveRecord::Base
  has_many :sons
  accepts_nested_attributes_for :sons
end

class Son < ActiveRecord::Base
  belongs_to :parent 
end
controller
parent = Parent.first
parent.name # => "john cena"

son = parent.sons.first
parent.name == son.parent.name # => true

parent.name = "sean"
parent.name == son.parent.name # => false

parent.name # => "sean"

son.parent.name # => "john cena"

所以就要為model加上inverse_of的屬性

models
class Parent < ActiveRecord::Base
  has_many :sons , inverse_of: :parent
  accepts_nested_attributes_for :sons
end

class Son < ActiveRecord::Base
  belongs_to :parent , inverse_of: :sons
end

這樣在更新parent資料後son.parent也會指向更新完資料。

inverse_of的另一個好處

inverse_of的另一個好處就是減少重複的資料查詢

例子一:在沒有inverse_of的情況下做查詢

models
class Parent < ActiveRecord::Base
  has_many :sons 
  accepts_nested_attributes_for :sons
end

class Son < ActiveRecord::Base
  belongs_to :parent 
end
controller
parent = Parent.first
son = parent.sons.first
son.parent == parent
`Parent Load (0.2ms)  SELECT  "parents".* FROM "parents" WHERE "parents"."id" = ? LIMIT 1  [["id", 2]]
true`

例子二:在有inverse_of的情況下做查詢

models
class Parent < ActiveRecord::Base
  has_many :sons ,inverse_of: :parent
  accepts_nested_attributes_for :sons
end

class Son < ActiveRecord::Base
  belongs_to :parent ,inverse_of: :sons
end
controller
parent = Parent.first
son = parent.sons.first
son.parent == parent
true

從例子一跟例子二看得出來哪裡有差別了嗎?
從上面可以看到在做son.parent == parent的時候沒有inverse_of的情況有去對db做query,在有inverse_of的情況下沒有在對db做一次query而是直接顯示比對結果,這是因為inverse_of會幫你只載入一個 parent object 物件,所以當你已經有了parent物件後之後再做查詢時就不會再幫你載入一次,進而提高效能。

在accepts_nested_attributes_for及validates存在的情況下

這個情況有點特別,他詳細的情況是這樣的,在has_many or has_one的model下使用accepts_nested_attributes_for,然後在belogs_to的model裡有validates,在這個情況下如果沒有加上inverse的屬性是無法寫入資料庫的。詳細可以看一下下面的code集截圖。

models
class Parent < ActiveRecord::Base
  has_many :sons
  accepts_nested_attributes_for :sons
end

class Son < ActiveRecord::Base
  belongs_to :parent
  validates :parent, presence: true
end


在有inverse_of情況下,可以看到資料就會在create的時候insert into 進db裡。

models
class Parent < ActiveRecord::Base
  has_many :sons , inverse_of: :parent
  accepts_nested_attributes_for :sons
end

class Son < ActiveRecord::Base
  belongs_to :parent, inverse_of: :sons
  validates :parent, presence: true
end

inverse_of不會作用的情況下

貼三個不語言可以參考一下

There are a few limitations to inverse_of support:
They do not work with :through associations.
They do not work with :polymorphic associations.
They do not work with :as associations.
For belongs_to associations, has_many inverse associations are ignored.

inverse_of 有幾點限制:
不能與 :through 關聯同時使用。
不能與 :polymorphic 關聯同時使用。
不能與 :as 選項同時使用。
對 belongs_to 關聯,會忽略 has_many 所設定的 inverse_of。

ただし、inverse_ofのサポートにはいくつかの制限があります。
:through関連付けと併用することはできません。
:polymorphic関連付けと併用することはできません。
:as関連付けと併用することはできません。
belongs_to関連付けの場合、has_manyの逆関連付けは無視されます。

參考資料

railsguides.jp 双方向関連付け
rubyonrails 3.5 Bi-directional Associations
rails.ruby.tw 3.5 雙向關聯
複数の子レコードを作成・更新する. accepts_nested_attributes_for
Exploring the :inverse_of Option on Rails Model Associations
ActiveRecord4, Rails4のinverse_ofについて理解したメモ
3.5 双方向の関連付け

comments powered by Disqus