天天看點

FleaPHP 使用 Table Data Gateway 代替 Active Record 來提供資料庫通路服務

許多開發者很疑惑為什麼 FleaPHP 以高效開發為目标,卻沒有提供 Active Record 模式。本文嘗試詳細闡述這個問題。 Active Record 是什麼?

  Active Record 模式中文名為“活動記錄”,在《企業應用架構模式》(PoEAA)一書中定義如下:

    活動記錄(Active Record):一個對象,它包裝資料庫表或視圖中的某一行,封裝資料庫通路,并在這些資料上增加了領域邏輯。

  舉個例子來說,一個圖書資料表,每一條記錄就是一本圖書的資訊。那麼采用 Active Record 時,每一本圖書就是一個 Active Record 對象執行個體。

Active Record 因 Ruby On Rails 而流行

  Active Record 之是以現在這麼炙手可熱,甚至許多人将 Active Record 和 ORM 劃等号,完全是 Ruby On Rails 的原因。

在 Ruby On Rails 中,Active Record 除了最基本的将資料記錄和一個對象互相映射外,還提供了資料(而不是對象)間關聯關系的處理。例如:

  一本圖書有一個或者多個作者,是以每一個圖書對象都和多個作者對象關聯。反過來一個作者可以寫多本書,是以一個作者對象也和多個圖書對象關聯。

  在 RoR 中,我們擷取一個圖書對象時,自動就獲得了該圖書對象所對應的作者對象(本質上是圖書資料對應的作者資料)。更進一步,通過圖書對象關聯的作者對象,我們 可以擷取該作者所寫的所有圖書的對象執行個體。而這些工作,在 RoR 中隻需要幾行代碼而已,以前我們需要寫上一大段代碼才能實作同樣的效果。

  RoR 中,對 Active Record 模式的實作完全利用了 Ruby 語言的靈活性,簡短幾行代碼就可以定義一個關聯。并且通過複雜的 ActiveRecord:Base 對象,提供了 CRUD(建立、讀取、更新、删除)操作的預設處理。是以使用 RoR 時,絕大部分常見的資料庫操作隻需要很少量的代碼就可以完成,大大提高了開發效率。

  但 Active Record 模式也不是完美的,Active Record 存在不少缺點。

  • Active Record 模式需要資料表結構和對象屬性一一對應(至少是大部分對應),否則将難以使用 Active Record 模式;
  • Active Record 模式并不能夠真正适合完全面向對象的應用程式。因為 Active Record 模式本質上就要求一個對象必須和一個資料表對應。但在完全面向對象的應用程式中,資料和操作資料的方法很可能分布在各個不同的對象中,這些對象卻并沒有和 某一個資料表完全對應,而且 Active Record 無法很好的處理對象的繼承、聚合等面向對象常見的對象間關系;
  • 随着逐漸向 Active Record 添加業務邏輯,Active Record 對象中會混入越來越多的 SQL 語句,這在更複雜的項目中顯然是一個不利因素。

  如果在 Active Record 模式中添加了對資料關系(注意,不是對象關系)的處理,那麼還要注意性能問題:   假如一個 Active Record 對象有多個關聯。那麼我取出一個對象時,很可能就連帶取出了其他不少對象。但這些對象可能根本就是本次操作用不上的。其次,将對象更新到資料庫時,也需要對關聯的對象進行處理,否則對關聯對象的修改就會丢失。

  雖然可以用各種技巧來避免這些情況,但毫無疑問需要開發者對 RoR 的 Active Record 很熟悉才行。否則看上去很簡單的代碼,背後則會是噩夢般的資料庫操作。

  其次,假設我們要将資料庫中每本書的單價減半,那麼采用 Active Record 模式時,就必須首先讀取所有的記錄并執行個體化為對象,然後更新對象屬性,再寫回資料庫。可想而知這樣會有多差的效率。   當然了,實際開發中沒有人會這樣做。開發者會編寫一個單獨的方法,用一條 SQL 語句完成對批量資料的更新。但也說明 Active Record 模式不适合批量處理資料,而現實世界中,批量處理資料的需求随處可見。

  不過由于 RoR 對開發效率戲劇性的提高,是以對于追求開發效率的項目,RoR 是一個很不錯的選擇。而且性能上的不足可以通過更新硬體或者配合其他技術手段來改善(例如 FastCGI 通常是運作 RoR 應用的首選)。是以在現實世界中,37signals.com 公司的所有基于 RoR 開發的應用,都獲得了良好的性能表現(但是同等的硬體,跑 PHP 開發的同樣功能應用是更好還是更差呢?這個問題沒有答案)。

Active Record 與 ORM

  許多人将 Active Record 與 ORM 劃等号,這是錯誤的。ORM(對象關系映射)是将對象及對象間的關系(繼承、聚合等)映射到關系式資料庫中。由于面向對象和關系式資料庫天生的不比對,是以這種映射是相當複雜的。

  而 Active Record 原本隻是将一個資料行記錄包裝為一個對象,隻是在 RoR 中由于添加了對關系的處理,而具有了一些 ORM 的特征。是以可以簡單的将 RoR 中的 Active Record 看作 ORM 的一種實作方式。但本質上,RoR 中的 Active Record 是處理資料間的關系而不是對象間的關系(但支援對象繼承),因為每一個 Active Record 對象都是和資料表一一對應的。

  那為什麼在 Java 世界中,沒有大量采用 Active Record 模式呢?

  在 Java 世界中,絕大部分 ORM 都是作為中間件存在的。由于 Java 與 Ruby、PHP 等腳本語言截然不同的運作機制。是以即便是很複雜的中間層,隻要能夠在運作時提供良好的性能,那就能夠被開發者接受。而 Hibernate 這樣的 ORM 中間件能夠提供比 Active Record 多得多的功能和靈活性,是以 Active Record 模式在 Java 世界不受歡迎就可以了解了。

  而在 .NET 世界中,大量使用的都是表資料入口(Table Data Gateway)和表子產品(Table Module)。這兩種模式由于有 Microsoft 出色的 IDE 支援,是以能夠獲得很高的開發效率,自然 .NET 開發者對 Active Record 模式也不感興趣了。

如果将 Active Record 或者 ORM 照搬到 PHP 中呢?

  許多開發者都很羨慕 Hibernate 的強大功能和 RoR 中 Active Record 的快速開發能力,但是這些東西如果照搬到 PHP 中,會遇到一個相當大的麻煩:

  PHP 本質上是解釋執行的腳本語言,是以對于每一次 HTTP 請求,PHP 執行環境都會将請求的 .php 檔案編譯為 opcode,然後執行 opcode,再清理所有的資源(記憶體、資料庫連接配接、檔案句柄等等)。在這種環境中,應用程式應該花盡可能少的時間去初始化底層架構,而是把大部分資源用 在業務邏輯的執行上。

  但 Ruby 也是解釋執行,為什麼就可以用 Active Record,而 PHP 就不應該呢? 簡單點說就是因為 PHP 在面向對象支援上的缺陷使得要實作和 RoR 同等功能的 Active Record 模式變得非常艱難。也許你對此不以為然,那麼可以實際嘗試一下使用 PHP on Trax(一個 RoR 的 PHP 克隆)。看看一次簡單的讀取操作需要載入多少檔案并調用多少對象和方法。

  是以有些 PHP 架構提供的 Active Record 模式實作非常簡單,根本不考慮關聯問題,但這樣一來使用 Active Record 能獲得的開發效率提升就太小了。

  至于更為複雜的 ORM,目前 PHP 領域還沒有一個真正的成功項目。雖然 Propel 是目前 PHP 領域唯一一個具有實際工作能力的 ORM。但由于其自身的複雜性和執行效率問題,一直沒有得到廣泛使用。即便是 Symfony 也是對 Propel 進行裁剪後才用于處理資料庫操作。

雖然在國内 PHP 社群中常看到有人說自己做的 ORM 如何如何先進,既有進階特征,又有好的效率。但自始至終沒有看到過有人公布代碼。至于不公布的原因不外乎:還不夠成熟,成熟後再公布;我是最領先的,除非 有了同水準的,不然我不會公布;商業産品,不能洩露。而且别說是代碼,就算問問實作原理通常也隻能得到幾句無關痛癢的回答。

是以如果你看到這篇文章後,覺得你實作了我認為很難實作的東西,請拿出實際證據。不要再搬出諸如此類的理由,沒有論據的辯論是毫無意義的。

  那麼 PHP 就注定和 Active Record 和 ORM 無源嗎? 如果這個問題的潛在意思是問:PHP 就不能找到和 Active Record 一樣好用的資料庫通路方法嗎?那麼答案是否定的。

Table Data Gateway 是一個更合理的選擇

  我仔細研究了 PoEAA 中關于表資料入口、表子產品的内容後,又做了大量實際測試。最終決定在 FleaPHP 中采用 Table Data Gateway(表資料入口)模式來提供資料庫服務。并在此基礎上實作對關聯資料的自動處理。

表資料入口(Table Data Gateway):充當資料表通路入口的對象,一個執行個體處理表中所有的行。

表子產品(Table Module):處理某一資料庫表或視圖中所有行的業務邏輯的一個執行個體。

  表資料入口是封裝一個資料表的操作,而不是一個記錄行。這樣一來,表資料入口可以很友善的處理針對單個記錄和多個記錄的操作,而操作的資料就是 PHP 中的數組。實際上我初期還寫了一些對象來封裝記錄集(也就是多行記錄),不過後來發現完全是多此一舉。PHP 的數組功能非常強大,再專門用對象包裝一下弊大于利。

  針對資料表提供單純的 CRUD 操作吸引力還不夠,是以我在表資料入口的基礎上增加了對 HasOne、HasMany、ManyToMany 以及 BelongsTo 關聯的處理。這四種關聯,基本上滿足了常見的資料關聯操作。

  不過有了自動化的關聯,類似 RoR ActiveRecord 中加載過量資料的問題依然存在,是以 FleaPHP 的表資料入口對象 FLEA_Db_TableDataGateway 也提供了針對關聯的方法,讓開發者可以細粒度的控制資料庫操作。

  而且由于表資料入口是針對純資料進行操作,而不是針對包裝了資料的對象。是以開發者可以很容易的優化資料庫操作,例如無需讀取即可更新資料或者一次性處理大批量的資料。

  相對于 Active Record 模式,Table Data Gateway 模式有下列優勢:

  • 表資料入口針對一個表封裝資料庫操作,這更接近傳統 PHP 開發的思維模式;
  • 處理批量資料時,表資料入口更友善,常見操作無需額外編寫處理方法;
  • 資料以數組的形式儲存和傳遞,比将每個記錄行執行個體化為對象具有好得多的性能;
  • 實作比 Active Record 簡單,每個操作執行更少的代碼;
  • 可以很好的與表子產品(Table Module)模式配合來封裝業務邏輯。進而避免了 Active Record 中将資料庫操作和業務邏輯寫在一起的問題。

  當然,表資料入口也有相對于 Active Record 不足的地方:

  • 由于表資料入口總是傳遞純資料,是以無法像 Active Record 一樣以屬性的形式封裝對資料的操作。不過這種操作即便使用 Active Record 也要多寫不少處理代碼,而使用表資料入口時,這部分代碼隻不過是轉移到了表子產品中;
  • 看上去更沒有那麼面向對象。可惜的是即便采用 Active Record,大多數應用程式從設計思想上也不是面向對象的,隻不過用了一個對象來傳遞資料而已。

  而且 Active Record 存在的一些問題,Table Data Gateway 依然無法避免。最主要的就是表資料入口和表子產品都是和資料表一一對應,是以不适用于持久化細粒度對象。不過熟悉 .NET 的開發者應該很容易找到解決辦法,那就是以表子產品完成大部分業務操作,而細粒度對象僅用于部分操作。這是因為 Microsoft 的開發環境一向都對表資料入口和表子產品有着偏好和最好的支援。

  不過使用表資料入口,相對于 Active Record 最大的好處就是能夠很容易的将業務邏輯操作從表資料入口對象分離到表子產品對象中,是以對于更大更複雜的項目,表資料入口配合表子產品的方式具有更高的可維護性。

表資料入口和表子產品的配合

  表資料入口封裝了針對資料表的操作,而表子產品則封裝了針對資料表的業務邏輯,兩者怎麼配合呢?我們就以操作圖書記錄為例,看看具體如何做。

  首先,從 FLEA_Db_TableDataGateway 派生一個類,作為圖書表的表資料入口對象,例如 TableBooks。接下來建立一個空白的類,名為 ModuleBooks。

  1. class TableBooks extends FLEA_Db_TableDataGateway
  2. {
  3.     // 隻需要指明資料表名稱和主鍵字段名即可,CRUD 操作已經有了預設實作
  4.     var $tableName = 'books';
  5.     var $primaryKey = 'book_id';
  6. }
  7. class ModuleBooks
  8. {
  9.     var $table;
  10. }

  現在我們要統計指定年份的出版的圖書。

Step1:在 TableBooks 中增加一個方法 countBooksRange():

  1. class TableBooks extends FLEA_Db_TableDataGateway
  2. {
  3.     ......
  4.     function countBooksRange($begin, $end)
  5.     {
  6.         // 對參數進行轉義,確定不會存在 SQL 攻擊漏洞
  7.         $begin = $this->_dbo->qstr($begin);
  8.         $end = $this->_dbo->qstr($end);
  9.         return $this-&gt;findCount("publish_date &gt;= AND publish_date <= ");
  10.     }
  11. }

  countBooksRange() 方法可以統計指定區間的圖書總數,是以我們再給 ModuleBooks 增加一個 countBooksByYear() 方法來統計指定年份的圖書。

  1. class ModuleBooks
  2. {
  3.     ......
  4.     function countBooksByYear($year)
  5.     {
  6.         $begin = date("/1/1");
  7.         $end = date("/12/31");
  8.         return $this->table-&gt;countBooksRange($begin, $end);
  9.     }
  10. }

  上面的例子雖然簡單,但是很清晰的描述了表資料入口如何封裝具體的資料庫操作,而表子產品又如何利用表資料入口的方法提供更高層的接口。如果需要 可運作的示例程式,可以參考 FleaPHP 的 SHOP 示例。這個示例中,Model 目錄下就是表子產品,而 Table 目錄下就是表資料入口。