11/01/2006

為你的 N:M Join Table 加入屬性

原本的設計

N:M Join Table 一般來說,通常只有兩個 Column ,兩個都是另外兩個 table 的 Foreign Key。舉例來說,假設今天有兩個 Table ,一個是 people ,另外一個是 firend_relations。人跟人之間的朋友關係由 friend_relations 來描述,所以 friend_relations 裡面的欄位應該有
  1. person_id
  2. friend_person_id
根據 Agile Web Development with Rails這本書,裡面有一段話
Active Record automatically includes all columns from the join tables when accessing rows using it. If the join table included a column called id, its id would overwrite the id of the rows in the joined table.
如果 join table 裡面,加上 id 這個欄位的話,當我們使用
Person.find(1).friends.each do |friend|
puts friend.id
end
這裡的 i.id 不是朋友在 people 這個 table 裡面的 id ,反而是 Join Table friend_relations 的 id 。也就是說,Join Table 裡面除了 Foreign Key 以外的欄位,都會覆蓋掉所有關聯性 Table 的欄位。這在 Coding 時,很容易造成混淆,必須盡量避免。

好處?

但是有時候這個特性很好用,假設有一需求。我想知道我跟你之間的朋友關係是啥時建立的?我們如果將這個時間 created_at 寫在 Join_Table frined_relations 裡面,那麼
Person.find(1).friends.each do |friend|
puts frined.created_at
end
這裡的 i.created_at 就是 freind_relations 裡面的 created_at (朋友關係建立的時間),而非 people 裡面的 created_at (person 資料建立的時間),因為已經被覆蓋掉了。的確,當你使用到 person.friends 的時候,通常你在使用 朋友關係 的資料,而非 這個人的基本資料 ,所以他被覆蓋過去也是很合理的。

我們還可以在 friend_relations 加入 relation 這個 column ,裡面放 '點頭之交' ,'朋友' ,'好友','密友' 之類的關係,可以作出
Person.find(1).friends.each do |friend|
puts friend.name+'是'+friend.relation
end
結果就會出現
小李是好友
小王是點頭之交
小陳是朋友
之類的結果,可說是相當方便的特性。

Bug? 還是特色?

不過今天發現了一個問題,HABTM 的 Table 在 Ruby on Rails 裡面是 read only ,我們無法寫入 Attribute (因為他沒有相對應的 Model),並且當我們使用 << 傳入 HABTM 關係時,我發現到假設 JOIN Table 有任何 column 跟傳入的 Column 同名,他會直接寫入 JOIN Table 裡面,也就是說假設,friend_relations 跟 people 都有 created_at 這個 column
a = Person.find(1)
b = Person.find(2)
a.friends << b
這個時候,friend_relations 裡面的 created_at 的確有新的值輸入,但可惜不是創造關係的時候的時間,而是 b 的 created_at 時間。如果我在 friend_relations 加入一個 Person 原本就有的
column,像是 name 好了 。我發現到執行上面的程式他會直接將 b 的 name 欄位直接寫進去,這真是太有趣了。

根據 Rails 的 Many-to-Many 討論 這篇的講法

主要來說 has_and_belongs_to_many 比較簡單,但是那個 join table 不適合再加欄位值,比如說該關聯建立的時間之類的,雖然 push_with_attributes 這個方法可以塞資料進去,但是卻是很亂的糟方式,已經不被推薦使用。

而 has_many :through 功夫多一些,結構更完整,用新增一個 join model 的方式來建立關聯資料。自己有一個 model class,因此使用上也比較豐富。就像一般 model 一樣,先 new 一個出來,assign 好那兩個關聯物件,再填好其它資料,最後 save 即可。

還有 Many-to-many Dance-off! 讓我新發現到 has_many :through 這個方式。我正在仔細思考重構的方式。不過在 Project 重構之前,我先使用 push_with_attribute 進行簡單的 Bug Fix
class User < ActiveRecord::Base
def add_friend user
firendss.push_with_attributes( user , :created_at => Time.now )
end
end
如此最起碼可以使用
a = Person.find(1)
b = Person.find(2)
a.add_friend(b)

達成最起碼的 BUG Fix。

結論

這個特性已經 缺點多於優點了 ,不推薦。雖然 push_with_attributes 可以寫入,但是不能修改,所以只能作一次性的寫入,像是 created_at 還能用,其他的就有問題了。

沒有留言: