深入理解Magento
作者:Alan Storm
翻譯:Hailong Zhang
第四章 – 模型和ORM基礎
對於任何一個MVC架構,模型(Model)層的實現都是佔據了很大一部分。對於Magento來說,模型佔據了一個更加重要的位置,因爲它常常包含了一部分商業邏輯代碼(可以說它對,也可以說它錯)。這些代碼在其他的MVC框架中往往出現在控制器或者幫助函數中。
傳統的PHP MVC架構中的模型
本來MVC的定義就不是很清晰,不同的人有不同的看法,而對於模型的定義爭議就更多了。在MVC模式被廣泛採用之前,PHP程序員往往通過SQL語 句直接操作數據庫。也有些程序員通過一個SQL抽象層來操作數據庫(比如AdoDB)。程序員往往關注SQL語句本身,而不是和數據相關的對象。
雖然直接操作SQL的方式一直被病詬,但是很多PHP框架還是以SQL爲中心的。模型層提供了一系列對象,抽象/封裝了數據操作,但是程序員最終還是需爲模型層對象寫SQL語句操作數據庫。
還有一些框架回避了SQL,使用了對象關係映射(Object Relational Mapping,ORM)來解決這個問題。使用這個方法的話,程序員不用關注SQL,而只需要和對象打交道。我們可以操作一個對象的屬性,當“Save” 方法被調用的時候,對象的屬性會作爲數據自動的被寫入數據庫。有些ORM框架會根據數據表的信息自動推測對象的屬性,也有框架要求用戶顯示的生命對象屬性 和表的關係。比較有名的ORM框架有ActiveRecord等等。【譯者注:ActiveRecord源自Ruby on Rails,不過現在PHP也有了】
關於ORM的概念,我就解釋到這裏。但是和許多計算機領域的其他概念一樣,ORM的定義也越來越模糊了。我不想在這片文章中討論關於ORM的爭議,所以我說的ORM就是那個最基本的ORM概念。
Magento的模型
Magento理所當然的也追隨潮流應用了ORM。雖然Magento自帶的Zend框架提供了SQL抽象層,但是在大多數情況下我們將通過 Magento自帶的模型和我們自己的模型來進行數據訪問。他和視圖層(View)一樣,Magento的模型層也不是簡單的ORM,而是一個高度靈活, 高度抽象甚至有點令人費解。
解剖Magento的模型
大部分的Magento模型分爲兩類。第一類是基本的ActiveRecord類型,一張表一個對象的模型。第二類是Entity Attribute Value(EAV)模型。【譯者注:EAV翻譯成“實體屬性值”有點詞不達意,還是就叫EAV的好】Magento自己定義了一個數據類型叫做模型集合 (Model Collection)。顧名思義,模型集合就是一個對象裏面包含了很多模型對象。Magento的創造者Varien團隊實現了PHP類庫的標準接 口,“IteratorAggregate”,“Countable”。這樣模型集合就能調用這些方法,這也是模型集合和數組的區別。
Magento的模型並不直接訪問數據庫。每一個模型都有一個資源模型(Resource Model),每一個資源模型擁有兩個適配器(Adapter),一個讀,一個寫。這樣的話邏輯模型和數據庫訪問就分開了,所以從理論上講更改底層數據庫 只需要重寫適配器就可以了,所有上層代碼都不需要更改。
創建一個基本模型
【譯者注:從這一章開始我用我自己的例子替換了Alan的例子】繼續我們Hello World的例子。在Hello World模塊中創建BlogController.php如下
- class Zhlmmc_Helloworld_BlogController extends Mage_Core_Controller_Front_Action {
- public function indexAction()
- {
- echo 'Hello Blog';
- }
- }
訪問以下URLhttp://127.0.0.1/Magento/helloworld/blog
你應該看到“Hello Blog”輸出。
創建數據表
我們可以通過Magento自帶的方法創建或者修改數據庫,但是爲了不引入過多新內容,我們暫且手工創建一張表。在你的數據庫中執行以下語句
- CREATE TABLE `blog_posts` (
- `blogpost_id` int(11) NOT NULL auto_increment,
- `title` text,
- `post` text,
- `date` datetime default NULL,
- `timestamp` timestamp NOT NULL default CURRENT_TIMESTAMP,
- PRIMARY KEY (`blogpost_id`)
- );
- INSERT INTO `blog_posts` VALUES (1,'My New Title','This is a blog post','2009-07-01 00:00:00','2009-07-02 23:12:30');
這裏我們創建了一張名爲“blog_posts”的表,並填充了一條數據。
創建模型
要設置一個模型一共有以下四個步驟
- 啓用模型
- 啓用資源模型
- 在資源模型中添加實體(Entity)。對於簡單的模型來說,實體就是數據表的名字
- 爲資源模型設置讀、寫適配器
在進行這些步驟之前,我們先來看假設這些步驟已經做完了,我們怎麼用一個模型。在Magento中,我們用以下的方式來實例化一個模型
- $model = Mage::getModel('helloworld/blogpost');
和我們以前講過的“Mage::getHelper()”的原理類似,這裏Magento也是通過全局配置去查找模型的類名。模型的類名和我們以前講過的 塊類名一樣,都是分組類名。這裏參數的前半部分“helloworld”是組名(Group Name),後半部分“blogpost”是半類名(Class Name)【譯者注:我將“Class Name”翻譯成半類名是爲了和類名區分開來】。具體步驟如下
- 從全局配置“/global/models/GROUP_NAME/class”獲得基本類名“Zhlmmc_Helloworld_Model”
- 檢查全局配置“/global/models/GROUP_NAME/rewrite/CLASS_NAME”是否設置,如果有那麼這個節點的值將被作爲類名實例化
- 否則,最終的類名將是基本類名加上半類名,也就是“Zhlmmc_Helloworld_Model_Blogpost”
啓用模型
修改模塊的config.xml
- <global>
- <!-- ... -->
- <models>
- <helloworld>
- <class>Zhlmmc_Helloworld_Model</class>
- <!--
- need to create our own resource, can't just
- use core_mysql4
- -->
- <resourceModel>helloworld_mysql4</resourceModel>
- </helloworld>
- </models>
- <!-- ... -->
- </global>
標籤<helloworld />就是組名,也應該和模塊名一致。<class />標籤的內容是基本類名,所有Helloworld模塊的模型都用這個基本類名,命名方式如下Packagename_Modulename_Model
<resourceModel />標籤指明瞭這個模塊的模型要用哪個資源模型。這個標籤的內容是組名加上“mysql4”我們將在後面詳細介紹資源模型。
現在讓我們來實例化一個模型看看,修改indexAction方法
- public function indexAction() {
- $blogpost = Mage::getModel('helloworld/blogpost');
- echo get_class($blogpost);
- }
清空Magento緩存,刷新頁面,你應該看到一個類似這樣的異常(請先打開Magento的開發模式 )
- include(Zhlmmc\Helloworld\Model\Blogpost.php) [<a href='function.include'>function.include</a>]: failed to open stream: No such file or directory
原因很簡單,就是Magento嘗試去實例化“Zhlmmc_Helloworld_Model_Blogpost”,但是它在Helloworld模塊的文件夾裏面找不到這個類。所以我們現在來創建這個類File: app/code/local/Zhlmmc/Helloworld/Model/Blogpost.php
- class Zhlmmc_Helloworld_Model_Blogpost extends Mage_Core_Model_Abstract
- {
- protected function _construct()
- {
- $this->_init('helloworld/blogpost');
- }
- }
刷新頁面,你應該看到頁面上顯示“Zhlmmc_Helloworld_Model_Blogpost”。所有的模型都必須繼承 “Mage_Core_Model_Abstract”類。這個抽象類強制你實現一個方法“_construct”(注意:這個不是PHP的構造行數 “__construct”)。這個方法應該調用父類已經定義好的“_init”方法,參數是資源模型的URI,也就是我們要告訴模型使用哪個資源模型。 我們將在解釋資源模型的時候再解釋這個URI。
啓用資源模型並添加實體
好了,我們設置好了模型,下面我們要爲模型設置資源模型。資源模型纔是真正和數據庫對話的組件。在模型的配置中,有一段這樣的代碼
- <resourceModel>helloworld_mysql4</resourceModel>
<resourceModel />的值將被用來實例化資源模型。我們不需要顯式的調用資源模型,但是當一個模型需要訪問數據庫的時候,Magento會自動實例化一個資源模型來使用。
- Mage::getResourceModel('helloworld/blogpost');
這裏“helloworld/blogpost”就是我們給模型的“_init”傳入的參數。“helloworld”是組名,“blogpost”是模 型的半類名。“Mage::getResourceModel”方法將以“helloworld/blogpost”爲URI在全局配置中找 到<resourceModel>標籤的值,在這裏是“helloworld_mysql4”。然後Magento會用 URI“helloworld_mysql4/blogpost”去實例化資源模型類。實例化的過程和我們前面講的模型的實例化是一樣的,所以我們也需要
在config.xml中添加資源模型的聲明
- <global>
- <!-- ... -->
- <models>
- <!-- ... -->
- <helloworld_mysql4>
- <class>Zhlmmc_Helloworld_Model_Resource_Mysql4</class>
- </helloworld_mysql4>
- </models>
- </global>
這裏我們可以看到,資源模型的聲明也是放在<models />下面的。有點搞,但是也不必深究了,Magento就這麼定義的。<class />標籤的值是所有資源模型類的基本類名,命名方式如下Packagename_Modulename_Model_Resource_Mysql4
好了,我們已經配置了資源模型,我們來試試裝載一些數據。修改indexAction如下
- public function indexAction() {
- $params = $this->getRequest()->getParams();
- $blogpost = Mage::getModel('helloworld/blogpost');
- echo("Loading the blogpost with an ID of ".$params['id']."<br/>");
- $blogpost->load($params['id']);
- $data = $blogpost->getData();
- var_dump($data);
- }
清空Magento緩存,訪問下面的頁面http://127.0.0.1/Magento/helloworld/blog/index/id/1
你應該看到一個類似下面這樣的異常
- include(Zhlmmc\Helloworld\Model\Resource\Mysql4\Blogpost.php) [function.include]: failed to open stream: No such file or directory
我想你看到這裏也明白了,我們要爲模型添加一個資源類,添加如下文件File: app/code/local/Zhlmmc/Helloworld/Model/Resource/Mysql4/Blogpost.php
- class Zhlmmc_Helloworld_Model_Resource_Mysql4_Blogpost extends Mage_Core_Model_Mysql4_Abstract{
- protected function _construct()
- {
- $this->_init('helloworld/blogpost', 'blogpost_id');
- }
- }
這裏“_init”方法的第一個參數這個資源模型將要使用的數據表的URI,第二個參數是數據表中的列名。這個列的內容必須唯一,往往是數據表的主鍵。
爲資源模型添加實體
刷新頁面,你是不是得到下面的異常?
- Can't retrieve entity config: helloworld/blogpost
那是因爲我們的資源文件現在還是一個空殼,並沒有和數據庫聯繫起來。現在我們來把資源模型和我們的表聯繫起來,修改config.xml如下
- <global>
- <!-- ... -->
- <models>
- <!-- ... -->
- <helloworld_mysql4>
- <class>Zhlmmc_Helloworld_Model_Resource_Mysql4</class>
- <entities>
- <blogpost>
- <table>blog_posts</table>
- </blogpost>
- </entities>
- </helloworld_mysql4>
- </models>
- </global>
我們前面設置了資源模型使用的數據表的URI是“helloworld/blogpost”,那麼Magento會把“helloworld”作爲組 名,“blogpost”作爲實體名,也就是<blogpost>。在Magento的簡單模型中(也就是繼承 Mage_Core_Model_Mysql4_Abstract的模型),一個實體對應一張數據表。我們的數據表是“blog_posts”,所以這 裏<table />標籤的內容就是“blog_posts”。
清空Magento緩存,再次刷新頁面,你應該看到以下內容
- Loading the blogpost with an ID of 1
- array(5) { ["blogpost_id"]=> string(1) "1" ["title"]=> string(12) "My New Title" ["post"]=> string(19) "This is a blog post" ["date"]=> string(19) "2009-07-01 00:00:00" ["timestamp"]=> string(19) "2009-07-02 23:12:30" }
設置讀寫適配器
在上面的例子中,我們已經可以從數據庫中取數據了,但是我們卻沒有爲資源模型設置讀寫適配器,怎麼回事呢?原因很簡單,那就是因爲Magento會爲沒有適配器的資源模型啓用默認適配器。我們也可以顯式的配置默認的適配器
- <global>
- <!-- ... -->
- <resources>
- <helloworld_write>
- <connection>
- <use>default_write</use>
- </connection>
- </helloworld_write>
- <helloworld_read>
- <connection>
- <use>default_read</use>
- </connection>
- </helloworld_read>
- </resources>
- </global>
在<resources />標籤下面有兩個部分,一個讀,一個寫。標籤名字中的“hellworld”是我們定義的組名【譯者注:在資源模型的“_init”函數中傳入的 數據表的URI “helloworld/blogpost”的前半部分就是適配器名字的前半部分】。從這裏我們也可以看出來一個資源組對應一對適配器。清空 Magento緩存,刷新瀏覽器,你應該看到和剛纔相同的頁面。【譯者注:如果你去全局配置中找“core_read”你會發現
“default_read”,然後是“default_setup”
- <default_setup>
- <connection>
- <model>mysql4</model>
- <initStatements>SET NAMES utf8</initStatements>
- <type>pdo_mysql</type>
- <host>localhost</host>
- <username>root</username>
- <password>admin</password>
- <dbname>zend-magento</dbname>
- <active>1</active>
- </connection>
- </default_setup>
這纔是最終和數據庫連接的詳細信息。如果你再往下深究,你會發現全局配置有這麼一段
- <resource>
- <connection>
- <types>
- <pdo_mysql>
- <class>Mage_Core_Model_Resource_Type_Db_Pdo_Mysql</class>
- </pdo_mysql>
- </types>
- </connection>
- </resource>
所以,“Mage_Core_Model_Resource_Type_Db_Pdo_Mysql”纔是最終連接數據庫的類。如果我們更換數據庫的話,我們要重寫一個相似的類來連接別的數據庫。
基本模型操作
所有的模型最終都繼承自類“Varien_Object”。這個類屬於Magento的系統類庫,不屬於Magento的核心模塊。你可以在以下位置找到這個類lib/Varien/Object.php
Magento模型的數據保存在“_data”屬性中,這個屬性是“protected”修飾的。父類“Varian_Object”定義了一些函數用來 取出這些數據。我們上面的例子用了“getData”,這個方法返回一個數組,數組的元素是“key/value”對。【譯者注:其實就是數據表中一行的 數據,“key”就是列名,“value”就是值】我們可以傳入一個參數獲取某個具體的“key”的值。
- $model->getData();
- $model->getData('title');
還有一個方法是“getOrigData”,這個方法會返回模型第一次被賦予的值。【譯者注:因爲模型在初始化以後,值可以被修改,這個方法就是拿到那個最原始的值】
- $model->getOrigData();
- $model->getOrigData('title');
“Varien_Object”也實現了一些PHP的特殊函數,比如神奇的“__call”。你可以對任何一個屬性調用“get, set, unset, has”方法
- $model->getBlogpostId();
- $model->setBlogpostId(25);
- $model->unsetBlogpostId();
- if($model->hasBlogpostId()){...}
這裏的方法名中的屬性名字符合“camelcase”命名規則 【譯 者注:簡單的說就是Java的命名規則,每個單詞的第一個字母大寫,第一個字母可以大寫也可以小寫】。爲了有效的利用這些方便的方法,我們在定義數據表列 名的時候要用小寫,並用下劃線作爲分隔符,比如“blogpost_id”。在最近的Magento版本中,這個規則已經被弱化,爲了實現PHP的
“ArrayAccess”接口
- $id = $model->['blogpost_id'];
- $model->['blogpost_id'] = 25;
- //etc...
也就是說,你會在Magento中同時看到這兩種技巧的使用。
Magento中的CRUD操作
Magento模型通過“load, save, delete”三個方法來支持基本的Create,Read,Update和Delete操作。我們在上面已經使用過“load”方法了。這個方法的參數就是要裝在的數據記錄的“id”。
- $blogpost->load(1);
“save”方法可以用來創建新數據或者修改已有數據。我們在BlogController.php中添加如下方法
- public function createNewPostAction() {
- $blogpost = Mage::getModel('helloworld/blogpost');
- $blogpost->setTitle('Code Post!');
- $blogpost->setPost('This post was created from code!');
- $blogpost->save();
- echo 'post created';
- }
訪問以下URLhttp://127.0.0.1/Magento/helloworld/blog/createNewPost
現在你數據表中應該有兩條數據了。下面來修改一條數據
- public function editFirstPostAction() {
- $blogpost = Mage::getModel('helloworld/blogpost');
- $blogpost->load(1);
- $blogpost->setTitle("The First post!");
- $blogpost->save();
- echo 'post edited';
- }
最後,我們來刪除一條數據
- public function deleteFirstPostAction() {
- $blogpost = Mage::getModel('helloworld/blogpost');
- $blogpost->load(1);
- $blogpost->delete();
- echo 'post removed';
- }
模型集合
上面的例子我們只是演示了對單個數據操作,現在我們來看看如何同時操作多條記錄。我們上面已經講過,每個Magento的模型都有一個獨特的模型集 合。這些模型集合實現了PHP的“IteratorAggregate”和“Countable”接口,也就是他們可以作爲“count”函數的參數,並 且可以在“for each”語句中使用。
現在讓我們來看看如何使用模型集合,在Blog控制器中添加如下方法
- public function showAllBlogPostsAction() {
- $posts = Mage::getModel('helloworld/blogpost')->getCollection();
- foreach($posts as $blog_post){
- echo '<h3>'.$blog_post->getTitle().'</h3>';
- echo nl2br($blog_post->getPost());
- }
- }
訪問如下URLhttp://127.0.0.1/Magento/helloworld/blog/showAllBlogPosts
你應該看到以下異常
- include(Zhlmmc\Helloworld\Model\Resource\Mysql4\Blogpost\Collection.php) [function.include]: failed to open stream: No such file or directory
我想你不會被這個異常嚇到,已經熟門熟路了。我們需要添加一個PHP類,定義Blogpost的模型集合。每個模型都有一個“protected”屬性 “_resourceCollectionName”【譯者注:從父類“Mage_Core_Model_Abstract”繼承來的】。這個屬性的值是 這個模型對應的模型集合的URI。
- protected '_resourceCollectionName' => string 'helloworld/blogpost_collection'
在默認情況下,這個值是模型的URI加上“_collection”。Magento把模型集合也看做是一種資源(Resrouce),所以運用資源模型的命名規則,模型集合的全名是Zhlmmc_Helloworld_Model_Resource_Mysql4_Blogpost_Collection
然後我們要創建如下文件File: app/code/local/Zhlmmc/Helloworld/Model/Resource/Mysql4/Blogpost/Collection.php
- class Zhlmmc_Helloworld_Model_Resource_Mysql4_Blogpost_Collection extends Mage_Core_Model_Mysql4_Collection_Abstract {
- protected function _construct()
- {
- $this->_init('helloworld/blogpost');
- }
- }
這裏的參數是模型的UR,用來I來初始化模型集合。刷新頁面,你應該看到數據庫中的Blog都顯示出來了。
總結
首先我要恭喜你,到這裏你已經創建並配置了你的第一個Magento模型。在後面的章節中我們將講解Magento的另外一種模型Entity Attribute Value Model。
在這章開始的時候,我撒了一個小謊。其實在Magento中,並不是所有的模型都繼承自“Mage_Core_Model_Abstract”。在 Magento最初的版本中,這個抽象類並不存在。所以有很多模型是直接繼承自“Varien_Object”。不過這些並不影響我們創建Magento 模型,瞭解一下就可以了,方便閱讀Magento的代碼。
from: http://www.zhlmmc.com/?p=659