有些裁縫只會塞塞襯里,修修補補,而有些裁縫卻能做出新東西--他們之間隔著一條深淵。 -- 《外套》
這里使用了NotORM進行DB操作,具體的數(shù)據(jù)庫操作使用文檔請見NotORM官網(wǎng): http://www.notorm.com
//查詢
$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();
在使用update操作時,如果更新的數(shù)據(jù)和原來的一樣,則會返回0(影響0行)。這時,會和更新失敗(同樣影響0行)混淆。
但NotORM是一個優(yōu)秀的類庫,所以提供了優(yōu)秀的解決文案。我們在使用update時,只須了解這兩者返回的結(jié)果的微妙區(qū)別即可。
因為失敗異常時,返回false;而相同數(shù)據(jù)更新會返回0。即:
用代碼表示,就是:
$rs = DI()->notorm->user->where('id', $userId)->update($data);
if ($rs >= 1) {
//成功
} else if ($rs === 0) {
//相同數(shù)據(jù),無更新
} else if ($rs === false) {
//更新失敗
}
以下單元測試代碼,可以驗證上面的判斷:
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);
}
如果是簡單的關(guān)聯(lián)查詢,可以使用NotORM支持的寫法,這樣的好處在于我們使用了一致的開發(fā),并且能讓PhalApi框架保持分布式的操作方式(注意,關(guān)聯(lián)的表仍然需要在同一個數(shù)據(jù)庫)。
以下是一個簡單的示例。
假設(shè)我們有這樣的數(shù)據(jù):
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);
那么對應(yīng)關(guān)聯(lián)查詢的代碼如下面:
public function testLeftJoin()
{
$rs = DI()->notorm->user_session_0
->select('expires_time, user.username, user.nickname')
->where('token', 'ABC')
->fetchRow();
var_dump($rs);
}
運行一下,我們可以看到這樣的輸出:
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"
}
這樣,我們就可以實現(xiàn)關(guān)聯(lián)查詢的操作。按照NotORM官網(wǎng)的說法,則是:
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')這一行調(diào)用將會【自動產(chǎn)生關(guān)聯(lián)操作】,而ON 的字段,則是這個字段關(guān)聯(lián)你配置的【表結(jié)構(gòu)】,外鍵默認為: 表名_id 。
NotORM已提供了NotORM_Literal,其用法如下:
DI()->notorm->user->where('id', 1)->update(array('age' => new NotORM_Literal("age + 1")));
當需要更新為當前時間,可以:
$array = array(
"title" => "NotORM",
"author_id" => null,
"created" => new NotORM_Literal("NOW()"),
);
但為了更符合項目的開發(fā),這里對NotORM的底層作了升級修改,以下為主要修改點和新的使用:
對原來的大部分使用無特別影響,可按原來的方式開發(fā)。主要目的是為了更方面處理返回的數(shù)據(jù),以及簡化對結(jié)果的再解析,簡單明了。
如:
DI()->notorm->user->where('username = ?', 'dogstar')->fetch();
返回的將是一個數(shù)組:
array(7) {
["id"]=>
string(3) "180"
["username"]=>
string(17) "dogstar"
["regtime"]=>
string(10) "1414811954"
//...
}
如:
$rows = DI()->notorm->event_picurl->where('eid', $eids)->fetchAll();
或:
$rows = DI()->notorm->event_picurl->where('eid', $eids)->fetchRows();
即可獲取全部的數(shù)據(jù),不再受限于分頁。
這里提供了fetchAll()和fetchRows()兩種等效的操作,是為了減少記憶的痛苦,下同。
當需要進行復(fù)雜的SQL查詢時,可以使用此接口,如:
(注意:limit替換值:start和:num必須使用int類型)
$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);
或:
$rs= DI()->notorm->example->queryRows($sql, $params);
取消了NotORM中對OFFSET關(guān)鍵字的使用,改用逗號的寫法,修改后正確的使用方法應(yīng)該是:
$table->limit(10); // limit 10 # 查詢前10個
$table->limit(5, 10); // limit 5,10 # 從第5個位置開始,查詢前10個
出于對數(shù)據(jù)的保護,當執(zhí)行刪除操作卻又沒有任何where條件時,將會禁止進行全表操作。如:
public function testDeleteAll()
{
DI()->notorm->user->delete();
}
可以看到:
$ 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
當處于debug模式時,可以輸入執(zhí)行的全部SQL語句,以便調(diào)試。
如:
SELECT times FROM tpl_user_session_10 WHERE (user_id = ?); -- '74110'
{"ret":0,"data":{"code":0},"msg":""}
之前,有開發(fā)同學提及到,為什么notorm的基類fetch為啥沒用limit(1)呢。后來,我去發(fā)了下NotORM的寫法,確實做得很微妙。
其實NotORM之所以沒有在fetch()里面自動limit 1是因為,你可以循環(huán)地獲取數(shù)據(jù),如:
$user = DI()->notorm->user->select('id, username, nickname')->where('id > ?', 0)->limit(3);
while(($row = $user->fetch())) {
var_dump($row);
}
但是,更多情況下,我們只需要獲取某一行的數(shù)據(jù),上面的做法會造成不必要的SQL查詢。為了保留原來的寫法,我特意添加擴展了一個新的操作:fetchRow(),用法同fetch(),但只會取第一條。
以下是使用示例:
$rs = DI()->notorm->user->select('id, username, nickname')->where('id > ?', 0)->fetchRow());
var_dump($rs);
//結(jié)果輸出類如:
array(3) {
["id"]=>
string(1) "1"
["username"]=>
string(5) "aevit"
["nickname"]=>
string(4) "test"
}
//對應(yīng)執(zhí)行的SQL語句:
[2 - 0.06544s]SELECT id, username, nickname FROM fami_user WHERE (id > ?) LIMIT 1; -- 0<br>
如果,我們只需要獲取這一行的某個字段,也可以像fecth()那樣使用,即:
$rs = DI()->notorm->user->select('id, username, nickname')->where('id > ?', 0)->fetchRow('nickname'));
var_dump($rs);
//結(jié)果輸出類如:
string(4) "test"
//紀錄不存在時,返回 bool(false)
很多時候,在開發(fā)時,我們對數(shù)據(jù)庫操作一開始會存在一些SQL語法的問題,PDO會返回false,且原來NotORM也是使用 靜默方式 來處理這類錯誤,從而使得開發(fā)人員有時難以發(fā)現(xiàn)這些問題,除非將調(diào)試的SQL手動放到數(shù)據(jù)庫執(zhí)行才能發(fā)現(xiàn)問題所在。
為了能給開發(fā)同學更早、更直觀的方式查看問題的所在,這里對NotORM底層進行了調(diào)整,使用了 顯式方式 的策略來處理,即:直接拋出PDO異常。
如:
$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();
將會看到:
[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'
如果是復(fù)雜的關(guān)聯(lián)查詢,則是建議使用原生態(tài)的SQL語句,但我們?nèi)匀豢梢员3趾芎玫膶懛?,如這樣一個示例:
$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());
注意,此時的表需要使用全名,即自帶前綴。這樣也可以實現(xiàn)更自由的關(guān)聯(lián)查詢。
關(guān)于事務(wù)操作,可以參考 NotORM官網(wǎng) 的說明:
$db->transaction = $command Assign 'BEGIN', 'COMMIT' or 'ROLLBACK' to start or stop transaction
即:
//第一步:先指定待進行事務(wù)的數(shù)據(jù)庫(通過獲取一個notorm表實例來指定;否則會提示:PDO There is no active transaction)
$user = DI()->notorm->user;
//第二步:開啟事務(wù)開關(guān)(此開關(guān)會將當前全部打開的數(shù)據(jù)庫都進行此設(shè)置)
DI()->notorm->transaction = 'BEGIN';
//第三步:進行數(shù)據(jù)庫操作
$user->insert(array('name' => 'test1',));
$user->insert(array('name' => 'test2',));
//第四:提交/回滾
DI()->notorm->transaction = 'COMMIT';
//DI()->notorm->transaction = 'ROLLBACK';
PhalApi一開始對事務(wù)這塊考慮不周,后來發(fā)現(xiàn)很多同學、很多項目都需要用到數(shù)據(jù)庫事務(wù)操作。
基于此,在不破壞原來的代碼基礎(chǔ)上,我們決定在PhalApi_DB_NotORM上添加對數(shù)據(jù)庫維度的事務(wù)操作支持。
示例簡單如下:
public function testTransactionCommit()
{
//Step 1: 開啟事務(wù)
$this->notorm->beginTransaction('db_demo');
//Step 2: 數(shù)據(jù)庫操作
$this->notorm->user>insert(array('name' => 'test1'));
$this->notorm->user>insert(array('name' => 'test2'));
//Step 3: 提交事務(wù)
$this->notorm->commit('db_demo');
}
溫馨提示: 以上操作,須PhalApi 1.3.1 及以上版本才能支持。
PhalApi使用的是NotORM來進行數(shù)據(jù)庫操作,而NotORM底層則是采用了PDO。目前,NotORM支持: MySQL, SQLite, PostgreSQL, MS SQL, Oracle (Dibi support is obsolete)。
但需要注意的是,PhalApi本身對NotORM進行了修改,需要調(diào)整一下代碼才能更好地支持除MySQL外的數(shù)據(jù)庫。即使NotORM不支持的數(shù)據(jù)庫,你也可以輕松通過添加擴展的方式來支持。如:
首先,定制自己的數(shù)據(jù)庫連接的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即可,如:
//數(shù)據(jù)操作 - 基于NotORM,$_GET['__sql__']可自行改名
DI()->notorm = function() {
$debug = !empty($_GET['__sql__']) ? true : false;
return new Common_MyDB(DI()->config->get('dbs'), $debug);
};
我們一直在考慮,是否應(yīng)該提供數(shù)據(jù)庫的基本操作支持,以減少開發(fā)人員重復(fù)手工編寫基本的數(shù)據(jù)操作。
最后,我們認為是需要的。然后就引發(fā)了新的問題:是以繼承還是以委托來支持?
委托有助于降低繼承的層級,但仍然需要編寫同類的操作然后再次委托。所以,這里提供了基于NotORM的Model基類:PhalApi_Model_NotORM。
然而提供這個基類還是會遇到一些問題,例如:如何界定基本操作?如何處理分表存儲?如何支持定制化?
由于我們這里的Model使用了 “表數(shù)據(jù)入口” 模式,而不是“行數(shù)據(jù)入口”,也不是“活動紀錄”,也不是復(fù)雜的“數(shù)據(jù)映射器”。所以在使用時可以考慮是否需要此基類。即使這樣,你也可以很輕松轉(zhuǎn)換到“行數(shù)據(jù)入口”和“活動紀錄”模式。這里,PhalApi中的Model是更廣義上的數(shù)據(jù)源層(后面會有更多說明),因此對應(yīng)地PhalApi_Model_NotORM基類充當了數(shù)據(jù)庫表訪問入口的對象,處理表中所有的行。
在明白了Model基類的背景后,再來了解其具體的操作和如何繼承會更有意義。
而具體的操作則與數(shù)據(jù)表的結(jié)構(gòu)相關(guān),在“約定編程”下:即每一個表都有一個主鍵(通常為id,也可以自由配置)以及一個序列化LOB字段ext_data。我們很容易想到Model接口的定義(注釋已移除,感興趣的同學可查看源碼):
interface PhalApi_Model {
public function get($id, $fields = '*');
public function insert($data, $id = NULL);
public function update($id, $data);
public function delete($id);
}
上面的接口在規(guī)約層上提供了基于表主鍵的CURD基本操作,在具體實現(xiàn)時,需要注意兩點:一是分表的處理;另一點則是LOB字段的序列化。
由于我們使用了NotORM進行數(shù)據(jù)庫的操作,所以這里也提供了基于NotORM的基類:PhalApi_Model_NotORM。下面以我們熟悉的獲取用戶的基本信息為例,說明此基類的使用。
為喚醒記憶,下面貼上Model_User類原來的代碼:
// $ vim ./Demo/Model/User.php
<?php
class Model_User {
public function getByUserId($userId) {
return DI()->notorm->user->select('*')->where('id = ?', $userId)->fetch();
}
}
對應(yīng)的調(diào)用:
$model = new Model_User();
$rs = $model->getByUserId($userId);
若繼承于PhalApi_Model_NotORM,則是:
// $ vim ./Demo/Model/User.php
<?php
class Model_User extends PhalApi_Model_NotORM {
protected function getTableName($id) {
return 'user';
}
}
從上面的代碼可以看出,基類已經(jīng)提供了基于主鍵的CURD操作,但我們需要鉤子函數(shù)以返回對應(yīng)的表名。相應(yīng)地,外部調(diào)用則調(diào)整為:
$model = new Model_User();
$rs = $model->get($userId);
再進一步,我們可以得到其他的基本操作:
$model = new Model_User();
//查詢
$row = $model->get(1);
$row = $model->get(1, 'id, name'); //取指定的字段
$row = $model->get(1, array('id', 'name')); //可以數(shù)組取指定要獲取的字段
//更新
$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);
正如上面提及到的兩個問題:LOB序列化和分表處理。所以,如果PhalApi現(xiàn)有就此兩問題的解決方案不能滿足項目的需求,可作定制化處理。
先是LOB序列化,考慮到有分表的存在,當發(fā)生數(shù)據(jù)庫變更時(特別在線上環(huán)境)會有一定的難度和風險,因此引入了擴展字段ext_data。當然,此字段也應(yīng)對數(shù)據(jù)庫變更的同時,也可以作為簡單明了的值對象的大對象。序列化LOB首先要考慮的問題是使用二進制(BLOB)還是文本(CLOB),出于通用性、易讀性和測試性,我們目前使用了json格式的文本序列化。所以,如果考慮到空間或性能問題(在少量數(shù)據(jù)下我認為問題不大,如果數(shù)據(jù)量大,應(yīng)該及時重新調(diào)整數(shù)據(jù)庫表結(jié)構(gòu)),可以重寫formatExtData() & parseExtData()。
如改成serialize序列化:
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后,
// $ vim ./Demo/Model/User.php
<?php
class Model_User extends Common_Model_NotORM {
//...
}
就可以輕松切換到序列化,如:
$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); //基于主鍵的快速更新
其次是分表處理,同樣考慮到分表的情況,以及不同的表可能配置不同的主鍵表,而基于主鍵的CURD又必須要先知道表的主鍵名才能進行SQL查詢。所以,問題就演變成了如何找到表的主鍵名。這里可以自動匹配,也可以手工指定。自動匹配是智能的,因為當我們更改表的主鍵時,可以自動同步更新而不需要擔心遺漏(雖然這種情況很少發(fā)生)。手工指定可以大大減少系統(tǒng)不必要的匹配操作,因為我們開發(fā)人員也知道數(shù)據(jù)庫的主鍵名是什么,但需要手工編寫一些代碼。在這里,提供了可選的手工指定,即可重寫getTableKey($table)來指定你的主鍵名。
如,當我們的表的主鍵都固定為id時:
abstract class Common_Model_NotORM extends PhalApi_Model_NotORM {
protected function getTableKey($table) {
return 'id';
}
}
更多建議: