_有些裁缝只会塞塞衬里,修修补补,而有些裁缝却能做出新东西--他们之间隔着一条深渊。 -- 《外套》_
##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:接口文档参考模板