_有些裁縫只會塞塞襯里,修修補補,而有些裁縫卻能做出新東西--他們之間隔著一條深淵。 -- 《外套》_
##1.15.1 NotORM官網
這里使用了NotORM進行DB操作,具體的數據庫操作使用文檔請見NotORM官網: [http://www.notorm.com](http://www.notorm.com)
###(1) 基本CURD
```javascript
//查詢
$row = DI()->notorm->user->where('id', 1)->fetch();
//更新
$data = array('name' => 'test', 'update_time' => time());
DI()->notorm->user->where('id', 1)->update($data);
//插入(須是同一個對象才能正確獲取插入的ID)
$data = array('name' => 'phalapi');
$userORM = DI()->notorm->user;
$userORM->insert($data);
$id = $userORM->insert_id();
//刪除
DI()->notorm->user->where('id', 1)->delete();
```
###(2)update相同的數據的判斷
在使用update操作時,如果更新的數據和原來的一樣,則會返回0(影響0行)。這時,會和更新失敗(同樣影響0行)混淆。
但NotORM是一個優秀的類庫,所以提供了優秀的解決文案。我們在使用update時,只須了解這兩者返回的結果的微妙區別即可。
因為失敗異常時,返回false;而相同數據更新會返回0。即:
+ 1、update相同的數據時,返回0,嚴格來說是:int(0)
+ 2、update失敗時,如更新一個不存在的字段,返回false,即:bool(false)
用代碼表示,就是:
```javascript
$rs = DI()->notorm->user->where('id', $userId)->update($data);
if ($rs >= 1) {
//成功
} else if ($rs === 0) {
//相同數據,無更新
} else if ($rs === false) {
//更新失敗
}
```
以下單元測試代碼,可以驗證上面的判斷:
```javascript
public function testUpdateOk()
{
$userId = 87;
$rs = DI()->notorm->user->where('id', $userId)->update(array('reg_time' => time()));
$this->assertSame(1, $rs);
}
public function testUpdateZero()
{
$userId = 1;
$rs = DI()->notorm->user->where('id', $userId)->update(array('username' => 'aevit'));
$this->assertSame(0, $rs);
}
public function testUpdateFail()
{
$userId = 1;
$rs = DI()->notorm->user->where('id', $userId)->update(array('wrong_username' => 'aevit'));
$this->assertSame(FALSE, $rs);
}
```
###(3)簡單的關聯查詢
如果是簡單的關聯查詢,可以使用NotORM支持的寫法,這樣的好處在于我們使用了一致的開發,并且能讓PhalApi框架保持分布式的操作方式(注意,關聯的表仍然需要在同一個數據庫)。
以下是一個簡單的示例。
假設我們有這樣的數據:
```javascript
INSERT INTO `phalapi_user` VALUES ('1', 'wx_edebc877070133c65161d00799e00544', 'weixinName', '******', '4CHqOhe1Jxi3X9HmRfPOXygDnU267eCA', '1431790647', 'phpunit.png');
INSERT INTO `phalapi_user_session_0` VALUES ('1', '1', 'ABC', '', '0', '0', '0', null);
```
那么對應關聯查詢的代碼如下面:
```javascript
public function testLeftJoin()
{
$rs = DI()->notorm->user_session_0
->select('expires_time, user.username, user.nickname')
->where('token', 'ABC')
->fetchRow();
var_dump($rs);
}
```
運行一下,我們可以看到這樣的輸出:
```javascript
SELECT expires_time, user.username, user.nickname FROM phalapi_user_session_0 LEFT JOIN phalapi_user AS user ON phalapi_user_session_0.user_id = user.id WHERE (token = 'ABC') LIMIT 1;
.[1 - 0.06318s]SELECT expires_time, user.username, user.nickname FROM phalapi_user_session_0 LEFT JOIN phalapi_user AS user ON phalapi_user_session_0.user_id = user.id WHERE (token = 'ABC') LIMIT 1;<br>
array(3) {
["expires_time"]=>
string(1) "0"
["username"]=>
string(35) "wx_edebc877070133c65161d00799e00544"
["nickname"]=>
string(10) "weixinName"
}
```
這樣,我們就可以實現關聯查詢的操作。按照NotORM官網的說法,則是:
> If the dot notation is used for a column anywhere in the query ("$table.$column") then NotORM automatically creates left join to the referenced table. Even references across several tables are possible ("$table1.$table2.$column"). Referencing tables can be accessed by colon: $applications->select("COUNT(application_tag:tag_id)").
->select('expires_time, user.username, user.nickname')這一行調用將會【自動產生關聯操作】,而ON 的字段,則是這個字段關聯你配置的【表結構】,外鍵默認為: 表名_id 。
###(4)加1操作
NotORM已提供了NotORM_Literal,其用法如下:
```javascript
DI()->notorm->user->where('id', 1)->update(array('age' => new NotORM_Literal("age + 1")));
```
當需要更新為當前時間,可以:
```javascript
$array = array(
"title" => "NotORM",
"author_id" => null,
"created" => new NotORM_Literal("NOW()"),
);
```
##1.15.2 NotORM的優化
但為了更符合項目的開發,這里對NotORM的底層作了升級修改,以下為主要修改點和新的使用:
###(1)將原來返回的結果全部從對象改成數組
對原來的大部分使用無特別影響,可按原來的方式開發。主要目的是為了更方面處理返回的數據,以及簡化對結果的再解析,簡單明了。
如:
```javascript
DI()->notorm->user->where('username = ?', 'dogstar')->fetch();
```
返回的將是一個數組:
```javascript
array(7) {
["id"]=>
string(3) "180"
["username"]=>
string(17) "dogstar"
["regtime"]=>
string(10) "1414811954"
//...
}
```
###(2)提供獲取全部結果的接口 - fetchAll() / fetchRows()
如:
```javascript
$rows = DI()->notorm->event_picurl->where('eid', $eids)->fetchAll();
```
或:
```javascript
$rows = DI()->notorm->event_picurl->where('eid', $eids)->fetchRows();
```
即可獲取全部的數據,不再受限于分頁。
這里提供了fetchAll()和fetchRows()兩種等效的操作,是為了減少記憶的痛苦,下同。
###(3)提供更靈活的查詢方式 - queryAll() / queryRows()
當需要進行復雜的SQL查詢時,可以使用此接口,如:
(注意:limit替換值:start和:num必須使用int類型)
```javascript
$sql = 'select * from example AS ep LEFT JOIN user AS u ON ep.ui
d = u.id where ep.touid = :userId ORDER BY dateline desc LIMIT :start,:num';
$params = array(':userId' => $userId, ':start' => $start, ':num' => $num);
$rs= DI()->notorm->example->queryAll($sql, $params);
```
或:
```javascript
$rs= DI()->notorm->example->queryRows($sql, $params);
```
###(4)limit 操作的調整
取消了NotORM中對OFFSET關鍵字的使用,改用逗號的寫法,修改后正確的使用方法應該是:
```javascript
$table->limit(10); // limit 10 # 查詢前10個
$table->limit(5, 10); // limit 5,10 # 從第5個位置開始,查詢前10個
```
###(5)禁止全表刪除,防止誤刪
出于對數據的保護,當執行刪除操作卻又沒有任何where條件時,將會禁止進行全表操作。如:
```javascript
public function testDeleteAll()
{
DI()->notorm->user->delete();
}
```
可以看到:
```javascript
$ phpunit --filter testDeleteAll ./Api/Api_User_Test.php
PHPUnit 4.3.4 by Sebastian Bergmann.
E
Time: 315 ms, Memory: 6.25Mb
There was 1 error:
1) PhpUnderControl_ApiUser_Test::testDeleteAll
Exception: sorry, you can not delete the whole table --dogstar
```
###(6)添加& \_\_sql\_\_ =1請求參數,可開啟HTTP調試模式
當處于debug模式時,可以輸入執行的全部SQL語句,以便調試。
如:
```javascript
SELECT times FROM tpl_user_session_10 WHERE (user_id = ?); -- '74110'
{"ret":0,"data":{"code":0},"msg":""}
```
###(7)關于NotORM中fetch()操作沒有limit 1的處理方案 - fetchOne() / fetchRow()
之前,有開發同學提及到,為什么notorm的基類fetch為啥沒用limit(1)呢。后來,我去發了下NotORM的寫法,確實做得很微妙。
其實NotORM之所以沒有在fetch()里面自動limit 1是因為,你可以循環地獲取數據,如:
```javascript
$user = DI()->notorm->user->select('id, username, nickname')->where('id > ?', 0)->limit(3);
while(($row = $user->fetch())) {
var_dump($row);
}
```
但是,更多情況下,我們只需要獲取某一行的數據,上面的做法會造成不必要的SQL查詢。為了保留原來的寫法,我特意添加擴展了一個新的操作:fetchRow(),用法同fetch(),但只會取第一條。
以下是使用示例:
```javascript
$rs = DI()->notorm->user->select('id, username, nickname')->where('id > ?', 0)->fetchRow());
var_dump($rs);
//結果輸出類如:
array(3) {
["id"]=>
string(1) "1"
["username"]=>
string(5) "aevit"
["nickname"]=>
string(4) "test"
}
//對應執行的SQL語句:
[2 - 0.06544s]SELECT id, username, nickname FROM fami_user WHERE (id > ?) LIMIT 1; -- 0<br>
```
如果,我們只需要獲取這一行的某個字段,也可以像fecth()那樣使用,即:
```javascript
$rs = DI()->notorm->user->select('id, username, nickname')->where('id > ?', 0)->fetchRow('nickname'));
var_dump($rs);
//結果輸出類如:
string(4) "test"
//紀錄不存在時,返回 bool(false)
```
###(8)顯式的SQL語法異常提示
很多時候,在開發時,我們對數據庫操作一開始會存在一些SQL語法的問題,PDO會返回false,且原來NotORM也是使用 **靜默方式** 來處理這類錯誤,從而使得開發人員有時難以發現這些問題,除非將調試的SQL手動放到數據庫執行才能發現問題所在。
為了能給開發同學更早、更直觀的方式查看問題的所在,這里對NotORM底層進行了調整,使用了 **顯式方式** 的策略來處理,即:直接拋出PDO異常。
如:
```javascript
$userId = 1;
//OK
$rs = DI()->notorm->user->select('username')->where('id', $userId)->fetchOne();
//WRONG
$rs = DI()->notorm->user->select('wrong_username')->where('id', $userId)->fetchOne();
```
將會看到:
```javascript
[1 - 0.06437s]SELECT username FROM fami_user WHERE (id = 1) LIMIT 1;<br>
[2 - 0.06496s]SELECT wrong_username FROM fami_user WHERE (id = 1) LIMIT 1;<br>
PDOException: Unknown column 'wrong_username' in 'field list'
```
###(9)復雜的關聯查詢
如果是復雜的關聯查詢,則是建議使用原生態的SQL語句,但我們仍然可以保持很好的寫法,如這樣一個示例:
```javascript
$sql = 'SELECT t.id, t.team_name, v.vote_num '
. 'FROM phalapi_team AS t LEFT JOIN phalapi_vote AS v '
. 'ON t.id = v.team_id '
. 'ORDER BY v.vote_num DESC';
$rows = $this->getORM()->queryAll($sql, array());
```
注意,此時的表需要使用全名,即自帶前綴。這樣也可以實現更自由的關聯查詢。
###(10)事務操作
關于事務操作,可以參考 [NotORM官網](http://www.notorm.com/#api) 的說明:
```
$db->transaction = $command Assign 'BEGIN', 'COMMIT' or 'ROLLBACK' to start or stop transaction
```
即:
```
//第一步:先指定待進行事務的數據庫(通過獲取一個notorm表實例來指定;否則會提示:PDO There is no active transaction)
$user = DI()->notorm->user;
//第二步:開啟事務開關(此開關會將當前全部打開的數據庫都進行此設置)
DI()->notorm->transaction = 'BEGIN';
//第三步:進行數據庫操作
$user->insert(array('name' => 'test1',));
$user->insert(array('name' => 'test2',));
//第四:提交/回滾
DI()->notorm->transaction = 'COMMIT';
//DI()->notorm->transaction = 'ROLLBACK';
```
####推薦使用PhalApi的事務操作方式
PhalApi一開始對事務這塊考慮不周,后來發現很多同學、很多項目都需要用到數據庫事務操作。
基于此,在不破壞原來的代碼基礎上,我們決定在PhalApi_DB_NotORM上添加對數據庫維度的事務操作支持。
示例簡單如下:
```
public function testTransactionCommit()
{
//Step 1: 開啟事務
$this->notorm->beginTransaction('db_demo');
//Step 2: 數據庫操作
$this->notorm->user>insert(array('name' => 'test1'));
$this->notorm->user>insert(array('name' => 'test2'));
//Step 3: 提交事務
$this->notorm->commit('db_demo');
}
```
> 溫馨提示: 以上操作,須PhalApi 1.3.1 及以上版本才能支持。
###(11)擴展對非MySQL數據庫的支持
PhalApi使用的是NotORM來進行數據庫操作,而NotORM底層則是采用了PDO。目前,NotORM支持: MySQL, SQLite, PostgreSQL, MS SQL, Oracle (Dibi support is obsolete)。
但需要注意的是,PhalApi本身對NotORM進行了修改,需要調整一下代碼才能更好地支持除MySQL外的數據庫。
即使NotORM不支持的數據庫,你也可以輕松通過添加擴展的方式來支持。如:
首先,定制自己的數據庫連接的PDO。
```
class Common_MyDB extends PhalApi_DB_NotORM {
protected function createPDOBy($dbCfg) {
/* Connect to an ODBC database using driver invocation */
$dsn = 'uri:file:///usr/local/dbconnect';
return new PDO($dsn, $dbCfg['user'], $dbCfg['password']);
}
}
```
隨后,在初始化文件init.php中重新注冊DI()->notorm即可,如:
```
//數據操作 - 基于NotORM,$_GET['__sql__']可自行改名
DI()->notorm = function() {
$debug = !empty($_GET['__sql__']) ? true : false;
return new Common_MyDB(DI()->config->get('dbs'), $debug);
};
```
##1.15.3 可選的Model基類
###(1)表數據入口模式
我們一直在考慮,是否應該提供數據庫的基本操作支持,以減少開發人員重復手工編寫基本的數據操作。
最后,我們認為是需要的。然后就引發了新的問題:是以繼承還是以委托來支持?
委托有助于降低繼承的層級,但仍然需要編寫同類的操作然后再次委托。所以,這里提供了基于NotORM的Model基類:PhalApi_Model_NotORM。
然而提供這個基類還是會遇到一些問題,例如:如何界定基本操作?如何處理分表存儲?如何支持定制化?
由于我們這里的Model使用了 **“表數據入口”** 模式,而不是“行數據入口”,也不是“活動紀錄”,也不是復雜的“數據映射器”。所以在使用時可以考慮是否需要此基類。即使這樣,你也可以很輕松轉換到“行數據入口”和“活動紀錄”模式。這里,PhalApi中的Model是更廣義上的數據源層(后面會有更多說明),因此對應地PhalApi_Model_NotORM基類充當了數據庫表訪問入口的對象,處理表中所有的行。
###(2)規約層的CURD
在明白了Model基類的背景后,再來了解其具體的操作和如何繼承會更有意義。
而具體的操作則與數據表的結構相關,在“約定編程”下:即每一個表都有一個主鍵(通常為id,也可以自由配置)以及一個序列化LOB字段ext_data。我們很容易想到Model接口的定義(注釋已移除,感興趣的同學可查看源碼):
```javascript
interface PhalApi_Model {
public function get($id, $fields = '*');
public function insert($data, $id = NULL);
public function update($id, $data);
public function delete($id);
}
```
上面的接口在規約層上提供了基于表主鍵的CURD基本操作,在具體實現時,需要注意兩點:一是分表的處理;另一點則是LOB字段的序列化。
###(3)不使用Model基類的寫法
由于我們使用了NotORM進行數據庫的操作,所以這里也提供了基于NotORM的基類:PhalApi_Model_NotORM。下面以我們熟悉的獲取用戶的基本信息為例,說明此基類的使用。
為喚醒記憶,下面貼上Model_User類原來的代碼:
```javascript
// $ vim ./Demo/Model/User.php
<?php
class Model_User {
public function getByUserId($userId) {
return DI()->notorm->user->select('*')->where('id = ?', $userId)->fetch();
}
}
```
對應的調用:
```javascript
$model = new Model_User();
$rs = $model->getByUserId($userId);
```
###(4)繼承Model基類的寫法
若繼承于PhalApi_Model_NotORM,則是:
```javascript
// $ vim ./Demo/Model/User.php
<?php
class Model_User extends PhalApi_Model_NotORM {
protected function getTableName($id) {
return 'user';
}
}
```
從上面的代碼可以看出,基類已經提供了基于主鍵的CURD操作,但我們需要鉤子函數以返回對應的表名。相應地,外部調用則調整為:
```javascript
$model = new Model_User();
$rs = $model->get($userId);
```
再進一步,我們可以得到其他的基本操作:
```javascript
$model = new Model_User();
//查詢
$row = $model->get(1);
$row = $model->get(1, 'id, name'); //取指定的字段
$row = $model->get(1, array('id', 'name')); //可以數組取指定要獲取的字段
//更新
$data = array('name' => 'test', 'update_time' => time());
$model->update(1, $data); //基于主鍵的快速更新
//插入
$data = array('name' => 'phalapi');
$id = $model->insert($data);
//$id = $model->insert($data, 5); //如果是分表,可以這樣指定
//刪除
$model->delete(1);
```
##1.15.4 定制化你的Model基類
正如上面提及到的兩個問題:LOB序列化和分表處理。所以,如果PhalApi現有就此兩問題的解決方案不能滿足項目的需求,可作定制化處理。
###(1)LOB序列化
先是LOB序列化,考慮到有分表的存在,當發生數據庫變更時(特別在線上環境)會有一定的難度和風險,因此引入了擴展字段ext_data。當然,此字段也應對數據庫變更的同時,也可以作為簡單明了的值對象的大對象。序列化LOB首先要考慮的問題是使用二進制(BLOB)還是文本(CLOB),出于通用性、易讀性和測試性,我們目前使用了json格式的文本序列化。所以,如果考慮到空間或性能問題(在少量數據下我認為問題不大,如果數據量大,應該及時重新調整數據庫表結構),可以重寫formatExtData() & parseExtData()。
如改成serialize序列化:
```javascript
abstract class Common_Model_NotORM extends PhalApi_Model_NotORM {
/**
* 對LOB的ext_data字段進行格式化(序列化)
*/
protected function formatExtData(&$data) {
if (isset($data['ext_data'])) {
$data['ext_data'] = serialize($data['ext_data']);
}
}
/**
* 對LOB的ext_data字段進行解析(反序列化)
*/
protected function parseExtData(&$data) {
if (isset($data['ext_data'])) {
$data['ext_data'] = unserialize($data['ext_data'], true);
}
}
// ...
}
```
將Model類繼承于Common_Model_NotORM后,
```javascript
// $ vim ./Demo/Model/User.php
<?php
class Model_User extends Common_Model_NotORM {
//...
}
```
就可以輕松切換到序列化,如:
```javascript
$model = new Model_User();
//帶有ext_data的更新
$extData = array('level' => 3, 'coins' => 256);
$data = array('name' => 'test', 'update_time' => time(), 'ext_data' => $extData);
$model->update(1, $data); //基于主鍵的快速更新
```
###(2)分表處理
其次是分表處理,同樣考慮到分表的情況,以及不同的表可能配置不同的主鍵表,而基于主鍵的CURD又必須要先知道表的主鍵名才能進行SQL查詢。所以,問題就演變成了如何找到表的主鍵名。這里可以自動匹配,也可以手工指定。自動匹配是智能的,因為當我們更改表的主鍵時,可以自動同步更新而不需要擔心遺漏(雖然這種情況很少發生)。手工指定可以大大減少系統不必要的匹配操作,因為我們開發人員也知道數據庫的主鍵名是什么,但需要手工編寫一些代碼。在這里,提供了可選的手工指定,即可重寫getTableKey($table)來指定你的主鍵名。
如,當我們的表的主鍵都固定為id時:
```javascript
abstract class Common_Model_NotORM extends PhalApi_Model_NotORM {
protected function getTableKey($table) {
return 'id';
}
}
```
- 歡迎使用PhalApi!
- 接口,從簡單開始!
- [1.1]-下載與安裝
- [1.2]-創建一個自己的項目
- [1.3]-在線體驗
- [1.4]-文檔、幫助和官網
- [1.10]-對PhalApi框架的抉擇
- [1.11]-快速入門(backup)
- [1.12]-參數規則:接口參數規則配置
- [1.13]-統一的接口請求方式:_sevice=XXX.XXX
- [1.14]-統一的返回格式和結構:ret-data-msg
- [1.15]-數據庫操作:基于NotORM的使用及優化
- [1.16]-配置讀取:內外網環境配置的完美切換
- [1.17]-日記紀錄:簡化版的日記接口
- [1.18]-快速函數:人性化的關懷
- [1.19]-DI服務速查:各資源服務一覽表
- [1.20]-DB操作:數據庫基本操作速查
- [1.21]-類的自動加載:遵循PEAR包的命名規范
- [1.22]-簽名驗證:自定義簽名規則
- [1.23]-請求和響應:GET和POST兩者皆可得及超越JSON格式返回
- [1.24]-緩存策略:更靈活地可配置化的多級緩存
- [1.25]-國際化翻譯:為走向國際化提前做好翻譯準備
- [1.26]-數據安全:數據對稱加密方案
- [1.27]-精益開發:更富表現力的Model層和重量級數據獲取的應對方案
- [1.28]-COOKIE:對COOKIE原生態的支持及記憶加密升級版
- [1.29]-開放與封閉:多入口和統一初始化
- [1.30]-保持的力量:接口開發最佳實踐
- [1.31]-新型計劃任務:以接口形式實現的計劃任務
- [2.11]-核心思想:DI依賴注入-讓資源更可控
- [2.12]-海量數據:可配置的分庫分表
- [2.13]-接口調試:在線SQL語句查看與性能優化
- [2.14]-測試驅動開發:意圖導向編程下的接口開發
- [2.15]-演進:新型計劃任務續篇
- [2.16]-領域驅動設計:應對復雜領域業務的Domain層
- [2.17]-微服務:Api接口服務層
- [2.18]-定制化:資源服務的再實現
- [2.19]-擴展庫:可重用的擴展類庫
- [2.20]-約定編程:架構明顯的編程風格
- [2.21]-服務器統一部署方案簡明版:CentOs---Nginx---php-fpm---MySql-[--Memcached]
- [2.22]-更多工具:精益項目和團隊建設
- [3.1]-擴展類庫:微信開發
- [3.2]-擴展類庫:代理模式下phprpc協議的輕松支持
- [3.3]-擴展類庫:基于PHPMailer的郵件發送
- [3.4]-擴展類庫:優酷開放平臺接口調用
- [3.5]-擴展類庫:七牛云存儲接口調用
- [3.6]-擴展類庫:新型計劃任務
- [3.8]-擴展類庫:用戶、會話和第三方登錄集成
- [3.9]-擴展類庫:swoole支持下的長鏈接和異步任務實現
- [3.11]-擴展類庫:基于FastRoute的快速路由
- [4.2]-開發實戰2:模擬優酷開放平臺接口項目開發
- [4.3]-開發實戰3:一個簡單的小型項目開發(奔跑吧兄弟投票活動)
- [5.1]-架構與思想:PhalApi核心設計和思想解讀
- [5.2]-雜談:扯一些PhalApi的前世和今生
- [5.3]-框架總結:術語表和PHP開發建議
- [5.4]-許可
- [5.5]-聯系和加入我們
- [5.6]-更新日記
- [5.8]-致框架貢獻者:加入PhalApi開源指南
- [6.1]-基于接口查詢語言的SDK包
- [6.2]-SDK包(JAVA版)
- [6.3]-SDK包(PHP版)
- [6.4]-SDK包(Objective-C版)
- [6.5]-SDK包(javascript版)
- [6.6]-SDK包(Ruby版)
- [8.1]-PhalApi視頻教程
- 附錄1:接口文檔參考模板