[原文地址:
http://www.dualface.com/blog/?p=357
]
許多開(kāi)發(fā)者很疑惑為什么 FleaPHP 以高效開(kāi)發(fā)為目標(biāo),卻沒(méi)有提供 Active Record 模式。本文嘗試詳細(xì)闡述這個(gè)問(wèn)題。
Active Record 是什么?
Active Record 模式中文名為“活動(dòng)記錄”,在《企業(yè)應(yīng)用架構(gòu)模式》(PoEAA)一書(shū)中定義如下:
活動(dòng)記錄(Active Record):一個(gè)對(duì)象,它包裝數(shù)據(jù)庫(kù)表或視圖中的某一行,封裝數(shù)據(jù)庫(kù)訪問(wèn),并在這些數(shù)據(jù)上增加了領(lǐng)域邏輯。
舉個(gè)例子來(lái)說(shuō),一個(gè)圖書(shū)數(shù)據(jù)表,每一條記錄就是一本圖書(shū)的信息。那么采用 Active Record 時(shí),每一本圖書(shū)就是一個(gè) Active Record 對(duì)象實(shí)例。
Active Record 因 Ruby On Rails 而流行
Active Record 之所以現(xiàn)在這么炙手可熱,甚至許多人將 Active Record 和 ORM 劃等號(hào),完全是 Ruby On Rails 的原因。
在 Ruby On Rails 中,Active Record 除了最基本的將數(shù)據(jù)記錄和一個(gè)對(duì)象互相映射外,還提供了數(shù)據(jù)(而不是對(duì)象)間關(guān)聯(lián)關(guān)系的處理 。例如:
一本圖書(shū)有一個(gè)或者多個(gè)作者,所以每一個(gè)圖書(shū)對(duì)象都和多個(gè)作者對(duì)象關(guān)聯(lián)。反過(guò)來(lái)一個(gè)作者可以寫(xiě)多本書(shū),所以一個(gè)作者對(duì)象也和多個(gè)圖書(shū)對(duì)象關(guān)聯(lián)。
在 RoR 中,我們獲取一個(gè)圖書(shū)對(duì)象時(shí),自動(dòng)就獲得了該圖書(shū)對(duì)象所對(duì)應(yīng)的作者對(duì)象(本質(zhì)上是圖書(shū)數(shù)據(jù)對(duì)應(yīng)的作者數(shù)據(jù))。更進(jìn)一步,通過(guò)圖書(shū)對(duì)象關(guān)聯(lián)的作者對(duì)象,我們 可以獲取該作者所寫(xiě)的所有圖書(shū)的對(duì)象實(shí)例。而這些工作,在 RoR 中只需要幾行代碼而已,以前我們需要寫(xiě)上一大段代碼才能實(shí)現(xiàn)同樣的效果。
RoR 中,對(duì) Active Record 模式的實(shí)現(xiàn)完全利用了 Ruby 語(yǔ)言的靈活性,簡(jiǎn)短幾行代碼就可以定義一個(gè)關(guān)聯(lián)。并且通過(guò)復(fù)雜的 ActiveRecord:Base 對(duì)象,提供了 CRUD(創(chuàng)建、讀取、更新、刪除)操作的默認(rèn)處理。所以使用 RoR 時(shí),絕大部分常見(jiàn)的數(shù)據(jù)庫(kù)操作只需要很少量的代碼就可以完成,大大提高了開(kāi)發(fā)效率。
但 Active Record 模式也不是完美的,Active Record 存在不少缺點(diǎn)。
- Active Record 模式需要數(shù)據(jù)表結(jié)構(gòu)和對(duì)象屬性一一對(duì)應(yīng)(至少是大部分對(duì)應(yīng)),否則將難以使用 Active Record 模式;
- Active Record 模式并不能夠真正適合完全面向?qū)ο蟮膽?yīng)用程序。因?yàn)?Active Record 模式本質(zhì)上就要求一個(gè)對(duì)象必須和一個(gè)數(shù)據(jù)表對(duì)應(yīng)。但在完全面向?qū)ο蟮膽?yīng)用程序中,數(shù)據(jù)和操作數(shù)據(jù)的方法很可能分布在各個(gè)不同的對(duì)象中,這些對(duì)象卻并沒(méi)有和 某一個(gè)數(shù)據(jù)表完全對(duì)應(yīng),而且 Active Record 無(wú)法很好的處理對(duì)象的繼承、聚合等面向?qū)ο蟪R?jiàn)的對(duì)象間關(guān)系;
- 隨著逐漸向 Active Record 添加業(yè)務(wù)邏輯,Active Record 對(duì)象中會(huì)混入越來(lái)越多的 SQL 語(yǔ)句,這在更復(fù)雜的項(xiàng)目中顯然是一個(gè)不利因素。
如果在 Active Record 模式中添加了對(duì)數(shù)據(jù)關(guān)系(注意,不是對(duì)象關(guān)系)的處理,那么還要注意性能問(wèn)題:
假如一個(gè) Active Record 對(duì)象有多個(gè)關(guān)聯(lián)。那么我取出一個(gè)對(duì)象時(shí),很可能就連帶取出了其他不少對(duì)象。但這些對(duì)象可能根本就是本次操作用不上的。其次,將對(duì)象更新到數(shù)據(jù)庫(kù)時(shí),也需要對(duì)關(guān)聯(lián)的對(duì)象進(jìn)行處理,否則對(duì)關(guān)聯(lián)對(duì)象的修改就會(huì)丟失。
雖然可以用各種技巧來(lái)避免這些情況,但毫無(wú)疑問(wèn)需要開(kāi)發(fā)者對(duì) RoR 的 Active Record 很熟悉才行。否則看上去很簡(jiǎn)單的代碼,背后則會(huì)是噩夢(mèng)般的數(shù)據(jù)庫(kù)操作。
其次,假設(shè)我們要將數(shù)據(jù)庫(kù)中每本書(shū)的單價(jià)減半,那么采用 Active Record 模式時(shí),就必須首先讀取所有的記錄并實(shí)例化為對(duì)象,然后更新對(duì)象屬性,再寫(xiě)回?cái)?shù)據(jù)庫(kù)。可想而知這樣會(huì)有多差的效率。
當(dāng)然了,實(shí)際開(kāi)發(fā)中沒(méi)有人會(huì)這樣做。開(kāi)發(fā)者會(huì)編寫(xiě)一個(gè)單獨(dú)的方法,用一條 SQL 語(yǔ)句完成對(duì)批量數(shù)據(jù)的更新。但也說(shuō)明 Active Record 模式不適合批量處理數(shù)據(jù),而現(xiàn)實(shí)世界中,批量處理數(shù)據(jù)的需求隨處可見(jiàn)。
不過(guò)由于 RoR 對(duì)開(kāi)發(fā)效率戲劇性的提高,所以對(duì)于追求開(kāi)發(fā)效率的項(xiàng)目,RoR 是一個(gè)很不錯(cuò)的選擇。而且性能上的不足可以通過(guò)更新硬件或者配合其他技術(shù)手段來(lái)改善(例如 FastCGI 通常是運(yùn)行 RoR 應(yīng)用的首選)。因此在現(xiàn)實(shí)世界中, 37signals.com 公司的所有基于 RoR 開(kāi)發(fā)的應(yīng)用,都獲得了良好的性能表現(xiàn)(但是同等的硬件,跑 PHP 開(kāi)發(fā)的同樣功能應(yīng)用是更好還是更差呢?這個(gè)問(wèn)題沒(méi)有答案)。
Active Record 與 ORM
許多人將 Active Record 與 ORM 劃等號(hào),這是錯(cuò)誤的。ORM(對(duì)象關(guān)系映射)是將對(duì)象及對(duì)象間的關(guān)系(繼承、聚合等)映射到關(guān)系式數(shù)據(jù)庫(kù)中。由于面向?qū)ο蠛完P(guān)系式數(shù)據(jù)庫(kù)天生的不匹配,所以這種映射是相當(dāng)復(fù)雜的。
而 Active Record 原本只是將一個(gè)數(shù)據(jù)行記錄包裝為一個(gè)對(duì)象,只是在 RoR 中由于添加了對(duì)關(guān)系的處理,而具有了一些 ORM 的特征。所以可以簡(jiǎn)單的將 RoR 中的 Active Record 看作 ORM 的一種實(shí)現(xiàn)方式。但本質(zhì)上,RoR 中的 Active Record 是處理數(shù)據(jù)間的關(guān)系而不是對(duì)象間的關(guān)系(但支持對(duì)象繼承),因?yàn)槊恳粋€(gè) Active Record 對(duì)象都是和數(shù)據(jù)表一一對(duì)應(yīng)的。
那為什么在 Java 世界中,沒(méi)有大量采用 Active Record 模式呢?
在 Java 世界中,絕大部分 ORM 都是作為中間件存在的。由于 Java 與 Ruby、PHP 等腳本語(yǔ)言截然不同的運(yùn)行機(jī)制。所以即便是很復(fù)雜的中間層,只要能夠在運(yùn)行時(shí)提供良好的性能,那就能夠被開(kāi)發(fā)者接受。而 Hibernate 這樣的 ORM 中間件能夠提供比 Active Record 多得多的功能和靈活性,所以 Active Record 模式在 Java 世界不受歡迎就可以理解了。
而在 .NET 世界中,大量使用的都是表數(shù)據(jù)入口(Table Data Gateway)和表模塊(Table Module)。這兩種模式由于有 Microsoft 出色的 IDE 支持,所以能夠獲得很高的開(kāi)發(fā)效率,自然 .NET 開(kāi)發(fā)者對(duì) Active Record 模式也不感興趣了。
如果將 Active Record 或者 ORM 照搬到 PHP 中呢?
許多開(kāi)發(fā)者都很羨慕 Hibernate 的強(qiáng)大功能和 RoR 中 Active Record 的快速開(kāi)發(fā)能力,但是這些東西如果照搬到 PHP 中,會(huì)遇到一個(gè)相當(dāng)大的麻煩:
PHP 本質(zhì)上是解釋執(zhí)行的腳本語(yǔ)言,所以對(duì)于每一次 HTTP 請(qǐng)求,PHP 執(zhí)行環(huán)境都會(huì)將請(qǐng)求的 .php 文件編譯為 opcode,然后執(zhí)行 opcode,再清理所有的資源(內(nèi)存、數(shù)據(jù)庫(kù)連接、文件句柄等等)。在這種環(huán)境中,應(yīng)用程序應(yīng)該花盡可能少的時(shí)間去初始化底層框架,而是把大部分資源用 在業(yè)務(wù)邏輯的執(zhí)行上。
但 Ruby 也是解釋執(zhí)行,為什么就可以用 Active Record,而 PHP 就不應(yīng)該呢?
簡(jiǎn)單點(diǎn)說(shuō)就是因?yàn)?PHP 在面向?qū)ο笾С稚系娜毕菔沟靡獙?shí)現(xiàn)和 RoR
同等功能
的 Active Record 模式變得非常艱難。也許你對(duì)此不以為然,那么可以實(shí)際嘗試一下使用 PHP on Trax(一個(gè) RoR 的 PHP 克隆)。看看一次簡(jiǎn)單的讀取操作需要載入多少文件并調(diào)用多少對(duì)象和方法。
所以有些 PHP 框架提供的 Active Record 模式實(shí)現(xiàn)非常簡(jiǎn)單,根本不考慮關(guān)聯(lián)問(wèn)題,但這樣一來(lái)使用 Active Record 能獲得的開(kāi)發(fā)效率提升就太小了。
至于更為復(fù)雜的 ORM,目前 PHP 領(lǐng)域還沒(méi)有一個(gè)真正的成功項(xiàng)目。雖然 Propel 是目前 PHP 領(lǐng)域唯一一個(gè)具有實(shí)際工作能力的 ORM。但由于其自身的復(fù)雜性和執(zhí)行效率問(wèn)題,一直沒(méi)有得到廣泛使用。即便是 Symfony 也是對(duì) Propel 進(jìn)行裁剪后才用于處理數(shù)據(jù)庫(kù)操作。
雖然在國(guó)內(nèi) PHP 社區(qū)中常看到有人說(shuō)自己做的 ORM 如何如何先進(jìn),既有高級(jí)特征,又有好的效率。但自始至終沒(méi)有看到過(guò)有人公布代碼。至于不公布的原因不外乎:還不夠成熟,成熟后再公布;我是最領(lǐng)先的,除非 有了同水平的,不然我不會(huì)公布;商業(yè)產(chǎn)品,不能泄露。而且別說(shuō)是代碼,就算問(wèn)問(wèn)實(shí)現(xiàn)原理通常也只能得到幾句無(wú)關(guān)痛癢的回答。
所以如果你看到這篇文章后,覺(jué)得你實(shí)現(xiàn)了我認(rèn)為很難實(shí)現(xiàn)的東西,請(qǐng)拿出實(shí)際證據(jù)。不要再搬出諸如此類(lèi)的理由,沒(méi)有論據(jù)的辯論是毫無(wú)意義的。
那么 PHP 就注定和 Active Record 和 ORM 無(wú)源嗎?
如果這個(gè)問(wèn)題的潛在意思是問(wèn):PHP 就不能找到和 Active Record 一樣好用的數(shù)據(jù)庫(kù)訪問(wèn)方法嗎?那么答案是否定的。
Table Data Gateway 是一個(gè)更合理的選擇
我仔細(xì)研究了 PoEAA 中關(guān)于表數(shù)據(jù)入口、表模塊的內(nèi)容后,又做了大量實(shí)際測(cè)試。最終決定在 FleaPHP 中采用 Table Data Gateway(表數(shù)據(jù)入口)模式來(lái)提供數(shù)據(jù)庫(kù)服務(wù)。并在此基礎(chǔ)上實(shí)現(xiàn)對(duì)關(guān)聯(lián)數(shù)據(jù)的自動(dòng)處理。
表數(shù)據(jù)入口(Table Data Gateway):充當(dāng)數(shù)據(jù)表訪問(wèn)入口的對(duì)象,一個(gè)實(shí)例處理表中所有的行。
表模塊(Table Module):處理某一數(shù)據(jù)庫(kù)表或視圖中所有行的業(yè)務(wù)邏輯的一個(gè)實(shí)例。
表數(shù)據(jù)入口是封裝一個(gè)數(shù)據(jù)表的操作,而不是一個(gè)記錄行。這樣一來(lái),表數(shù)據(jù)入口可以很方便的處理針對(duì)單個(gè)記錄和多個(gè)記錄的操作,而操作的數(shù)據(jù)就是 PHP 中的數(shù)組。實(shí)際上我初期還寫(xiě)了一些對(duì)象來(lái)封裝記錄集(也就是多行記錄),不過(guò)后來(lái)發(fā)現(xiàn)完全是多此一舉。PHP 的數(shù)組功能非常強(qiáng)大,再專(zhuān)門(mén)用對(duì)象包裝一下弊大于利。
針對(duì)數(shù)據(jù)表提供單純的 CRUD 操作吸引力還不夠,所以我在表數(shù)據(jù)入口的基礎(chǔ)上增加了對(duì) HasOne、HasMany、ManyToMany 以及 BelongsTo 關(guān)聯(lián)的處理。這四種關(guān)聯(lián),基本上滿(mǎn)足了常見(jiàn)的數(shù)據(jù)關(guān)聯(lián)操作。
不過(guò)有了自動(dòng)化的關(guān)聯(lián),類(lèi)似 RoR ActiveRecord 中加載過(guò)量數(shù)據(jù)的問(wèn)題依然存在,所以 FleaPHP 的表數(shù)據(jù)入口對(duì)象 FLEA_Db_TableDataGateway 也提供了針對(duì)關(guān)聯(lián)的方法,讓開(kāi)發(fā)者可以細(xì)粒度的控制數(shù)據(jù)庫(kù)操作。
而且由于表數(shù)據(jù)入口是針對(duì)純數(shù)據(jù)進(jìn)行操作,而不是針對(duì)包裝了數(shù)據(jù)的對(duì)象。所以開(kāi)發(fā)者可以很容易的優(yōu)化數(shù)據(jù)庫(kù)操作,例如無(wú)需讀取即可更新數(shù)據(jù)或者一次性處理大批量的數(shù)據(jù)。
相對(duì)于 Active Record 模式,Table Data Gateway 模式有下列優(yōu)勢(shì):
- 表數(shù)據(jù)入口針對(duì)一個(gè)表封裝數(shù)據(jù)庫(kù)操作,這更接近傳統(tǒng) PHP 開(kāi)發(fā)的思維模式;
- 處理批量數(shù)據(jù)時(shí),表數(shù)據(jù)入口更方便,常見(jiàn)操作無(wú)需額外編寫(xiě)處理方法;
- 數(shù)據(jù)以數(shù)組的形式保存和傳遞,比將每個(gè)記錄行實(shí)例化為對(duì)象具有好得多的性能;
- 實(shí)現(xiàn)比 Active Record 簡(jiǎn)單,每個(gè)操作執(zhí)行更少的代碼;
- 可以很好的與表模塊(Table Module)模式配合來(lái)封裝業(yè)務(wù)邏輯。從而避免了 Active Record 中將數(shù)據(jù)庫(kù)操作和業(yè)務(wù)邏輯寫(xiě)在一起的問(wèn)題。
當(dāng)然,表數(shù)據(jù)入口也有相對(duì)于 Active Record 不足的地方:
- 由于表數(shù)據(jù)入口總是傳遞純數(shù)據(jù),所以無(wú)法像 Active Record 一樣以屬性的形式封裝對(duì)數(shù)據(jù)的操作。不過(guò)這種操作即便使用 Active Record 也要多寫(xiě)不少處理代碼,而使用表數(shù)據(jù)入口時(shí),這部分代碼只不過(guò)是轉(zhuǎn)移到了表模塊中;
- 看上去更沒(méi)有那么面向?qū)ο蟆?上У氖羌幢悴捎?Active Record,大多數(shù)應(yīng)用程序從設(shè)計(jì)思想上也不是面向?qū)ο蟮模徊贿^(guò)用了一個(gè)對(duì)象來(lái)傳遞數(shù)據(jù)而已。
而且 Active Record 存在的一些問(wèn)題,Table Data Gateway 依然無(wú)法避免。最主要的就是表數(shù)據(jù)入口和表模塊都是和數(shù)據(jù)表一一對(duì)應(yīng),因此不適用于持久化細(xì)粒度對(duì)象。不過(guò)熟悉 .NET 的開(kāi)發(fā)者應(yīng)該很容易找到解決辦法,那就是以表模塊完成大部分業(yè)務(wù)操作,而細(xì)粒度對(duì)象僅用于部分操作。這是因?yàn)?Microsoft 的開(kāi)發(fā)環(huán)境一向都對(duì)表數(shù)據(jù)入口和表模塊有著偏好和最好的支持。
不過(guò)使用表數(shù)據(jù)入口,相對(duì)于 Active Record 最大的好處就是能夠很容易的將業(yè)務(wù)邏輯操作從表數(shù)據(jù)入口對(duì)象分離到表模塊對(duì)象中,因此對(duì)于更大更復(fù)雜的項(xiàng)目,表數(shù)據(jù)入口配合表模塊的方式具有更高的可維護(hù)性。
表數(shù)據(jù)入口和表模塊的配合
表數(shù)據(jù)入口封裝了針對(duì)數(shù)據(jù)表的操作,而表模塊則封裝了針對(duì)數(shù)據(jù)表的業(yè)務(wù)邏輯,兩者怎么配合呢?我們就以操作圖書(shū)記錄為例,看看具體如何做。
首先,從 FLEA_Db_TableDataGateway 派生一個(gè)類(lèi),作為圖書(shū)表的表數(shù)據(jù)入口對(duì)象,例如 TableBooks。接下來(lái)建立一個(gè)空白的類(lèi),名為 ModuleBooks。
- class ? TableBooks extends FLEA_Db_TableDataGateway
- {
- ? ? // 只需要指明數(shù)據(jù)表名稱(chēng)和主鍵字段名即可,CRUD 操作已經(jīng)有了默認(rèn)實(shí)現(xiàn)
- ? ? var ? $tableName = ' books ' ;
- ? ? var ? $primaryKey = ' book_id ' ;
- }
- ?
- class ? ModuleBooks
- {
- ? ? var ? $table ;
- }
現(xiàn)在我們要統(tǒng)計(jì)指定年份的出版的圖書(shū)。
Step1: 在 TableBooks 中增加一個(gè)方法 countBooksRange():
- class ? TableBooks extends FLEA_Db_TableDataGateway
- {
- ? ? ......
- ? ?
- ? ? /**
- ? ?? * 統(tǒng)計(jì)指定時(shí)間區(qū)間的圖書(shū)總數(shù)
- ? ?? */
- ? ? function ? countBooksRange ( $begin , $end )
- ? ? {
- ? ? ? ? // 對(duì)參數(shù)進(jìn)行轉(zhuǎn)義,確保不會(huì)存在 SQL 攻擊漏洞
- ? ? ? ? $begin = $this -> _dbo -> qstr ( $begin ) ;
- ? ? ? ? $end = $this -> _dbo -> qstr ( $end ) ;
- ? ? ? ? return ? $this -> findCount ( " publish_date >= {$begin} AND publish_date <= {$end} " ) ;
- ? ? }
- }
countBooksRange() 方法可以統(tǒng)計(jì)指定區(qū)間的圖書(shū)總數(shù),所以我們?cè)俳o ModuleBooks 增加一個(gè) countBooksByYear() 方法來(lái)統(tǒng)計(jì)指定年份的圖書(shū)。
- class ? ModuleBooks
- {
- ? ? ......
- ? ?
- ? ? function ? countBooksByYear ( $year )
- ? ? {
- ? ? ? ? $begin = date ( " {$year} /1/1 " ) ;
- ? ? ? ? $end = date ( " {$year} /12/31 " ) ;
- ? ? ? ? return ? $this -> table -> countBooksRange ( $begin , $end ) ;
- ? ? }
- }
上面的例子雖然簡(jiǎn)單,但是很清晰的描述了表數(shù)據(jù)入口如何封裝具體的數(shù)據(jù)庫(kù)操作,而表模塊又如何利用表數(shù)據(jù)入口的方法提供更高層的接口。如果需要 可運(yùn)行的示例程序,可以參考 FleaPHP 的 SHOP 示例。這個(gè)示例中,Model 目錄下就是表模塊,而 Table 目錄下就是表數(shù)據(jù)入口。
Trackback: http://tb.blog.csdn.net/TrackBack.aspx?PostId=1442087
為什么 FleaPHP 使用 Table Data Gateway 代替 Active Record 來(lái)提供數(shù)據(jù)庫(kù)訪問(wèn)服務(wù)
更多文章、技術(shù)交流、商務(wù)合作、聯(lián)系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號(hào)聯(lián)系: 360901061
您的支持是博主寫(xiě)作最大的動(dòng)力,如果您喜歡我的文章,感覺(jué)我的文章對(duì)您有幫助,請(qǐng)用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點(diǎn)擊下面給點(diǎn)支持吧,站長(zhǎng)非常感激您!手機(jī)微信長(zhǎng)按不能支付解決辦法:請(qǐng)將微信支付二維碼保存到相冊(cè),切換到微信,然后點(diǎn)擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對(duì)您有幫助就好】元
