寫一個「特殊」的查詢構造器 - (6、關聯)

關聯查詢是關係型數據庫典型的查詢語句,根據兩個或多個表中的列之間的關係,從這些表中查詢數據。在 SQL 標準中使用 JOIN 和 ON 關鍵字來實現關聯查詢。php

Join 子句

join 子句的構造並不難,注意事項就是關聯查詢的注意事項:mysql

  • 寫對語法和關聯的條件
  • 使用 table.field 模式防止字段重名

基類中新建 join() 方法:git

// $table 要關聯的表
// $one   做爲關聯條件的一個表的字段
// $two   做爲關聯條件的另外一個表的字段
// $type  關聯模式 inner、left、right
public function join($table, $one, $two, $type = 'INNER')
{
    // 判斷模式是否合法
    if( ! in_array($type, ['INNER', 'LEFT', 'RIGHT'])) {
        throw new \InvalidArgumentException("Error join mode");
    }
    // 構建 join 子句字符串
    $this->_join_str .= ' '.$type.' JOIN '.self::_wrapRow($table).
        ' ON '.self::_wrapRow($one).' = '.self::_wrapRow($two);
    return $this;
}

leftJoin() 和 rightJoin() 方法:github

public function leftJoin($table, $one, $two)
{
    return $this->join($table, $one, $two, 'LEFT');
}

public function rightJoin($table, $one, $two)
{
    return $this->join($table, $one, $two, 'RIGHT');
}
注:Sqlite 是不支持 right join 的,因此 rightJoin() 方法在 Sqlite 驅動類中無效。

構建 SELECT student.name, class.name FROM student INNER JOIN class ON student.class_id = class.id;正則表達式

$results = $driver->table('student')
            ->select('student.name', 'class.name')
            ->join('class', 'student.class_id', 'class.id')
            ->get();

表前綴

爲何要有表前綴

之前不少數據表放在一個數據庫中的時候,須要表前綴來區分功能。雖然如今這樣的狀況已經不多,可是對於查詢構造器而言,仍是要提供一個方便的方法來對錶前綴進行設置,特別是當你沒有權限修改表名的時候。sql

自動添加表前綴的方法

對於有表前綴的表,咱們並不想每次都寫一個前綴,這樣會致使前綴更改後,應用層要跟着修改。因此咱們將表前綴做爲一個配置參數傳入查詢構造器,在查詢構造器的底層進行自動前綴添加。數據庫

表前綴的配置,假設表前綴爲 'test_' :socket

// 以 mysql 爲例
$config = [
    'host'        => 'localhost',
    'port'        => '3306',
    'user'        => 'username',
    'password'    => 'password',
    'dbname'      => 'dbname',
    'charset'     => 'utf8',
    'prefix'      => 'test_',
    'timezone'    => '+8:00',
    'collection'  => 'utf8_general_ci',
    'strict'      => false,
    // 'unix_socket' => '/var/run/mysqld/mysqld.sock',
];
$db = new Mysql($config);

進行自動添加前綴的方法:函數

protected function _wrapTable($table)
{
    // 構造函數傳入的配置中有前綴參數嗎?
    $prefix = array_key_exists('prefix', $this->_config) ?
            $this->_config['prefix'] : '';
    // 拼接前綴
    return $prefix.$table;
}

修改 table() 方法:單元測試

public function table($table)
{
    // 自動添加前綴
    $this->_table = self::_wrapRow($this->_wrapTable($table));

    return $this;
}

join 子句中也涉及到表,因此修改 join() 方法:

public function join($table, $one, $two, $type = 'INNER')
{
    if( ! in_array($type, ['INNER', 'LEFT', 'RIGHT'])) {
        throw new \InvalidArgumentException("Error join mode");
    }
    // 添加表前綴
    $table = $this->_wrapTable($table);
    
    $this->_join_str .= ' '.$type.' JOIN '.self::_wrapRow($table).
        ' ON '.self::_wrapRow($one).' = '.self::_wrapRow($two);
    return $this;
}

table.field 模式的表前綴添加

增長了表前綴後,咱們會發現一個問題:

使用 table()、join() 方法傳入的表能夠自動的添加前綴,可是 table.field 格式中的表無法自動添加前綴,如上面的 join('class', 'student.class_id', 'class.id'),咱們總不能每次都寫成 join('class', 'test_student.class_id', 'test_class.id') 這種 (這樣的話和所有手工添加前綴沒什麼兩樣),必須找到一個自動添加前綴的辦法。

觀察 table.field 模式,它出現的位置不定,可能在列、任何一個子句中出現,因此在固定的位置去添加前綴是不大可能的。那麼咱們反過來想一下,若是在 SQL 已經構造完成但還未執行時,這時已經知道有哪些地方使用了這種格式,去一一替換便可。那麼如何知道有哪些地方使用了這種格式?

使用正則

咱們用正則表達式找到 table.field 的 table 部分,給 table 加上表前綴便可 (這裏不考慮跨庫查詢時三個點的狀況)。

基類新增 _wrapPrepareSql() 方法:

// 替換 table.field 爲 prefixtable.field
protected function _wrapPrepareSql()
{
    $quote = static::$_quote_symbol;
    $prefix_pattern = '/'.$quote.'([a-zA-Z0-9_]+)'.$quote.'(\.)'.$quote.'([a-zA-Z0-9_]+)'.$quote.'/';
    $prefix_replace = self::_quote($this->_wrapTable('$1')).'$2'.self::_quote('$3');

    $this->_prepare_sql = preg_replace($prefix_pattern, $prefix_replace, $this->_prepare_sql);
}

修改 _execute() 方法:

protected function _execute()
{
    try {
        // table.field 模式添加表前綴
        $this->_wrapPrepareSql();
        $this->_pdoSt = $this->_pdo->prepare($this->_prepare_sql);
        $this->_bindParams();
        $this->_pdoSt->execute();
        $this->_reset();
    } catch (PDOException $e) {
        if($this->_isTimeout($e)) { 

            $this->_closeConnection();
            $this->_connect();
            
            try {
                // table.field 模式添加表前綴
                $this->_wrapPrepareSql();
                $this->_pdoSt = $this->_pdo->prepare($this->_prepare_sql);
                $this->_bindParams();
                $this->_pdoSt->execute();
                $this->_reset();
            } catch (PDOException $e) {
                throw $e;
            }
        } else {
            throw $e;
        }
    }

}

最後咱們進行一個完整的測試:

require_once dirname(dirname(__FILE__)) . '/vendor/autoload.php';

use Drivers\Mysql;

$config = [
    'host'        => 'localhost',
    'port'        => '3306',
    'user'        => 'username',
    'password'    => 'password',
    'dbname'      => 'database',
    'prefix'      => 'test_',
    'charset'     => 'utf8',
    'timezone'    => '+8:00',
    'collection'  => 'utf8_general_ci',
    'strict'      => false,
];

$driver = new Mysql($config);

$results = $driver->table('student')
    ->select('student.name', 'class.name')
    ->join('class', 'student.class_id', 'class.id')
    ->get();

var_dump($results);

試試看吧!

複雜語句的構造

到目前位置,查詢相關的 SQL 構造方法基本開發完畢,咱們進行一些複雜的 SQL 構造吧。

注:這裏只是以個人測試環境舉例,你們能夠按照本身的思路去建表

構造語句 SELECT * FROM t_user WHERE username = 'Jackie aa' OR ( NOT EXISTS ( SELECT * FROM t_user WHERE username = 'Jackie aa' ) AND (username = 'Jackie Conroy' OR username = 'Jammie Haag')) AND g_id IN ( SELECT id FROM t_user_group) ORDER BY id DESC LIMIT 1 OFFSET 0

$results = $driver->table('user')
            ->where('username', 'Jackie aa')
            ->orWhereBrackets(function($query) {
                $query->whereNotExists(function($query) {
                    $query->table('user')->where('username', 'Jackie aa');
                })->WhereBrackets(function($query) {
                    $query->where('username', 'Jackie Conroy')
                            ->orWhere('username', 'Jammie Haag');
                });
            })
            ->whereInSub('g_id', function($query) {
                $query->table('user_group')->select('id');
            })
            ->orderBy('id', 'DESC')
            ->limit(0, 1)
            ->get();

構造語句 SELECT t_user.username, t_user_group.groupname FROM t_user LEFT JOIN t_user_group ON t_user.g_id = t_user_group.id WHERE username = 'Jackie aa' OR ( NOT EXISTS ( SELECT * FROM t_user WHERE username = 'Jackie aa' ) AND username = 'Jackie Conroy' )

$results = $driver->table('user')
            ->select('user.username', 'user_group.groupname')
            ->leftJoin('user_group', 'user.g_id', 'user_group.id')
            ->where('user.username', 'Jackie aa')
            ->orWhereBrackets(function($query) {
                $query->whereNotExists(function($query) {
                    $query->table('user')->where('username', 'Jackie aa');
                })->where('user.username', 'Jackie Conroy');
            })
            ->get();

更多例子參考 WorkerF 單元測試 - PDODQL

相關文章
相關標籤/搜索