寫一個「特殊」的查詢構造器 - (7、DML 語句、事務)

查詢語句 (DQL) 的構造功能開發完畢,咱們再給查詢構造器增長一些對 DML (Data Manipulation Language) 語句的支持,如簡單的 insert、update、delete 操做。php

insert

咱們先回顧下 PDO 原生的 insert 操做怎麼進行:html

// 預編譯
$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

數據插入mysql

和查詢語句的執行過程並無太大差異,只是語法不一樣。回想第二篇,咱們新建了 _buildQuery() 方法去構造最終的 SQL,對於 insert,咱們在基類新建 _buildInsert() 方法:sql

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() 方法的參數是一個鍵值數組,用來傳入要插入的字段、值映射。默認返回被影響的行數 (比較通用)。post

測試測試

試着插入一條數據吧:fetch

$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

相關文章
相關標籤/搜索