最近在架構某個專案,初步決定會以特定 Framework 開發;雖然之前摸過一陣子 Symfony,不過並沒有熟到能夠閉著眼睛使用的程度。考量基於 PHP 的各種 MVC Framework 發展已成熟,這次就跳來摸 Code Igniter 了。
與 Symfony 相比,CI 號稱架構比較簡單、輕量化、速度也比較快;CI 使用的 view 非常接近 template engine,例如 smarty 的型式,要從假畫面產生樣版的動作,只要幹過 php web coder 的人應該都很熟悉。Controller 方面兩者相當接近,也都有 route 一類的 URI -> Object/Method 的機制。雖然對於第三個以後的變數預設處理方式不同,還是能透過 route 或函式實作來調整。
然而 CI 所提供的 model (功能) 就有點遜,需要自己手刻。雖然 CI 提供了 database 與 Active Record 可以簡化與資料庫的溝通,不過想到要手動針對每項核心資料建立物件、實作關聯性 (查表與產生新物件)、回寫、檢查,實在是有點煩人。前陣子才手刻 ORM 到想翻桌的我,當然無法忍受這種架構 (純屬個人情緒表現)。相對於 Symfony tutorial 曾摸過的 Doctrine 來說,這樣的 model 實在是太陽春啦。
查了一下,發現 CI 有套 ORM 叫作 http://codeigniter.com/wiki/DataMapper_ORM/,這幾天也稍微給它摸了一下。使用 Data Mapper 時,還是需要手動建立核心物件,但無需篆寫屬性相關內容;Data Mapper 會依照物件名稱,自動查找對應表格,在執行期補入內容。對於關聯性的部份,則以 $has_one 與 $has_many 兩項屬性來指定。表格命名若符合格式 (使用小寫英文,物件之複數型式; join table 則為兩個表格名稱依字母排序,底線相連) 就不用額外設定。對重覆 (例如 Post 的 Author 和 Editor 兩個欄位都是 User) 或者表格內參照 (例如實作 Tree 時的 parent_node_id),都可以用簡單的設定解決。
不過碰到比較複雜的表格,例如想把兩個 join table 擠在一起,以 ENUM 欄位來標示,這就不知道該怎麼弄了:
以購物車系統為例,如果有 `item` 與 `package` 兩類的商品可供購買,兩者均有 id 但可能重覆;訂單存在 `order` 裡,每筆訂單可以包含多個 item 與 package,並以 join table `items_orders`儲存相關資料。那麼這個 join table 有幾種可能的結構:
- id, order_id, purchase_type ENUM(‘item’,’package’), purchase_id
我個人偏好這種型式,因為只要兩者 id 格式相同,這可能是最符合正規化要求,並且沒有贅餘欄位與資料的方式;然而透過 Data Mapper,我還不懂要如何實作 - id, order_id, item_id NULL, package_id NULL
這是範例中的做法,如果有一筆 order 帶有一個 item 加上一個 package,那麼可能會在這個表格中佔兩行,分別有一個 null - 使用兩個 join table
除了很醜以外,沒有太多缺點;也要實作兩個方法分別提取 - 錯開 item / package 的 id
愚蠢到不行的做法,除非是要最小限度修改舊系統,否則千萬不要使用;會帶來很多繁瑣的限制
撇開這點不談,其實 Data Mapper 已經能很容易的操弄資料間的關聯性。然而因為他的表單資料是在 runtime 生成,因此使用 IDE 編輯 PHP 碼時,沒有辦法對這些欄位提供建議,相當可惜 ˇ ˇ 最近會來摸摸 Doctrine 與 propel 這兩套有名的 ORM,看看他們是否更適合當前需求。
Hi endielo,
我在考慮之後,還是選用了 Doctrine2,主要考量為效能、編輯難易度 (全透過 PHP 撰寫 對於文件和 IDE 整合太強大了);不過上述問題其實普遍存在於各種 ORM 系統中。
閣下所說,將 item / package 視為不同的類別,分別與 order 設定關聯,其實屬於方法3;撇開大量 code 重複的問題,實做上的擴展性很好。
透過 Doctrine2 的 STI 其實與方法 1 完全相同,而且可以做得很漂亮;可惜我在寫這篇文章時還沒看到!因為 item/order 存在於同一個主表 (parent table) ,因此又能滿足資料庫正規化、錯開 ID 等需求;這也是小弟目前選用的作法。
上面的方法 1-3 對於「新增類別」其實都不會有問題;方法 4 指的是設定兩者 Autoincrement 參數,來錯開 id(例如一個從 1 開始 step 2, 另一個從 2 開始 step 2)。雖然書上有寫這種作法,不過光想到「要是有第三種類別出現」需要做的工,我想還是別採用的好。
小弟使用 CI / ORM 得資歷尚淺 (平常習慣自己手打 Model),如果有什麼錯誤還請多多指教啊 OTZ
你好.
我也第一次使用 DM , 雖然沒有真正比較過其餘兩個ORM
但一般使用我覺得 Datamapper 是可以完全可以應付.
我在想..如果我要跟閣下一樣做購物車系統.我要如何做呢.
先想想方法(1) 的做法.
DM 可以於 Package 及 Item 的 Class $has_many 中.
‘手動’ set relation 的 join_other_field 為 ‘purchase’
這樣.每次 join 時. DM 就以 ‘purchase’ . ‘_id’ 的方式關聯.
解決以 class name 作為關聯的約定問題.
當然, 我還要在每次 get() 時.
都要刻意加上 ->where(‘purchase_type’ , ‘item’)->get();
或者 ->where(‘purchase_type’ , ‘package’)->get();
但回想..除非遇有第三種物品..否則我會用第(2)個方法.
因為日子久了…寫 get 時忘了寫 那句 where
小小的 bug 就要花幾小時了..
因為文化不同..我不太肯定你說的”錯開”是否解作:
item 及 package share 同一個 auto id
如果是.其實第(4)個方法我又正測試中及使用.
方法其實即是其他 ORM 的 Single Table Inheritance,
但因為DM STI不支撐,
我必需自己稍作修改一下.詳細我就不再述了..
不過使用ORM最重要一點..就是約定..
我這半年.不停來來回回看 DM 的手冊 / source code.
因為很多時..我跟本沒想到會有這個約定..
只是照自己的’想法’寫..就會發生奇怪的bugs
網路上關於 Doctrine 與 Propel 的討論可真不少;整體而論,Propel 現行 1.5 版已引入 Active Record,因此引人詬病的 Criteria 已不復在;而 Doctrine 2 則改採 Data Mapper 語法。
Doctrine 1 並不會實作 getter/setter,因此 IDE 無法提供 code hint;這點在 Propel 不成問題;看來現在的目標是 propel 了 !