数据库操作:基于NotORM的使用及优化
有些裁缝只会塞塞衬里,修修补补,而有些裁缝却能做出新东西--他们之间隔着一条深渊。 -- 《外套》
1.15.1 NotORM官网
这里使用了NotORM进行DB操作,具体的数据库操作使用文档请见NotORM官网: http://www.notorm.com
(1) 基本CURD
//查询
$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)
用代码表示,就是:
$rs = DI()->notorm->user->where('id', $userId)->update($data);
if ($rs >= 1) {
//成功
} else if ($rs === 0) {
//相同数据,无更新
} 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);
}
(3)简单的关联查询
如果是简单的关联查询,可以使用NotORM支持的写法,这样的好处在于我们使用了一致的开发,并且能让PhalApi框架保持分布式的操作方式(注意,关联的表仍然需要在同一个数据库)。
以下是一个简单的示例。
假设我们有这样的数据:
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);
那么对应关联查询的代码如下面:
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"
}
这样,我们就可以实现关联查询的操作。按照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,其用法如下:
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()"),
);
1.15.2 NotORM的优化
但为了更符合项目的开发,这里对NotORM的底层作了升级修改,以下为主要修改点和新的使用:
(1)将原来返回的结果全部从对象改成数组
对原来的大部分使用无特别影响,可按原来的方式开发。主要目的是为了更方面处理返回的数据,以及简化对结果的再解析,简单明了。
如:
DI()->notorm->user->where('username = ?', 'dogstar')->fetch();
返回的将是一个数组:
array(7) {
["id"]=>
string(3) "180"
["username"]=>
string(17) "dogstar"
["regtime"]=>
string(10) "1414811954"
//...
}
(2)提供获取全部结果的接口 - fetchAll() / fetchRows()
如:
$rows = DI()->notorm->event_picurl->where('eid', $eids)->fetchAll();
或:
$rows = DI()->notorm->event_picurl->where('eid', $eids)->fetchRows();
即可获取全部的数据,不再受限于分页。
这里提供了fetchAll()和fetchRows()两种等效的操作,是为了减少记忆的痛苦,下同。
(3)提供更灵活的查询方式 - queryAll() / queryRows()
当需要进行复杂的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);
(4)limit 操作的调整
取消了NotORM中对OFFSET关键字的使用,改用逗号的写法,修改后正确的使用方法应该是:
$table->limit(10); // limit 10 # 查询前10个
$table->limit(5, 10); // limit 5,10 # 从第5个位置开始,查询前10个
(5)禁止全表删除,防止误删
出于对数据的保护,当执行删除操作却又没有任何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
(6)添加& __sql__ =1请求参数,可开启HTTP调试模式
当处于debug模式时,可以输入执行的全部SQL语句,以便调试。
如:
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是因为,你可以循环地获取数据,如:
$user = DI()->notorm->user->select('id, username, nickname')->where('id > ?', 0)->limit(3);
while(($row = $user->fetch())) {
var_dump($row);
}
但是,更多情况下,我们只需要获取某一行的数据,上面的做法会造成不必要的SQL查询。为了保留原来的写法,我特意添加扩展了一个新的操作:fetchRow(),用法同fetch(),但只会取第一条。
以下是使用示例:
$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()那样使用,即:
$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异常。
如:
$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'
(9)复杂的关联查询
如果是复杂的关联查询,则是建议使用原生态的SQL语句,但我们仍然可以保持很好的写法,如这样一个示例:
$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官网 的说明:
$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接口的定义(注释已移除,感兴趣的同学可查看源码):
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类原来的代码:
// $ vim ./Demo/Model/User.php
<?php
class Model_User {
public function getByUserId($userId) {
return DI()->notorm->user->select('*')->where('id = ?', $userId)->fetch();
}
}
对应的调用:
$model = new Model_User();
$rs = $model->getByUserId($userId);
(4)继承Model基类的写法
若继承于PhalApi_Model_NotORM,则是:
// $ vim ./Demo/Model/User.php
<?php
class Model_User extends PhalApi_Model_NotORM {
protected function getTableName($id) {
return 'user';
}
}
从上面的代码可以看出,基类已经提供了基于主键的CURD操作,但我们需要钩子函数以返回对应的表名。相应地,外部调用则调整为:
$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')); //可以数组取指定要获取的字段
//更新
$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序列化:
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); //基于主键的快速更新
(2)分表处理
其次是分表处理,同样考虑到分表的情况,以及不同的表可能配置不同的主键表,而基于主键的CURD又必须要先知道表的主键名才能进行SQL查询。所以,问题就演变成了如何找到表的主键名。这里可以自动匹配,也可以手工指定。自动匹配是智能的,因为当我们更改表的主键时,可以自动同步更新而不需要担心遗漏(虽然这种情况很少发生)。手工指定可以大大减少系统不必要的匹配操作,因为我们开发人员也知道数据库的主键名是什么,但需要手工编写一些代码。在这里,提供了可选的手工指定,即可重写getTableKey($table)来指定你的主键名。
如,当我们的表的主键都固定为id时:
abstract class Common_Model_NotORM extends PhalApi_Model_NotORM {
protected function getTableKey($table) {
return 'id';
}
}
更多建议: