第四章 – 模型和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';
}
}
通路以下URL
http://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';
}
通路以下URL
http://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());
}
}
通路如下URL
http://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的代碼。
轉載于:https://my.oschina.net/u/3934842/blog/3011252