需求分析
本篇我们将通过 Swoole 实现一个自带连接池的 MySQL 查询器:
- 支持通过链式调用构造并执行 SQL 语句;
- 支持连接池技术;
- 支持多协程事务并发执行(协程安全性);
- 支持连接对象的健康检测;
- 支持连接对象断线重连;
- 程序需要可扩展,为未来的改造留好扩展点;
完整项目地址:协程版 MySQL 查询器
(注:该项目并非示例项目,而是生产可用的,已经在公司内部稳定使用。)
使用示例
- 查询:
$query->select(['uid', 'name'])
->from('users u')
->join('auth_users au', "u.uid=au.uid")
->where(['uid' => $uid])
->groupBy("u.phone")
->having("count(u.phone)>1")
->orderBy("u.uid desc")
->limit(10, 0)
->list();
- 插入:
$query->insert('users')
->values(
[
[
'name' => 'linvanda',
'phone' => '18687664562',
'nickname' => '林子',
],
[
'name' => 'xiake',
'phone' => '18989876543',
'nickname' => '侠客',
],
]
)->execute();// 这里是批量插入,不需要批量插入的话,传入一维数组即可
// 延迟插入
$query->insert('users')
->delayed()
->values(
[
'name' => 'linvanda',
'phone' => '18687664562',
'nickname' => '林子',
]
)->execute();
- 更新:
$query->update('users u')
->join('auth_users au', "u.uid=au.uid")
->set(['u.name' => '粽子'])
->where("u.uid=:uid", ['uid' => 123])
->execute();
- 删除:
$query->delete('users')
->where("uid=:uid", ['uid' => 123])
->execute();
- 事务:
$query->begin();
$query->update('users u')
->join('auth_users au', "u.uid=au.uid")
->set(['u.name' => '粽子'])
->where("u.uid=:uid", ['uid' => 123])
->execute();
...
$query->commit();
模块设计
- 查询模块:
- 查询器(Query,入口)
- SQL构造器(Builder)
- 事务模块:
- 事务接口(ITransaction)
- 协程版事务类(CoTransaction)
- 协程上下文(TContext)
- 连接池模块:
- 连接池接口(IPool)
- 协程连接池类(CoPool)
- 数据库连接(驱动)模块:
- 连接接口(IConnector)
- 连接生成器接口(IConnectorBuilder)
- 协程连接类(CoConnector)
- 协程连接生成器(CoConnectorBuilder)
- 数据库连接配置类(DBConfig)
- 数据库连接(统计)信息类(ConnectorInfo)
我们希望通过统一的入口对外提供服务,将复杂性隐藏在内部。该统一入口由查询模块提供。该模块由查询器和SQL 构造器构成,其中查询器作为外界唯一入口,而构造器是一个 Trait,因为这样可以让外界通过查询器入口直接使用构造器提供的 SQL 组装功能。
查询器通过事务模块执行 SQL。这里的事务有两个层面含义:数据库操作的事务性(显式或隐式事务,由 CoTransaction
类保障),以及多协程下的执行环境隔离性(由 TContext
类保障)。
事务模块需要通过数据库连接对象执行具体的 SQL。连接对象由连接池模块提供。
连接池模块维护(创建、回收、销毁)数据库连接对象,具体是通过数据库连接模块的连接生成器生成新数据库连接。
模块之间依赖于接口而非具体实现:查询模块依赖事务模块的 ITransaction
接口;事务模块依赖连接池模块的 IPool
接口和数据库连接模块的 IConnector
接口;连接池模块依赖数据库连接模块的 IConnectorBuilder
接口。
UML 类图
下面,我们分模块具体讲解。
入口
由查询模块对外提供统一的使用入口。查询模块由两部分构成:查询器和 SQL 构造器。为了让调用方可以直接通过查询器来构造 SQL(而不用先实例化一个构造器构造 SQL 然后传给查询器),我将构造器设计成 Trait 供查询器 Query 使用。
我们先看看查询器类 Query:
class Query
{
use Builder;
public const MODEL_READ = 'read';
public const MODEL_WRITE = 'write';
private $transaction;
public function __construct(ITransaction $transaction)
{
$this->transaction = $transaction;
}
/**
* 开启事务
*/
public function begin($model = 'write'): bool
{
return $this->transaction->begin($model);
}
/**
* 提交事务
*/
public function commit(): bool
{
return $this->transaction->commit();
}
/**
* 回滚事务
*/
public function rollback(): bool
{
return $this->transaction->rollback();
}
/**
* 便捷方法:列表查询
*/
public function list(): array
{
$list = $this->transaction->command(...$this->compile());
if ($list === false) {
throw new DBException($this->lastError(), $this->lastErrorNo());
}
return $list;
}
/**
* 便捷方法:查询一行记录
*/
public function one(): array
{
$list = $this->transaction->command(...$this->limit(1)->compile());
if ($list === false) {
throw new DBException($this->lastError(), $this->lastErrorNo());
}
if ($list) {
return $list[0];
}
return [];
}
...
/**
* 执行 SQL
* 有两种方式:
* 1. 调此方法时传入相关参数;
* 2. 通过 Builder 提供的 Active Record 方法组装 SQL,调此方法(不传参数)执行并返回结果
*/
public function execute(string $preSql = '', array $params = [])
{
if (!func_num_args()) {
$result = $this->transaction->command(...$this->compile());
} else {
$result = $this->transaction->command(...$this->prepareSQL($preSql, $params));
}
if ($result === false) {
throw new DBException($this->lastError(), $this->lastErrorNo());
}
return $result;
}
public function lastInsertId()
{
return $this->transaction->lastInsertId();
}
public function affectedRows()
{
return $this->transaction->affectedRows();
}
}
该入口类做了以下几件事情:
- 提供 list()、one()、page()、execute() 等方法执行 SQL 语句,其内部是通过 transaction 实现的;
- 通过 Builder 这个 Trait 对外提供 SQL 构造功能;
- 委托 transaction 实现事务功能;
我们再简单看下 Builder 的实现:
Trait Builder
{
...
public function select($fields = null)
{
if ($this->type) {
return $this;
}
$this->type = 'select';
$this->fields($fields);
return $this;
}
/**
* 预处理 SQL
* @param string $sql 格式:select * from t_name where uid=:uid
* @param array $params 格式:['uid' => $uid]
* @return array 输出格式:sql: select * from t_name where uid=?,params: [$uid]
* @throws \Exception
*/
private function prepareSQL(string $sql, array $params)
{
$sql = trim($sql);
if (!$params) {
return [$sql, []];
}
preg_match_all('/:([^\s;]+)/', $sql, $matches);
if (!($matches = $matches[1])) {
return [$sql, []];
}
if (count($matches) !== count($params)) {
throw new \Exception("SQL 占位数与参数个数不符。SQL:$sql,参数:" . print_r($params, true));
}
$p = [];
foreach ($matches as $flag) {
if (!array_key_exists($flag, $params)) {
throw new \Exception("SQL 占位符与参数不符。SQL:$sql,参数:" . print_r($params, true));
}
$value = $params[$flag];
if ($this->isExpression($value)) {
$sql = preg_replace("/:$flag(?=\s|$)/", $value, $sql);
} else {
$p[] = $value;
}
}
$sql = preg_replace('/:[-a-zA-Z0-9_]+/', '?', $sql);
return [$sql, $p];
}
/**
* 编译
* 目前仅支持 select,update,insert,replace,delete
* @param bool $reset 编译后是否重置构造器
* @return array [$preSql, $params]
*/
private function compile(bool $reset = true)
{
if (!$this->type) {
return ['', []];
}
$method = 'compile' . ucfirst($this->type);
if (method_exists($this, $method)) {
$this->rawSqlInfo = $this->$method();
if ($reset) {
$this->reset();
}
return $this->rawSqlInfo;
}
return ['', []];
}
private function c