iT邦幫忙

2017 iT 邦幫忙鐵人賽
DAY 3
0
Modern Web

Ruby礦工的Rails地圖系列 第 3

STI , MTI 與多型關聯(Polymorphic Associations) 系列二

MTI (multiple table inheritance)

昨天我們介紹了STI,運用在大量重複欄位出現時
可以用同一個表格實作複數以上的model
但是當不同的欄位比相同的還多很多
卻又想要適度的統整時,STI就不太適合
這個時候就可以考慮比較不為人知的MTI
顧名思義,這是複數表格繼承
接續上面的範例
也就是原本的Dog與Cat都有自己的表格,但同時也繼承了Animal
實作上稍有不同
首先在migration的時候就要特別注意,
雖然實作的model是Dog,Cat,但是table不一樣

#animal一樣
create_table :animals do |t|
  t.string :name
  t.string :type
  t.timestamps null: false
end
#cat,dog需要以companions後綴
create_table :cat_companions do |t|
  t.integer :cat_id
  t.string :sleep
end
create_table :dog_companions do |t|
    t.integer :dog_id
  t.string :run
end

接著rake db:migrate

#animal.rb
class Animal < ActiveRecord::Base
	#...
end
#cat.rb
class Cat < Animal
	has_one :companion, class_name: "CatCompanion", inverse_of: :cat, dependent: :destroy, autosave: true
	delegate :sleep, :sleep=, to: :lazily_built_companion
	private
	def lazily_built_companion
		companion || build_companion
	end
end
#cat_companion.rb
class CatCompanion < ActiveRecord::Base
  belongs_to :cat, inverse_of: :companion
  validates :cat, presence: true
end
#dog.rb
class Dog < Animal
	has_one :companion , class_name: "DogCompanion", inverse_of: :dog, dependent: :destroy, autosave: true
	delegate :run, :run=, to: :lazily_built_companion
	private
	def lazily_built_companion
		companion || build_companion
	end
end
#dog_companion.rb
class DogCompanion < ActiveRecord::Base
  belongs_to :dog, inverse_of: :companion
  validates :dog, presence: true
end

登愣!設定到這邊就完成啦!
如果進console測試
Dog.create(name: "Woo", run: "fast")
是可以成功寫入
Dog這個model不存在真實的表格,裡面的欄位由Animal與DogCompanion組合而成
取用時也跟一般的實體model沒什麼不同
例如:

dog = Dog.last
dog.name
=> "Woo"
dog.run
=> "fast"

可惜的是,這樣方便的技術(除了初始的設定稍微麻煩一些之外)有一個障礙
原生的ActiveRecord可能會有問題,例如:

create_table :dog_companions do |t|
  t.integer :dog_id
  t.integer :master_id
  t.string :run
end
create_table :masters do |t|
  t.string :name
  t.timestamps null: false
end

假設新增了主人(Master)的model,每個主人會有多隻狗
Master對Dog為一對多

#master.rb
class Master < ActiveRecord::Base
	has_many :dogs
end
#dog.rb
class Dog < Animal
    #...省略
	belongs_to :master
    delegate :run, :run=,:master_id,:master_id=, to: :lazily_built_companion
    #...省略
end

在給取值時會發生下面的問題

m = Master.create(name: "Bater")
m.dogs
=>Dog Load (0.7ms)  SELECT "animals".* FROM "animals" WHERE "animals"."type" IN ('Dog') AND "animals"."master_id" = ?  [[nil, 1]]
ActiveRecord::StatementInvalid: SQLite3::SQLException: no such column: animals.master_id: SELECT "animals".* FROM "animals" WHERE "animals"."type" IN ('Dog') AND "animals"."master_id" = ?

我們可以看到他依然會在animal裡面試圖尋找master_id這個欄位
如果反過來從dog去查詢

d = Dog.create(name: "Woo",run: "slow",master_id: 1)
d.master_id 
=> 1

雖然直接呼叫master_id可以得到正確的值
但是d.master卻只會得到nil的回應
要解決這樣的問題,可以透過增加model方法,例如在Dog Model寫下

def get_master
    Master.find(master_id)
end

之後的Dog就可以透過get_master方法取得Master
可惜反向卻會遇上一樣的問題
Dog.where(master_id: 1) 依然只會在animal裡面找master_id
這個問題目前還沒找到解答,等知道了之後再行補充。

以上就是今天的內容,有沒有很簡單呢?


上一篇
STI , MTI 與多型關聯(Polymorphic Associations) 系列一
下一篇
STI , MTI 與多型關聯(Polymorphic Associations) 系列三
系列文
Ruby礦工的Rails地圖30

尚未有邦友留言

立即登入留言