Swoole 实战:MySQL 查询器的实现(协程连接池版)

需求分析

本篇我们将通过 Swoole 实现一个自带连接池的 MySQL 查询器:

  1. 支持通过链式调用构造并执行 SQL 语句;
  2. 支持连接池技术;
  3. 支持多协程事务并发执行(协程安全性);
  4. 支持连接对象的健康检测;
  5. 支持连接对象断线重连;
  6. 程序需要可扩展,为未来的改造留好扩展点;

完整项目地址:协程版 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();

模块设计

  1. 查询模块:
    • 查询器(Query,入口)
    • SQL构造器(Builder)
  2. 事务模块:
    • 事务接口(ITransaction)
    • 协程版事务类(CoTransaction)
    • 协程上下文(TContext)
  3. 连接池模块:
    • 连接池接口(IPool)
    • 协程连接池类(CoPool)
  4. 数据库连接(驱动)模块:
    • 连接接口(IConnector)
    • 连接生成器接口(IConnectorBuilder)
    • 协程连接类(CoConnector)
    • 协程连接生成器(CoConnectorBuilder)
    • 数据库连接配置类(DBConfig)
    • 数据库连接(统计)信息类(ConnectorInfo)

我们希望通过统一的入口对外提供服务,将复杂性隐藏在内部。该统一入口由查询模块提供。该模块由查询器SQL 构造器构成,其中查询器作为外界唯一入口,而构造器是一个 Trait,因为这样可以让外界通过查询器入口直接使用构造器提供的 SQL 组装功能。

查询器通过事务模块执行 SQL。这里的事务有两个层面含义:数据库操作的事务性(显式或隐式事务,由 CoTransaction 类保障),以及多协程下的执行环境隔离性(由 TContext 类保障)。

事务模块需要通过数据库连接对象执行具体的 SQL。连接对象由连接池模块提供。

连接池模块维护(创建、回收、销毁)数据库连接对象,具体是通过数据库连接模块的连接生成器生成新数据库连接。

模块之间依赖于接口而非具体实现:查询模块依赖事务模块的 ITransaction 接口;事务模块依赖连接池模块的 IPool 接口和数据库连接模块的 IConnector 接口;连接池模块依赖数据库连接模块的 IConnectorBuilder 接口。

UML 类图

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
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

林子er

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值