# Model数据模型层与数据库操作
Model层称为数据模型层,负责技术层面上对数据信息的提取、存储、更新和删除等操作,数据可来自内存,也可以来自持久化存储媒介,甚至可以是来自外部第三方系统。虽然PhalApi的Model层是广义上的数据层,但考虑到大部分数据都是来自于数据库的操作,所以这一章将重点讲述如何进行数据库操作。
## 数据库配置
数据库的配置文件为./config/dbs.php,默认使用的是MySQL数据库,所以需要配置MySQL的连接信息。servers选项用于配置数据库服务器相关信息,可以配置多组数据库实例,每组包括数据库的账号、密码、数据库名字等信息。不同的数据库实例,使用不同标识作为下标。
servers数据库配置项|说明
---|---
host|数据库域名
name|数据库名字
user|数据库用户名
password|数据库密码
port|数据库端口
charset|数据库字符集
tables选项用于配置数据库表的表前缀、主键字段和路由映射关系,可以配置多个表,下标为不带表前缀的表名,其中```__default__```下标选项为缺省的数据库路由,即未配置的数据库表将使用这一份默认配置。
表2-12 表配置项
tables表配置项|说明
---|---
prefix|表前缀
key|表主键
map|数据库实例映射关系,可配置多组。每组格式为:```array('db' => 服务器标识, 'start' => 开始分表标识, 'end' => 结束分表标识)```,start和end要么都不提供,要么都提供
例如默认数据库配置为:
```php
return array(
/**
* DB数据库服务器集群
*/
'servers' => array(
'db_master' => array( //服务器标记
'host' => '127.0.0.1', //数据库域名
'name' => 'phalapi', //数据库名字
'user' => 'root', //数据库用户名
'password' => '', //数据库密码
'port' => 3306, //数据库端口
'charset' => 'UTF8', //数据库字符集
),
),
/**
* 自定义路由表
*/
'tables' => array(
//通用路由
'__default__' => array(
'prefix' => 'tbl_',
'key' => 'id',
'map' => array(
array('db' => 'db_master'),
),
),
),
);
```
其中,在servers中配置了名称为db_master数据库实例,意为数据库主库,其host为localhost,名称为phalapi,用户名为root等。在tables中,只配置了通用路由,并且表前缀为tbl_,主键均为id,并且全部使用db_demo数据库实例。
> **温馨提示:**当tables中配置的db数据库实例不存在servers中时,将会提示数据库配置错误。
### 如何排查数据库连接错误?
普通情况下,数据库连接失败时会这样提示:
```
{
"ret": 500,
"data": [],
"msg": "服务器运行错误: 数据库db_demo连接失败"
}
```
考虑到生产环境不方便爆露服务器的相关信息,故这样简化提示。当在开发过程中,需要定位数据库连接失败的原因时,可使用debug调试模式。开启调试后,当再次失败时,会看到类似这样的提示:
```
{
"ret": 500,
"data": [],
"msg": "服务器运行错误: 数据库db_demo连接失败,异常码:1045,错误原因:SQLSTATE[28000] [1045] ... ..."
}
```
然后,便可根据具体的错误提示进行排查解决。
## NotORM简介
NotORM是一个优秀的开源PHP类库,可用于操作数据库。PhalApi的数据库操作,主要是依赖此NotORM来完成。
> 参考:NotORM官网:[www.notorm.com](http://www.notorm.com/)。
所以,如果了解NotORM的使用,自然而然对PhalApi中的数据库操作也就一目了然了。但为了更符合接口类项目的开发,PhalApi对NotORM的底层进行优化和调整。以下改动点包括但不限于:
+ 将原来返回的结果全部从对象类型改成数组类型,便于数据流通
+ 添加查询多条纪录的接口:```NotORM_Result::fetchAll()```和```NotORM_Result::fetchRows()```
+ 添加支持原生SQL语句查询的接口:```NotORM_Result::queryAll()```和```NotORM_Result::queryRows()```
+ limit 操作的调整,取消原来OFFSET关键字的使用
+ 当数据库操作失败时,抛出PDOException异常
+ 将结果集中以主键作为下标改为以顺序索引作为下标
+ 禁止全表删除,防止误删
+ 调整调试模式
### 如何获取NotORM实例?
在PhalApi中获取NotORM实例,有两种方式:全局获取方式、局部获取方式。
+ **全局获取**:在任何地方,使用DI容器中的全局notorm服务:```\PhalApi\DI()->notorm```
+ **局部获取**:在继承PhalApi\Model\NotORMModel的子类中使用:```$this->getORM()```
第一种全局获取的方式,可以用于任何地方,这是因为我们已经在初始化文件中注册了```\PhalApi\DI()->notorm```这一服务。
第二种局部获取的方式,仅限用于继承PhalApi\Model\NotORMModel的子类中。首先需要实现相应的Model子类,通常一个表对应一个Model子类。例如为user表创建相应的Model类。
PhalApi推荐使用封装的第二种方式,并且下面所介绍的使用都是基于第二种快速方式。以下是获取一个NotORM实例的示例。
```php
class User extends NotORM {
public function doSth() {
$orm = $this->getORM();
}
}
```
## Model子类与表名
### 如何新增一个Model类?
通常情况下,一张表对应一个Model类。当需要新增时,可以继承于PhalApi\Model\NotORMModel类,并放置在App\Model命名空间下。例如,对于数据库表tbl_user:
```sql
CREATE TABLE `tbl_user` (
`id` int(11) NOT NULL,
`name` varchar(45) DEFAULT NULL,
`age` int(3) DEFAULT NULL,
`note` varchar(45) DEFAULT NULL,
`create_date` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
```
可以新增App\Model\User.php文件,并在里面放置以下代码。
```php
<?php
namespace App\Model;
use PhalApi\Model\NotORMModel as NotORM;
class User extends NotORM {
}
```
至此,便可得到一个基本的Model子类了。
#### Model的基本操作
对于基本的Model子类,可以得到基本的数据库操作。以下示例演示了Model的基本CURD操作。
```php
$model = new App\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); //如果是分表,可以通过第二个参数指定分表的参考ID
// 删除
$model->delete(1);
```
#### 数据库表名配置
上面的App\Model\User类,自动匹配的表名为:user,加上配置前缀“tbl_”,完整的表名是:tbl_usre。默认表名的自动匹配规则是:取“\Model\”后面部分的字符全部转小写,并且在转化后会加上配置的表前缀。
双如:
```php
<?php
namespace App\Model\User;
use PhalApi\Model\NotORMModel as Model;
class Friends extends Model {
}
```
则类App\Model\User\Friends自动匹配的表名为```user_friends```。以下是2.x版本的一些示例:
2.x 的Model类名|对应的文件|自动匹配的表名
---|---|---
App\Model\User|./src/app/Model/User.php|user
App\ModelUser\Friends|./src/app/Model/User/Friends.php|user_friends
App\User\Model\Friends|./src/app/user/Model/Friends.php|friends
App\User\Model\User\Friends|./src/app/user/Model/User/Friends.php|user_friends
但在以下场景或者其他需要手动指定表名的情况,可以重写```PhalApi\Model\NotORMModel::getTableName($id)```方法并手动指定表名。
+ 存在分表
+ Model类名不含有“Model_”
+ 自动匹配的表名与实际表名不符
+ 数据库表使用蛇形命名法而类名使用大写字母分割的方式
如,当Model_User类对应的表名为:my_user表时,可这样重新指定表名:
```php
<?php
namespace App\Model;
use PhalApi\Model\NotORMModel as NotORM;
class User extends NotORM {
protected function getTableName($id) {
return 'my_user';
}
}
```
其中,$id参数用于进行分表的参考主键,只有当存在分表时才需要用到。通常传入的$id是整数,然后对分表的总数进行求余从而得出分表标识。
即存在分表时,需要返回的格式为:表名称 + 下划线 + 分表标识。分表标识通常从0开始,为连续的自然数。
## 在Model内的CURD基本操作
假设对于前面的tbl_user表,有以下数据。
```sql
INSERT INTO `tbl_user` VALUES ('1', 'dogstar', '18', 'oschina', '2015-12-01 09:42:31');
INSERT INTO `tbl_user` VALUES ('2', 'Tom', '21', 'USA', '2015-12-08 09:42:38');
INSERT INTO `tbl_user` VALUES ('3', 'King', '100', 'game', '2015-12-23 09:42:42');
```
下面将结合示例,分别介绍NotORM更为丰富的数据库操作。在开始之前,假定已有:
```php
class User extends NotORM {
public function test() {
$user = $this->getORM();
}
}
```
#### SQL基本语句介绍
+ **SELECT字段选择**
选择单个字段:
```php
// SELECT id FROM `tbl_user`
$user->select('id')
```
选择多个字段:
```php
// SELECT id, name, age FROM `tbl_user`
$user->select('id, name, age')
```
使用字段别名:
```php
// SELECT id, name, MAX(age) AS max_age FROM `tbl_user`
$user->select('id, name, MAX(age) AS max_age')
```
选择全部表字段:
```php
// SELECT * FROM `tbl_user`
$user->select('*')
```
+ **WHERE条件**
单个条件:
```php
// WHERE id = 1
$user->where('id', 1)
$user->where('id = ?', 1)
$user->where(array('id', 1))
```
多个AND条件:
```php
// WHERE id > 1 AND age > 18
$user->where('id > ?', 1)->where('age > ?', 18)
$user->and('id > ?', 1)->and('age > ?', 18)
$user->where('id > ? AND age > ?', 1, 18)
$user->where(array('id > ?' => 1, 'age > ?' => 10))
// WHERE name = 'dogstar' AND age = 18
$user->where(array('name' => 'dogstar', 'age' => 18))
```
多个OR条件:
```php
// WHERE name = 'dogstar' OR age = 18
$user->or('name', 'dogstar')->or('age', 18)
```
嵌套条件:
```php
// WHERE ((name = ? OR id = ?)) AND (note = ?) -- 'dogstar', '1', 'xxx'
// 实现方式1:使用AND拼接
$user->where('(name = ? OR id = ?)', 'dogstar', '1')->and('note = ?', 'xxx')
// 实现方式2:使用WHERE,并顺序传递多个参数
$user->where('(name = ? OR id = ?) AND note = ?', 'dogstar', '1', 'xxx')
// 实现方式3:使用WHERE,并使用一个索引数组顺序传递参数
$user->where('(name = ? OR id = ?) AND note = ?', array('dogstar', '1', 'xxx'))
// 实现方式4:使用WHERE,并使用一个关联数组传递参数
$user->where('(name = :name OR id = :id) AND note = :note',
array(':name' => 'dogstar', ':id' => '1', ':note' => 'xxx'))
```
IN查询:
```php
// WHERE id IN (1, 2, 3)
$user->where('id', array(1, 2, 3))
// WHERE id NOT IN (1, 2, 3)
$user->where('NOT id', array(1, 2, 3))
// WHERE (id, age) IN ((1, 18), (2, 20))
$user->where('(id, age)', array(array(1, 18), array(2, 20)))
```
模糊匹配查询:
```php
// WHERE name LIKE '%dog%'
$user->where('name LIKE ?', '%dog%')
// WHERE name NOT LIKE '%dog%'
$user->where('name NOT LIKE ?', '%dog%')
```
> **温馨提示:**需要模糊匹配时,不可写成:where('name LIKE %?%', 'dog')。
NULL判断查询:
```php
// WHERE (name IS NULL)
$user->where('name', null)
```
非NULL判断查询:
```php
// WHERE (name IS NOT ?) LIMIT 1; -- NULL
$user->where('name IS NOT ?', null)
```
+ **ORDER BY排序**
单个字段升序排序:
```php
// ORDER BY age
$user->order('age')
$user->order('age ASC')
```
单个字段降序排序:
```php
// ORDER BY age DESC
$user->order('age DESC')
```
多个字段排序:
```php
// ORDER BY id, age DESC
$user->order('id')->order('age DESC')
$user->order('id, age DESC')
```
+ **LIMIT数量限制**
限制数量,如查询前10个:
```php
// LIMIT 10
$user->limit(10)
```
分页限制,如从第5个位置开始,查询前10个:
```php
// LIMIT 5, 10
$user->limit(5, 10)
```
+ **GROUP BY和HAVING**
只有GROUP BY,没有HAVING:
```php
// GROUP BY note
$user->group('note')
```
既有GROUP BY,又有HAVING:
```php
// GROUP BY note HAVING age > 10
$user->group('note', 'age > 10')
```
#### CURD之插入操作
插入操作可分为插入单条纪录、多条纪录,或根据条件插入。
操作|说明|示例|备注|是否PhalApi新增
---|---|---|---|---
insert()|插入数据|```$user->insert($data);```|全局方式需要再调用insert_id()获取插入的ID|否
insert_multi()|批量插入|```$user->insert_multi($rows);```|可批量插入|否
insert_update()|插入/更新|接口签名:```insert_update(array $unique, array $insert, array $update = array()```|不存时插入,存在时更新|否
插入单条纪录数据,注意,必须是保持状态的同一个NotORM表实例,方能获取到新插入的行ID,且表必须设置了自增主键ID。
```php
// INSERT INTO tbl_user (name, age, note) VALUES ('PhalApi', 1, 'framework')
$data = array('name' => 'PhalApi', 'age' => 1, 'note' => 'framework');
$user->insert($data);
$id = $user->insert_id();
var_dump($id);
// 输出:新增的ID
int (4)
// 或者使用Model封装的insert()方法
$model = new Model_User();
$id = $model->insert($data);
var_dump($id);
```
批量插入多条纪录数据:
```php
// INSERT INTO tbl_user (name, age, note) VALUES ('A君', 12, 'AA'), ('B君', 14, 'BB'), ('C君', 16, 'CC')
$rows = array(
array('name' => 'A君', 'age' => 12, 'note' => 'AA'),
array('name' => 'B君', 'age' => 14, 'note' => 'BB'),
array('name' => 'C君', 'age' => 16, 'note' => 'CC'),
);
$rs = $user->insert_multi($rows);
var_dump($rs);
// 输出,成功插入的条数
int(3)
```
插入/更新:
```php
// INSERT INTO tbl_user (id, name, age, note) VALUES (8, 'PhalApi', 1, 'framework')
// ON DUPLICATE KEY UPDATE age = 2
$unique = array('id' => 8);
$insert = array('id' => 8, 'name' => 'PhalApi', 'age' => 1, 'note' => 'framework');
$update = array('age' => 2);
$rs = $user->insert_update($unique, $insert, $update);
var_dump($rs);
// 输出影响的行数
```
#### CURD之更新操作
操作|说明|示例|备注|是否PhalApi新增
---|---|---|---|---
update()|更新数据|```$user->where('id', 1)->update($data);```|更新异常时返回false,数据无变化时返回0,成功更新返回1|否
根据条件更新数据:
```php
// UPDATE tbl_user SET age = 2 WHERE (name = 'PhalApi');
$data = array('age' => 2);
$rs = $user->where('name', 'PhalApi')->update($data);
var_dump($rs);
// 输出
int(1) //正常影响的行数
int(0) //无更新,或者数据没变化
boolean(false) //更新异常、失败
```
在使用update()进行更新操作时,如果更新的数据和原来的一样,则会返回0(表示影响0行)。这时,会和更新失败(同样影响0行)混淆。但NotORM是一个优秀的类库,它已经提供了优秀的解决文案。我们在使用update()时,只须了解这两者返回结果的微妙区别即可。因为失败异常时,返回false;而相同数据更新会返回0。即:
+ 1、更新相同的数据时,返回0,严格来说是:int(0)
+ 2、更新失败时,如更新一个不存在的字段,返回false,即:bool(false)
用代码表示,就是:
```php
$rs = DI()->notorm->user->where('id', $userId)->update($data);
if ($rs >= 1) {
// 成功
} else if ($rs === 0) {
// 相同数据,无更新
} else if ($rs === false) {
// 更新失败
}
```
更新数据,进行加1操作:
```php
// UPDATE tbl_user SET age = age + 1 WHERE (name = 'PhalApi')
$rs = $user->where('name', 'PhalApi')->update(array('age' => new NotORM_Literal("age + 1")));
var_dump($rs);
// 输出影响的行数
```
#### CURD之查询操作
查询操作主要有获取一条纪录、获取多条纪录以及聚合查询等。
操作|说明|示例|备注|是否PhalApi新增
---|---|---|---|---
fetch()|循环获取每一行|```while($row = $user->fetch()) { ... ... }```||否
fetchOne()|只获取第一行|```$row = $user->where('id', 1)->fetchOne();```|等效于fetchRow()|是
fetchRow()|只获取第一行|```$row = $user->where('id', 1)->fetchRow();```|等效于fetchOne()|是
fetchPairs()|获取键值对|```$row = $user->fetchPairs('id', 'name');```|第二个参数为空时,可取多个值,并且多条纪录|否
fetchAll()|获取全部的行|```$rows = $user->where('id', array(1, 2, 3))->fetchAll();```|等效于fetchRows()|是
fetchRows()|获取全部的行|```$rows = $user->where('id', array(1, 2, 3))->fetchRows();```|等效于fetchAll()|是
queryAll()|复杂查询下获取全部的行,默认下以主键为下标|```$rows = $user->queryAll($sql, $parmas);```|等效于queryRows()|是
queryRows()|复杂查询下获取全部的行,默认下以主键为下标|```$rows = $user->queryRows($sql, $parmas);```|等效于queryAll()|是
count()|查询总数|```$total = $user->count('id');```|第一参数可省略|否
min()|取最小值|```$minId = $user->min('id');```||否
max()|取最大值|```$maxId = $user->max('id');```||否
sum()|计算总和|```$sum = $user->sum('age');```||否
循环获取每一行,并且同时获取多个字段:
```php
// SELECT id, name FROM tbl_user WHERE (age > 18);
$user = $user->select('id, name')->where('age > 18');
while ($row = $user->fetch()) {
var_dump($row);
}
// 输出
array(2) {
["id"]=>
string(1) "2"
["name"]=>
string(3) "Tom"
}
array(2) {
["id"]=>
string(1) "3"
["name"]=>
string(4) "King"
}
... ...
```
循环获取每一行,并且只获取单个字段。需要注意的是,指定获取的字段,必须出现在select里,并且返回的不是数组,而是字符串。
```php
// SELECT id, name FROM tbl_user WHERE (age > 18);
$user = $user->select('id, name')->where('age > 18');
while ($row = $user->fetch('name')) {
var_dump($row);
}
// 输出
string(3) "Tom"
string(4) "King"
... ...
```
注意!以下是错误的用法。还记得前面所学的NotORM状态的保持吗?因为这里每次循环都会新建一个NotORM表实例,所以没有保持前面的查询状态,从而死循环。
```php
while ($row = DI()->notorm->user->select('id, name')->where('age > 18')->fetch('name')) {
var_dump($row);
}
```
只获取第一行,并且获取多个字段,等同于fetchRow()操作。
```php
// SELECT id, name FROM tbl_user WHERE (age > 18) LIMIT 1;
$rs = $user->select('id, name')->where('age > 18')->fetchOne();
var_dump($rs);
// 输出
array(2) {
["id"]=>
string(1) "2"
["name"]=>
string(3) "Tom"
}
```
只获取第一行,并且只获取单个字段,等同于fetchRow()操作。
```php
var_dump($user->fetchOne('name'));
// 输出
string(3) "Tom"
```
获取键值对,并且获取多个字段:
```
// SELECT id, name, age FROM tbl_user LIMIT 2;
$rs = $user->select('name, age')->limit(2)->fetchPairs('id'); //指定以ID为KEY
var_dump($rs);
// 输出
array(2) {
[1]=>
array(3) {
["id"]=>
string(1) "1"
["name"]=>
string(7) "dogstar"
["age"]=>
string(2) "18"
}
[2]=>
array(3) {
["id"]=>
string(1) "2"
["name"]=>
string(3) "Tom"
["age"]=>
string(2) "21"
}
}
```
获取键值对,并且只获取单个字段。注意,这时的值不是数组,而是字符串。
```php
// SELECT id, name FROM tbl_user LIMIT 2
var_dump($user->limit(2)->fetchPairs('id', 'name')); //通过第二个参数,指定VALUE的列
// 输出
array(2) {
[1]=>
string(7) "dogstar"
[2]=>
string(3) "Tom"
}
```
获取全部的行,相当于fetchRows()操作。
```php
// SELECT * FROM tbl_user
var_dump($user->fetchAll());
// 输出全部表数据,结果略
```
使用原生SQL语句进行查询,并获取全部的行:
```php
// SELECT name FROM tbl_user WHERE age > :age LIMIT 1
$sql = 'SELECT name FROM tbl_user WHERE age > :age LIMIT 1';
$params = array(':age' => 18);
$rs = $user->queryAll($sql, $params);
var_dump($rs);
// 输出
array(1) {
[0]=>
array(1) {
["name"]=>
string(3) "Tom"
}
}
// 除了使用上面的关联数组传递参数,也可以使用索引数组传递参数
$sql = 'SELECT name FROM tbl_user WHERE age > ? LIMIT 1';
$params = array(18);
// 也使用queryRows()别名
$rs = $user->queryRows($sql, $params);
```
在使用```queryAll()queryRows()```进行原生SQL操作时,需要特别注意:
+ 1、需要手动填写完整的表名字,包括分表标识,并且需要通过任意表实例来运行
+ 2、尽量使用参数绑定,而不应直接使用参数来拼接SQL语句,慎防SQL注入攻击
下面是不好的写法,很有可能会导致SQL注入攻击
```php
// 存在SQL注入的写法
$id = 1;
$sql = "SELECT * FROM tbl_demo WHERE id = $id";
$rows = $this->getORM()->queryAll($sql);
```
对于外部不可信的输入数据,应改用参数传递的方式。
```php
// 使用参数绑定方式
$id = 1;
$sql = "SELECT * FROM tbl_demo WHERE id = ?";
$rows = $this->getORM()->queryAll($sql, array($id));
```
查询总数:
```php
// SELECT COUNT(id) FROM tbl_user
var_dump($user->sum('id'));
// 输出
string(3) "3"
```
查询最小值:
```php
// SELECT MIN(age) FROM tbl_user
var_dump($user->min('age'));
// 输出
string(2) "18"
```
查询最大值:
```php
// SELECT MAX(age) FROM tbl_user
var_dump($user->max('age'));
// 输出
string(3) "100"
```
计算总和:
```php
// SELECT SUM(age) FROM tbl_user
var_dump($user->sum('age'));
// 输出
string(3) "139"
```
#### CURD之删除操作
操作|说明|示例|备注|是否PhalApi新增
---|---|---|---|---
delete()|删除|```$user->where('id', 1)->delete();```|禁止无where条件的删除操作|否
按条件进行删除,并返回影响的行数:
```php
// DELETE FROM tbl_user WHERE (id = 404);
$user->where('id', 404)->delete();
```
请特别注意,PhalApi禁止全表删除操作。即如果是全表删除,将会被禁止,并抛出异常。如:
```php
// Exception: sorry, you can not delete the whole table
$user->delete();
```
## 事务操作、关联查询和其他操作
### 事务操作
以下是事务操作的一个示例。
```php
// Step 1: 开启事务
\PhalApi\DI()->notorm->beginTransaction('db_demo');
// Step 2: 数据库操作
\PhalApi\DI()->notorm->user->insert(array('name' => 'test1'));
\PhalApi\DI()->notorm->user->insert(array('name' => 'test2'));
// Step 3: 提交事务/回滚
\PhalApi\DI()->notorm->commit('db_demo');
//\PhalApi\DI()->notorm->rollback('db_demo');
```
### 关联查询
对于关联查询,简单的关联可使用NotORM封装的方式,而复杂的关联,如多个表的关联查询,则可以使用PhalApi封装的接口。
如果是简单的关联查询,可以使用NotORM支持的写法,这样的好处在于我们使用了一致的开发,并且能让PhalApi框架保持分布式的操作方式。需要注意的是,关联的表仍然需要在同一个数据库。
以下是一个简单的示例。假设我们有这样的数据:
```sql
INSERT INTO `phalapi_user` VALUES ('1', 'wx_edebc', 'dogstar', '***', '4CHqOhe1', '1431790647', '');
INSERT INTO `phalapi_user_session_0` VALUES ('1', '1', 'ABC', '', '0', '0', '0', null);
```
那么对应关联查询的代码如下面:
```php
// 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
$rs = \PhalApi\DI()->notorm->user_session_0
->select('expires_time, user.username, user.nickname')
->where('token', 'ABC')
->fetchRow();
var_dump($rs);
```
会得到类似这样的输出:
```php
array(3) {
["expires_time"]=>
string(1) "0"
["username"]=>
string(35) "wx_edebc"
["nickname"]=>
string(10) "dogstar"
}
```
这样,我们就可以实现关联查询的操作。按照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')```这一行调用将会NotORM自动产生关联操作,而ON的字段,则是这个字段关联你配置的表结构,外键默认为:表名_id 。
如果是复杂的关联查询,则是建议使用原生的SQL语句,但仍然可以保持很好的写法,如这样一个示例:
```php
$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());
var_dump($rows);
```
如前面所述,这里需要手动填写完整的表名,以及慎防SQL注入攻击。
### 其他数据库操作
有时,我们还需要进行一些其他的数据库操作,如创建表、删除表、添加表字段等。对于需要进行的数据库操作,而上面所介绍的方法未能满足时,可以使用更底层更通用的接口,即:```NotORM_Result::query($query, $parameters)```。
例如,删除一张表。
```php
$user->query('DROP TABLE tbl_user', array());
```
## 数据库分表
### 分表的配置
假设有以下多个数据库表,它们的表结构一样。
数据库表|数据库实例
---|---
tbl_demo|db_master
tbl_demo_0|db_master
tbl_demo_1|db_master
tbl_demo_2|db_master
为了使用分表存储,可以修改数据库表的配置,让它支持分表的情况。
```php
return array(
'tables' => array(
'demo' => array(
'prefix' => 'tbl_',
'key' => 'id',
'map' => array(
array('db' => 'db_master'),
array('start' => 0, 'end' => 2, 'db' => 'db_master'),
),
),
),
);
```
上面配置map选项中```array('db' => 'master')```用于指定缺省主表使用master数据库实例,而下一组映射关系则是用于配置连续在同一台数据库实例的分表区间,即tbl_demo_0、tbl_demo_1、tbl_demo_2都使用了master数据库实例。
> 温馨提示:当分表找不到时,PhalApi会自动退化使用缺省主表。
### Model子类实现分表逻辑
假设分别的规则是根据ID对3进行求余。当需要使用分表时,在使用Model基类的情况下,可以通过重写```PhalApi\Model\NotORMModel::getTableName($id)```实现相应的分表规则。
```php
<?php
namespace App\Model;
use PhalApi\Model\NotORMModel as NotORM;
class Demo extends NotORM {
protected function getTableName($id) {
$tableName = 'demo';
if ($id !== null) {
$tableName .= '_' . ($id % 3);
}
return $tableName;
}
}
```
然后,便可使用之前一样的CURD基本操作,但框架会自动匹配分表的映射。例如:
```php
$model = new App\Model\Demo();
$row = $model->get('3', 'id'); // 使用分表tbl_demo_0
$row = $model->get('10', 'id'); // 使用分表tbl_demo_1
$row = $model->get('2', 'id'); // 使用分表tbl_demo_2
```
回到使用Model基类的上下文,更进一步,我们可以通过```$this->getORM($id)```来获取分表的实例从而进行分表的操作。如:
```php
<?php
namespace App\Model;
use PhalApi\Model\NotORMModel as NotORM;
class Demo extends NotORM {
public function getNameById($id) {
$row = $this->getORM($id)->select('name')->fetchRow();
return !empty($row) ? $row['name'] : '';
}
}
```
通过传入不同的$id,即可获取相应的分表实例。
### 自动生成SQL建表语句
把数据库表的基本建表语句保存到./data目录下,文件名与数据库表名相同,后缀统一为“.sql”。如这里的./data/demo.sql文件。
```sql
`name` varchar(11) DEFAULT NULL,
```
需要注意的是,这里说的基本建表语句是指:仅是这个表所特有的字段,排除已固定公共有的自增主键id、扩展字段ext_data和CREATE TABLE关键字等。
然后可以使用phalapi-buildsqls脚本命令,快速自动生成demo缺省主表和全部分表的建表SQL语句。如下:
```bash
$ ./bin/phalapi-buildsqls ./config/dbs.php demo
```
正常情况下,会生成类似以下的SQL语句:
```sql
CREATE TABLE `demo` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(11) DEFAULT NULL,
`ext_data` text COMMENT 'json data here',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `tpl_demo_0` ... ...;
CREATE TABLE `tpl_demo_1` ... ...;
CREATE TABLE `tpl_demo_2` ... ...;
```
在将上面的SQL语句导入数据库后,或者手动创建数据库表后,便可以像之前那样操作数据库了。