🔥码云GVP开源项目 12k star Uniapp+ElementUI 功能强大 支持多语言、二开方便! 广告
_有些裁缝只会塞塞衬里,修修补补,而有些裁缝却能做出新东西--他们之间隔着一条深渊。 -- 《外套》_ ##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'; } } ```