What have you found for these years?

2009-08-13

couchdb + datamapper (2)

想來想去,用比較簡單的結構的話,就是一個 model 配一個 db.
比方說:

DataMapper.setup(:default, 'couchdb://localhost/test')

然後有 model Photo, Album, User 的話就是會建立三個 db,
test_photos, test_albums, test_users.

這樣做最大的理由是做 partition 會比較容易。
其次是先減少 database 的複雜度,因為想想覺得
如果要放在同一個 db 裡,relation 一多的話,
整個情況會變得非常複雜。先想簡單的,熟悉之後再看複雜的。

一決定這樣做之後,view 要怎麼設就很清楚了。
User.has n :photos
Photo.belongs_to :user
就是 test_photos 裡有個 view, 是:

emit(photo.user_id, photo)

雖然我不確定能不能直接把整個 doc 當成 value, 總之先這樣看。
因此寫:

User.get("id").photos 時會產生:

GET http://localhost/test_users/id
GET http://localhost/test_photos/_views/user_id?
startkey=id&reduce=false

reduce 的部份,當然就是 count, 也就是 values 的 size 了。
另一邊的查詢則不需要 view.

Photo.get("id").user 時會產生:

GET http://localhost/test_photos/id
GET http://localhost/test_users/#{photo.user_id}

從這邊可以確定一件事:

belongs_to 需要儲存 parent_id, 也需要一個 view:
parent_id => data

多對多的關係則比較複雜一點,但可能還比 RDBMS 簡單?

Photo.has n, :albums, :through => :photo_albums
Album.has n, :photos, :through => :photo_albums

應該用不到那個 :through, 只要兩邊都建立 view 即可。
test_photos 有 album_ids
test_albums 有 photo_ids
兩邊都需要一樣的 view:

for(i in photo.album_ids) emit(photo.album_ids[i], photo)

另一邊當然就是名字換一下而已。
因此當我們寫:

Album.get("id").photos 時會產生:

GET http://localhost/test_albums/id
GET http://localhost/test_photos/_views/album_id?
startkey=id&reduce=false

反之亦然。

因此需要事先定義的 view 有:

1. belongs_to
2. has n :through

當然啦,第一次讀取時再建立也不是不行。
而且搞不好這樣做反而比較好?總之是實作細節。

剩下沒辦法解決的問題則是在 join 中有條件的。
例如原本的:

all(permission.send(user.relationship(visitor)).not => :none,
permission.title.not => 'password',
:status => :common)

這超複雜就很麻煩..............
還有其他的條件,例如:

all(:status.not => :system)

像這種的大概就會需要臨時產生 view?
因為在 migrate 中沒辦法判斷有沒有這種操作。
或是說像是這樣:

Photo.new.methods(false).each(&:call)

強迫執行所有的 method, 以取得所有的可能需要的 view..
算了吧,條件式怎麼辦?
因此應該是兩種作法:

1. 動態產生 view
2. 要求 model 先寫好 view

前者可能會變成需要暖機之類的。
這倒是或許可以靠 test case 來處理...
如果有 100% coverage 的話,
可以確保 test 跑完所有 view 都產生了。

但其實事先寫好 view 也沒什麼不對,應該說本來就應該這樣!
缺點就是,與其他 adapter 就會出現不同介面..
所以想先試 1.
反正沒有衝突,真的有必要再手動補 view 吧。

all(:status.not => :system) 的話,可能就是...

if photo.status != :system
emit(null, photo)

GET http://localhost/test_photos/_views/status_not_system

因此這邊會需要把整個 ruby 的 query object,
compile 成 javascript...
然後產生一份 unique 的 name 當 view 的 name.

...這樣寫的話,速度應該超快,因為一堆 index,
但不知道會不會變成 database 超肥大...
折衷方式就是如果用 equal 判斷,例如:

all(:status => :system)

這樣就可以單純拿 status 做 index 即可,
startkey 用 system 就好。唔, not equal 可能會變成要兩次:

endkey=system
startkey=ststem

然後兩次接起來。到底哪個好,現在還沒釐清...
真的要做真的會很複雜哩 :(
看來 rdbms 大概是前人經驗都幫你想好了...
所以有 best practice 在那邊。
如果要改 couchdb 的話,怎麼做 model 就還要自己想。
又搞不好慢慢等別人分享經驗還比較快 @@

嘖。

6 retries:

Hinoris said...

來說說我剛剛做到的夢吧。

傳說中四百年前,我和一些考古學家曾經探索過某個在天空漂浮的遺跡。那時的我們不小心放出了兩個強大的怪獸。怪獸的力量很強,殺了人,不過他們接著又自相殘殺,最後舊一起死了。

那天有位當時協助我考古的女助手來找我。一片混亂(忘了什麼了),她說那遺跡好像有個神明,問我要不要去看。我說好呀。

一群人經歷了一些在天空飛來飛去的事件好像,終於又抵達遺跡了。我們分成了許多小隊,小心翼翼地探索……

突然間傳來一陣虛弱的尖叫,大家都面面相覷。有了上次的經驗,誰都不敢輕舉妄動(包括我)。後來同隊的一個男性自告奮勇地進入石室,幾秒後,說他發現神了!發現神了!

我聽了非常興奮,連忙衝進去想看看神是什麼樣子。有人突然說了一句--

那不是『脂肪之神』嗎?= =?Godfat?

這才想起四百年前脂肪之神好像有和我一起去考古。結果好像莫名其妙地被封印在遺跡裡了。地上有老鼠屍體;也不知道這傢伙是怎麼活的,不過他已經瘦得像木乃伊一樣了。大家於是把Godfat抬出來。

接著畫面一轉,大家坐在一張圓桌旁開始追究責任。很多人都說是我的責任,然後一個人就把四百年前的『日誌/通聯記錄』拿給我看,說我是最後一個看到Godfat了。我看了一眼就看不下去了,說:『這是我四百年前寫的東西,文筆太爛了,我不想看。』然後又提了許多證據反駁蓄意謀殺的說法……

然後就醒了。

Lin Jen-Shin (godfat) said...

XDDDDDDDD
什麼鬼,果然夢都是很莫名其妙的

不過總覺得這幾段,好像就會讓我想到一些以前的事...(遠目)

所以你現在寫到哪了啊? XD

scrazy said...

寫得很好 @@
但是這些因為要partition產生的作法,
跟RDBMS要partition遇到的情況跟解法一樣了,
如果是這樣,
why couchdb ,why not RDBMS?

Lin Jen-Shin (godfat) said...

是嗎? XD

我不太清楚 RDBMS partition 要怎麼做,
不過總覺得,如果要 model 類似的東西,
最終走向一樣的路,結果 why not RDBMS,
確實是一個很大的問題 @@

所以或許得回頭來想想,真的是要一對一對應嗎?
是不是根本就不應該寫很複雜的查詢?

不過我覺得 CouchDB 有個好處在,
就是他的 view 會使得一些 index/cache
變得比較容易使用,而不是自己還要發明一套

感覺要研究的課題還非常非常多....

scrazy said...

>不過我覺得 CouchDB 有個好處在,
就是他的 view 會使得一些 index/cache
變得比較容易使用,而不是自己還要發明一套
這個沒錯...
後來我想想,最近在Cassandra list,
有人一直在畫model的圖,
後來有個人說:
I think a good approach to designing a Cassandra schema from scratch is to make a list of the queries that you *know* you will need to be fast and then look at your model attempts and see how well it fits while trying to minimize overhead. Example:
-All bookmarks for a user
-All of a user's bookmarks for a tag
-All bookmarks for a tag
-All tags for a bookmark
-etc..

也就是說應該視實際使用時(render view?)的需求來建構model,
這樣的方式,跟我們先從用ORM時的object relation來構思是不一樣的,
所以這類打散Relation的模式是不是真的需要ORM?

另外RDBMS也可以partition,
而這樣作會損失掉的就是 1. join/relation
2.sort/index這些可能要重處理
這些跟我們把CouchDB打散會遇到的事差不多。
當然Cassandra也是一樣了...

Lin Jen-Shin (godfat) said...

唔... 可是要能列出那些東西,就已經是確定的規格了
如果不需要 ORM, 感覺也是因為資料夠簡單才行
像是上面提到的那種超複雜權限判斷,
可能就要從給予條件而 filter,
變成本身就要有一份額外的資料,
例如 photos_viewable_friends
photos_viewable_guest
photos_viewable_owner
然後產生 photo 時,全部都要存一份
刪除或改變時,也要記得全部都去改

噫,或許不用 RDBMS 就會變成,
避免你使用到無法 scale 的 join :s
這麼一來,一開始使用 RDBMS,
接下來再慢慢轉到其他 DB, 好像也沒什麼不好?
除非一開始就打算要做到很大的規模?

另一方面,或許可以用另一種方式,
把 RDBMS 當成最後面的 back-end,
接著把 CouchDB 或 Cassandra 當 cache...
不過這樣好像就很難確保使用者能看到最新資料了 @@

Post a Comment

Note: Only a member of this blog may post a comment.



All texts are licensed under CC Attribution 3.0