What have you found for these years?

2008-11-19

rails 的 polymorphic association

簡介可以看這篇:
Understanding Polymorphic Associations

談這個就要再把去年拿出來講了。去年完全不懂 relational database 怎麼
運作的時候,一直在想要怎麼做各種 OO 上的結構?其中一個方法是 single
table inheritance
, abbr. STI, 就是讓某個 instance 可以有某個
parent pointer, 指向某個 child instance. 也就是說,長得像這樣:


article
| (has a)
/ \
image audio

一個文章可以夾一個圖片檔或聲音檔。而這個 has a 則是透過:

attachment
| (is a)
/ \
image audio

也就是說,寫成 C++ 像是:

class Attachment; // forward declaration
class Article{
Attachment* attachment;
};

class Attachment{
public:
virtual string to_blob() = 0;
string title;
};

class Image: public Attachment{
public:
virtual string to_blob(){
return blah;
}
int width, height;
};

class Audio: public Attachment{
public:
virtual string to_blob(){
return blah_blah;
}
int length;
};

類似這個樣子。而在 relational database 中,則是只有 attachments
這個 table, 其中紀錄所有的 derived classes 的 property,
在這邊就是需要 length, width, height, title, 全都來一份。
接著另有一個 type 紀錄實際上這個 instance 的 class 是什麼?
讀出來時,就會依照這個屬性去還原物件,例如:

Object.const_get(data[:type]).new(data)


資源浪費,沒錯,不過 relational database 就是和 OO 對不起來,
只能想盡各種方法去模擬。另一個 OO 上比較簡單的概念,在 relational
database 更麻煩,就是最普通的 has a. 擴充上例:

class User{
Attachment* attachment;
};

光是這樣就辦不到了,因為一般 association 是作成被擁有的人,
有一個 foreign key. 也就是說,Attachment 裡有個 article_id,
表示這個 attachment 是被誰擁有。如果這邊也要 User has an attachment,
則也需要 user_id, 然後 attachment 就會爆炸,塞滿各種無用的 id...

解法是反過來從屬關係,變成 attachment has a user, has an article,
etc. 也就是說,讓附檔去擁有別人,這樣 user 需要 attachment_id,
article 需要 attachment_id, 就不會造成 attachment 爆炸了。

我不太懂為什麼 rails 要作成這種樣子,我猜是為了跟 has many 一致吧?
因為一個 instance 只能有一個 ooo_id, 所以如果 has many 的主人擁有
ooo_id 的話,就變成需要無止盡的 id, 例如:

class User{
Attachment** attachments;
};

不幸的是,沒有 array 這種概念(postgres 好像有),這做不來。除非是讓
attachments 本身變成一個 id string, 例如:"1,2,3,4" 然後從 db 中讀出來,
再去做切割成一堆東西。但這種方式,會使得資料庫搜尋變得很難做,
因為層級矮了一層,像是 first class 變成 second class 那樣的感覺。

也就是說,讓被擁有的人擁有主人的 id, 就能使得多筆資料,同時都有相同的主人,
這樣 has many 就成立了。因為 has many 需要讓 foreign key 寫在被擁有的人,
所以 has a 也就變成寫在被擁有的人身上,我猜是因為這樣。

因此,這邊利用反轉從屬關係,也是可以辦到的。反正 one to one association,
其實根本就沒有真正的從屬關係,兩邊是對稱的。但是如果擴充到 many to many,
那情況就不一樣了。這邊就需要引入 has many through, 透過一個 relationship
來建立兩邊的 association, 像是:

o o o
| /|\ |
r r r r r
|/ | |/
x x x

這樣一來,r 只要同時有 o_id 和 x_id, 而 o 本身 has many r,
x 也 has many r, 就能透過 r 找到所有屬於他的 x/o.

然而如果這邊再加進來一個 s 怎麼辦?
o has many x
x has many o
s has many x
x has many s
這邊 o 就是 Article, 而 x 就是 Attachment, 而 s 就是後來的 User.
變成 r 還需要一個 s_id, a_id, b_id, c_id, etc....

因此 rails 提出 polymorphic association, 讓 r 本身不要一堆 id,
而是改用 owner_id, 和 owner_type, 這是用來宣示這個 relation 到底是
user 的呢,還是 article 的?這樣做的好處就是,於是你可以只有一種
relationship, 就能傳達出兩種以上的 association.

這樣做的好處是,attachment 就真的是 attachment.
我的意思是,你可以很輕易地抓出所有的 attachment, 不管他屬於誰。
缺點就是,如果 attachment 之間並沒有這樣強烈的關係。

例如,我們有兩個 data structure 相同,但行為完全相異的 class.
class 2D_Point; class 2D_Dimension; class HP_MP;
現在有三個各自有 2 個 real number 的 class, 但是他們是完全無關的。
假設我們有更多... 這樣就能用 polymorphic association 來用,
於是我們只要定義一個 table, 一個 relation, 就能完工了。

問題是,完全不同的資料,其實根本沒有放在一起的必要。
或是,如果這個 table 根本會成長到很誇張肥大的程度...
那麼我們就有必要分開這些資料,而不是全部混在一起。
例如:user_comments, photo_comments, article_comments,
blog_comments, etc_comments, 一大堆資料全部塞在同一個 table,
這也會變得很恐怖,最好拆開來。

看了 datamapper mailing list 上的討論:Polymorphic Associations
讓我忽然間開始質疑 rails 的作法。最後又瞥到 dm-is-remixable,
不禁想拍案說這才是對的啊!ruby 有這麼強大的彈性,沒有必要把難過的地方,
卡死在 database 上。don't repeat yourself, 但是 repeat program
可無所謂。透過 metaprogramming, 我們可以產生無數容易管理的程式,
我們可以生出一堆 UserComment, PhotoComment, ArticleComment,
OOOComment, 而這些全部當然都能共享實作,透過 mixin 或 inheritance,
或是單純的 metaprogramming (and duck typing), 都可以很輕易辦到。

當然,這不是在說這種作法能完全取代 polymorphic association,
只是另一種,我覺得比較乾淨的 approach.


==
我覺得 datamapper 和 merb 跟 rails 比,實在太佔便宜了。
rails 踩過的地雷,證明不可行的方式,dm 跟 merb 當然不會重蹈覆轍...
再加上學習 rails 良好之處,再加上 dm 跟 merb 還是兩個團隊...

像是偉大的烈士似的,忽然這樣覺得

0 retries:

Post a Comment

All texts are licensed under CC Attribution 3.0