写一个“特殊”的查询构造器 - (七、DML 语句、事务)

查询语句 (DQL) 的构造功能开发完毕,我们再给查询构造器增加一些对 DML (Data Manipulation Language) 语句的支持,如简单的 insert、update、delete 操作。

insert

我们先回顾下 PDO 原生的 insert 操作怎么进行:

// 预编译
$pdoSt = $pdo->prepare("INSERT INTO test_table ('username', 'age') VALUES (:username, :age);");
// 绑定参数
$pdoSt->bindValue(':username', 'Jack', PDO::PARAM_STR)
$pdoSt->bindValue(':age', 18, PDO::PARAM_INT)
// 执行
$pdoSt->execute(); 
// 获取执行数据
$count = $pdoSt->rowCount(); // 返回被影响的行数
$lastId = $pdo->lastInsertId(); // 获取最后插入行的 ID

数据插入

和查询语句的执行过程并没有太大差别,只是语法不同。回想第二篇,我们新建了 _buildQuery() 方法去构造最终的 SQL,对于 insert,我们在基类新建 _buildInsert() 方法:

protected function _buildInsert()
{
    $this->_prepare_sql = 'INSERT INTO '.$this->_table.$this->_insert_str;
}

基类添加 _insert_str 属性:

protected $_insert_str = '';

修改 _reset() 函数,将 _insert_str 属性的初始化过程添加进去:

protected function _reset()
{
    $this->_table = '';
    $this->_prepare_sql = '';
    $this->_cols_str = ' * ';
    $this->_where_str = '';
    $this->_orderby_str = '';
    $this->_groupby_str = '';
    $this->_having_str = '';
    $this->_join_str = '';
    $this->_limit_str = '';
    $this->_insert_str = ''; // 重置 insert 语句
    $this->_bind_params = [];
}

基类中添加 insert() 方法:

public function insert(array $data)
{
    // 构建字符串
    $field_str = '';
    $value_str = '';
    foreach ($data as $key => $value) {
        $field_str .= ' '.self::_wrapRow($key).',';
        $plh = self::_getPlh(); // 生产占位符
        $this->_bind_params[$plh] = $value; //保存绑定数据
        $value_str .= ' '.$plh.',';
    }
    // 清除右侧多余的逗号
    $field_str = rtrim($field_str, ',');
    $value_str = rtrim($value_str, ',');

    $this->_insert_str = ' ('.$field_str.') VALUES ('.$value_str.') ';
    // 构造 insert 语句
    $this->_buildInsert();
    // 执行
    $this->_execute();
    // 获取影响的行数
    return $this->_pdoSt->rowCount();
}

对上述代码,我们申明了 insert() 方法的参数是一个键值数组,用来传入要插入的字段、值映射。默认返回被影响的行数 (比较通用)。

测试

试着插入一条数据吧:


$insert_data = [
    'username' => 'jack',
    'age'      => 18,
];

$results = $driver->table('test_table')->insert($insert_data);

获取最后插入行的 ID

当一个表中有自增 id 且为主键时,这个 id 可以被看作区分数据的唯一标识。而在插入一条数据后获取这条新增数据的 id 也是常见的业务需求。

PDO 提供了一个简单的获取最后插入行的 ID 的方法 PDO::lastInsertId() 供我们使用。

基类添加 insertGetLastId() 方法:

public function insertGetLastId(array $data)
{
    $this->insert($data);

    return $this->_pdo->lastInsertId();
}

测试:

$insert_data = [
    'username' => 'jack',
    'age'      => 18,
];

$lastId = $driver->table('test_table')->insertGetLastId($insert_data);

个体差异

然而,上述的 insertGetLastId() 方法在 PostgreSql 中并不奏效。PostgreSql 中,使用 PDO::lastInsertId() 获取结果需要传入正确的自增序列名 (PostgreSQL 中创建表时,如果使用 serial 类型,默认生成的自增序列名为:表名 + _ + 字段名 + _ + seq)。【1】

但是这个方式并不好用,因为访问 insertGetLastId() 方法时必须手动传入这个序列名称,这样 insertGetLastId() 方法对底层的依赖严重,比如当底层驱动从 postgresql 换到 mysql 时,需要更改上层应用。而我们希望无论是 mysql 还是 postgresql,上层应用调用 insertGetLastId() 方法时是无差别的,即底层对上层透明。

为了解决这个问题,就需要用到 postgresql 的 returning 语法了。postgresql 中 insert、update 和 delete 操作都有一个可选的 returning 子句,可以指定最后执行的字段进行返回,返回的数据可以像 select 一样取结果。【2】

对于我们返回最后插入行的 ID 的需求,只需 returning id 就好。

当然,基类的 insertGetLastId() 方法对于 postgresql 而言已经无效了,我们在 Pgsql 驱动类中重写 insertGetLastId() 方法:

public function insertGetLastId(array $data)
{
    // 构建语句字符串、绑定数据
    $field_str = '';
    $value_str = '';
    foreach ($data as $key => $value) {
        $field_str .= ' '.self::_wrapRow($key).',';
        $plh = self::_getPlh();
        $this->_bind_params[$plh] = $value;
        $value_str .= ' '.$plh.',';
    }

    $field_str = rtrim($field_str, ',');
    $value_str = rtrim($value_str, ',');
    // 使用 returning 子句返回 id
    $this->_insert_str = ' ('.$field_str.') VALUES ('.$value_str.') RETURNING id ';
    // execute
    $this->_buildInsert();
    $this->_execute();
    // 使用 returning 子句后,可以像使用 SELECT 一样获取一个 returning 指定字段的结果集。 
    $result = $this->_pdoSt->fetch(PDO::FETCH_ASSOC);
    // 返回 id
    return $result['id'];
}

OK,我们再来测试看看,是不是就好用了呢?

update

做完 insert,update 就很简单了,不同的是为了防止全局更新的失误发生,update 构造时强行要求使用 where 子句。

同样的,添加 _update_str 属性,修改 _reset() 函数:

protected $_update_str = '';

...

protected function _reset()
{
    $this->_table = '';
    $this->_prepare_sql = '';
    $this->_cols_str = ' * ';
    $this->_where_str = '';
    $this->_orderby_str = '';
    $this->_groupby_str = '';
    $this->_having_str = '';
    $this->_join_str = '';
    $this->_limit_str = '';
    $this->_insert_str = '';
    $this->_update_str = '';
    $this->_bind_params = [];
}

构造 update 语句的方法:

protected function _buildUpdate()
{
    $this->_prepare_sql = 'UPDATE '.$this->_table.$this->_update_str.$this->_where_str;
}

基类中添加 update() 方法:

public function update(array $data)
{
    // 检测有没有设置 where 子句
    if(empty($this->_where_str)) {
        throw new \InvalidArgumentException("Need where condition");
    }
    // 构建语句、参数绑定
    $this->_update_str = ' SET ';
    foreach ($data as $key => $value) {
        $plh = self::_getPlh();
        $this->_bind_params[$plh] = $value;
        $this->_update_str .= ' '.self::_wrapRow($key).' = '.$plh.',';
    }

    $this->_update_str = rtrim($this->_update_str, ',');

    $this->_buildUpdate();
    $this->_execute();
    // 返回影响的行数
    return $this->_pdoSt->rowCount();
}

更新数据示例:

$update_data = [
    'username' => 'lucy',
    'age'      => 22,
];

$results = $driver->table('test_table')
            ->where('username', 'jack')
            ->update($update_data);

delete

相比 insert、update,delete 语句更为简单,只需 where 子句即可。和 update 一样,需要防止误操作删除所有数据。

构造 delete 语句的方法:

protected function _buildDelete()
{
    $this->_prepare_sql = 'DELETE FROM '.$this->_table.$this->_where_str;
}

基类中添加 delete() 方法:

public function delete()
{
    // 检测有没有设置 where 子句
    if(empty($this->_where_str)) {
        throw new \InvalidArgumentException("Need where condition");
    }

    $this->_buildDelete();
    $this->_execute();

    return $this->_pdoSt->rowCount();
}

删除数据示例:

$results = $driver->table('test_table')
            ->where('age', 18)
            ->delete();

事务

既然有了 DML 操作,那么就少不了事务。对于事务,我们可以直接使用 PDO 提供的 PDO::beginTransaction()PDO::commit()PDO::rollBack()PDO::inTransaction() 方法来实现。

基类添加 beginTrans() 方法:

// 开始事务
public function beginTrans()
{
    try {
        return $this->_pdo->beginTransaction();
    } catch (PDOException $e) {
        // 断线重连
        if ($this->_isTimeout($e)) {

            $this->_closeConnection();
            $this->_connect();

            try {
                return $this->_pdo->beginTransaction();
            } catch (PDOException $e) {
                throw $e;
            }

        } else {
            throw $e;
        }
    }
}

注:因为 PDO::beginTransaction() 也是和 PDO::prepare() 一样会连接数据库的方法,所以需要做断线重连的操作。

commitTrans() 方法:

// 提交事务
public function commitTrans()
{
    return $this->_pdo->commit(); 
}

rollBackTrans() 方法:

// 回滚事务
public function rollBackTrans()
{
    if ($this->_pdo->inTransaction()) {
        // 如果已经开始了事务,则运行回滚操作
        return $this->_pdo->rollBack();
    }
}

事务使用示例:

// 注册事务
$driver->beginTrans();
$results = $driver->table('test_table')
            ->where('age', 18)
            ->delete();
$driver->commitTrans(); // 确认删除

// 回滚事务
$driver->beginTrans();
$results = $driver->table('test_table')
            ->where('age', 18)
            ->delete();
$driver->rollBackTrans(); // 撤销删除

参考

【1】PHP Manual - PDO::lastInsertId

【2】PostgreSQL Documentation - Returning Data From Modified Rows

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容