What have you found for these years?

2008-10-15

DataMapper vs ActiveRecord

其實我懶得多做什麼比較了,真的。因為一用就知道兩者勝負差異之大...

剛開始用 ActiveRecord 會覺得他很方便,因為什麼都是先假設好了。
用久了就會開始覺得他有些地方很神秘,搞不太懂為什麼。
深入撰寫 extension 並閱讀程式碼之後,會發覺 AR 內部根本就是一團亂。

就在我一直無法用 AR 的 named_scope 做出 pagify 後,我調查了 will_paginate.
我發覺 will_pagiante 也同時擴充了 array! 其實以 lib 而言,
真的應該避免針對一些很常用的基本型別進行「任何」修改。
framework 是例外,因為兩個 framework 通常不會混著用,但 lib 會!

尤其在 ruby 這種環境,更不應該修改基本型別。尤其是基本型別的:
require, const_missing, method_missing, 這三個!
rails 內部改寫了這些東西,導致出錯時的 call stack trace,
都會變得很恐怖。

尤其 rails 一堆 plugin 又瘋狂在 alias_method_chain,
還 chain 在 method_missing 底下... 於是當你寫:

123.asd_qwe


時,你就會看到很恐怖的 method_missing trace,
哇哇,一堆毫無關係的 lib 都進去了...

噢,扯遠了。這邊其實只是想說 AR 比 DM 爛很多而已。
pagify 的實作應該可以看出些端倪。我為了同時支援這兩個 ORM,
把相關的地方都抽出來,所以可以很清楚地看到兩者不同之處。

DM 大部份的東西都很直覺,AR 有些地方就是很奇怪,而且 consistency 非常非常低。

AR:
user.pets.all(:conditions => ['name = ?', 'godfat'])


我常常不小心寫成:
user.pets(:conditions => ['name = ?', 'godfat'])


這樣的結果是,忽略所有 conditions, 沒有任何錯誤。
所以撈出來的不是有條件的資料,而是全部的資料。
這邊的麻煩在於,當你說 user.pets 時是取全部的集合,
而 user.pets.all(...) 時,卻反而是部份集合?這很不合理啊。
以前看到 user.pets.find :all 時就覺得很詭異了...

DM:
user.pets(:name => 'godfat')

但是寫成這樣也行:
user.pets(:condtions => ['name = ?', 'godfat'])

像 AR 也行!
user.pets.all(:condtions => ['name = ?', 'godfat'])


all 加不加都行的原因是,DM 對於 query 有做很好的處理,
所有的 query 都是到了非讀取不可時,才會真的撈出來。
如果你一直串東西,例如:
user.pets.red.green.first.attributes.inspect

前面的 pets.red.green 都絕對不會真的進行 query,
他們只會不斷把 "query" 串起來,透過 Query 這個物件。
Query 跟 Query 間可以 merge, 也可以 merge hash!

比較可惜的是 query 不能轉成 hash 啦... 所以 dm-aggregates 的 count,
沒辦法隨便傳一個 query 進去,他的 count 只吃 hash.
但是實際上 dm-core 的每一個吃 query 的地方,幾乎都是 query 或 hash 都吃。
然後一個個 merge 起來,最後透過 with_scope 才從資料庫撈資料。

同時,在 DM 裡你可以任意定義 model 的 class method,
而這些 class method 在 collection proxy 中,直接可以使用。
裡面的所有 query 也會符合預期,因為每一個 query 都會記得前面有哪些條件。
也就是說,這功能很像 AR 的 named_scope, but far more useful and consistent!

那天本來一直想用 AR 的 named_scope 寫出 pagify, 因為 DM 我就是這樣做的。
透過 sql log 可以輕易地看出,確實都是依照最理想的狀況進行 query.
然而 AR 不太清楚要怎麼打開 sql log, 但光用 named_scope, 連正確結果都做不出來。
可能是因為 named_scope 本身是回傳 Scope 這東西?也只是猜的,
因為 AR 也同樣把各種資訊都吃掉了,如果呼叫 .class 只會看到都是 Array......
但是實際上他們根本就不是 Array, 很多 method 和行為結果都是不同的。

如果針對 Array 做擴充,我不是很確定,但 query condition 的串接我想多半會失效。
畢竟拿到 array 時,就是表示資料都拿到手了啊...
不像 DM 實際上還有用 LazyArray, 雖然我不確定用在哪裡就是了。


*


DM 要做 pagify 的方法應該有不少。我選擇了針對 model, 所以 pagify 變成這樣:
model.send :with_scope, query do
model.all(query_opts.merge(:offset => offset, :limit => per_page))
end

這邊需要用 with_scope 加一個 query, 其實是有一點點 tricky.
我也是實驗了一段時間才找到這個方法。簡單地說,這邊為了要使用 class method
一次解決 User.pagify 和 users.pagify 的用法,勢必得這樣寫。

前者不需要額外的 query condition, 而後者需要記憶 users 既存的 query.
當你說 users.ooo 時,DM 底下其實就是用 with_scope 做的。
他沒有把 class method 複製過來,而是直接使用 model 呼叫 class method,
同時把 context(也就是這邊的 query)一起丟進 with_scope 來操作。

所以我在 pagify 裡面呼叫 self, 會得到的肯定是 User, 也就是你原本的 model.
這也是在說明,model 本身的 query 會不斷隨著 context 改變而改變。
我不確定這樣是不是 thread safe, 因為我沒仔細閱讀 dm-core 的 source code.
也許有空的時候就會讀讀看吧,他的架構真的滿有意思的...

總之利用 with_scope, 就可以定義一次 class method 就解決有 association 和
沒有 association 的狀況。that is, 你可以隨時修改 class method,
所有的 model 都會受此反應,而不是因為 module_eval 而使得修改必然需要兩次以上。

AR 我看大概是做不到,他的 named_scope 比起 DM 實在太殘廢了。
所以我也只好回到 will_paginate 的方式,替「所有的」association 定義。
也因此需要:
[ActiveRecord::Base, ActiveRecord::Associations::AssociationCollection].each{ |klass|
klass.module_eval do
extend Pagify::Pagifier
def self.pagify_pager_create model, opts
Pagify::ActiveRecordPager.new model, opts
end
end
}

而這邊,will_paginate 寫得更複雜,HasManyThrough 好像有特例?
搞不太清楚,我就先直接省略掉,省得做了多餘的事。

如此一來,pagify 的部份就直接寫:
model.find(:all, query_opts.merge(:offset => offset, :limit => per_page))

即可,因為那個 model 本身也有可能會是 association proxy.

DM 要這樣做也行,我想就是針對 DataMapper::Collection 吧。


*


於是如果你在 pagify 裡面 trace caller 是誰,就會發現 DM 裡就是
User, Pet, etc. 完全就是自己定義的 model.
而 AR 的話,就會一下子是 model, 一下子會是 "Array" 了。
假的 Array, 實際上是 proxy... 不過很難判斷,之前用 console,
用 .class 看是 array, 用 kind_of? 判斷也判斷不出來。

我忘記那時候是要做什麼了,只記得很苦惱完全無法判斷資料到底撈出來了沒...
有點像是薛丁格的貓吧,一旦你觀察了,原本的狀態也被改變了...

那時我記得想用 respond_to? 去檢查 proxy 特有的 method,
印象中也是失敗的... 他們都很聰明,都有特別改寫過 respond_to? 和 kind_of?
RMagick 裡面也有幹這種事!他的 ImageList 操作會把東西 delegate 給
目前指到的 Image 上。後來我是發現他把原本的 respond_to? 改名為:
__respond_to__? 所以我就直接用這原本的 method, 跳過他改寫的,就成功了。
只是 AR 更狠,連 .class 都拿掉,變得很難掌握 query 時機。

另一方面 AR 的 proxy 種類也好複雜... DM 很單純就是 One/Many to One/Many
共四種組合。其中 ManyToMany 也處理 has many through 的東西。


*


這篇不知道為什麼,寫得有點無力。硬著頭皮寫到這... 我知道有點不知所云,
反正是個紀錄就是了... 最近應該都不算晚睡才對,但老是昏昏沉沉的,
本來打算趕快寫完的,結果居然寫不下去,只好拖到晚上再繼續寫。

我覺得太早睡也有問題啊。早上就會太早起,太早起的話下午就會開始想睡了。
而最近則是早上也會想睡啊... 該不會是氧氣不足吧?



btw, 原本我還擔心 AR named_scope 用太多,不容易移植到 DM 上。
看來這完全是白擔心了,DM 比 AR 強多囉... 要把 DM 移植到 AR 上應該才會痛苦。

4 retries:

老林 said...

發現文章漏字 XD
「更『不』應該修改基本型別」
那個「不」字少掉了 XD

godfat 真常 said...

謝了,已更正 @_@b
我還滿常這樣念過去然後就漏打...

通常自己讀一次時會發現就是了

老林 said...

有空回去 ptt C++ 板看一下 xD
yoco 大大有發一篇應該蠻值得討論的問題
和 small obj 有關的

godfat 真常 said...

回了... XD
只是沒有建設性就是了...

Post a Comment

All texts are licensed under CC Attribution 3.0