The Art of Readable Code && High Performance Comments

最近在架構某個專案,初步決定會以特定 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 欄位來標示,這就不知道該怎麼弄了:

以購物車系統為例,如果有 itempackage 兩類的商品可供購買,兩者均有 id 但可能重覆;訂單存在 order 裡,每筆訂單可以包含多個 item 與 package,並以 join table items_orders儲存相關資料。那麼這個 join table 有幾種可能的結構:

  1. id, order_id, purchase_type ENUM(‘item’,’package’), purchase_id
    我個人偏好這種型式,因為只要兩者 id 格式相同,這可能是最符合正規化要求,並且沒有贅餘欄位與資料的方式;然而透過 Data Mapper,我還不懂要如何實作
  2. id, order_id, item_id NULL, package_id NULL
    這是範例中的做法,如果有一筆 order 帶有一個 item 加上一個 package,那麼可能會在這個表格中佔兩行,分別有一個  null
  3. 使用兩個 join table
    除了很醜以外,沒有太多缺點;也要實作兩個方法分別提取
  4. 錯開 item / package 的 id
    愚蠢到不行的做法,除非是要最小限度修改舊系統,否則千萬不要使用;會帶來很多繁瑣的限制

撇開這點不談,其實 Data Mapper 已經能很容易的操弄資料間的關聯性。然而因為他的表單資料是在 runtime 生成,因此使用 IDE 編輯 PHP 碼時,沒有辦法對這些欄位提供建議,相當可惜 ˇ ˇ 最近會來摸摸 Doctrine propel 這兩套有名的 ORM,看看他們是否更適合當前需求。

3 Comments

  1. clifflu clifflu
    2011 年 03 月 19 日    

    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

  2. endielo endielo
    2011 年 03 月 14 日    

    你好.

    我也第一次使用 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

  3. clifflu clifflu
    2011 年 02 月 16 日    

    網路上關於 Doctrine 與 Propel 的討論可真不少;整體而論,Propel 現行 1.5 版已引入 Active Record,因此引人詬病的 Criteria 已不復在;而 Doctrine 2 則改採 Data Mapper 語法。
    Doctrine 1 並不會實作 getter/setter,因此 IDE 無法提供 code hint;這點在 Propel 不成問題;看來現在的目標是 propel 了 !

發表迴響

分類

%d 位部落客按了讚: