Fork me on GitHub

上一章 文档首页 下一章


有些裁缝只会塞塞衬里,修修补补,而有些裁缝却能做出新东西--他们之间隔着一条深渊。 -- 《外套》

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>

如果,我们只需要获取这一行的某个字段,也可以像fetch()那样使用,即:

$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: 开启事务
        DI()->notorm->beginTransaction('db_demo');

        //Step 2: 数据库操作
        DI()->notorm->user>insert(array('name' => 'test1'));
        DI()->notorm->user>insert(array('name' => 'test2'));

        //Step 3: 提交事务
        DI()->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';
    }
}

上一章 文档首页 下一章