写一个“特殊”的查询构造器 - (六、关联)

关联查询是关系型数据库典型的查询语句,根据两个或多个表中的列之间的关系,从这些表中查询数据。在 SQL 标准中使用 JOIN 和 ON 关键字来实现关联查询。

Join 子句

join 子句的构造并不难,注意事项就是关联查询的注意事项:

  • 写对语法和关联的条件
  • 使用 table.field 模式防止字段重名

基类中新建 join() 方法:

// $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() 方法:

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();

表前缀

为什么要有表前缀

以前很多数据表放在一个数据库中的时候,需要表前缀来区分功能。虽然现在这样的情况已经很少,但是对于查询构造器而言,还是要提供一个方便的方法来对表前缀进行设置,特别是当你没有权限修改表名的时候。

自动添加表前缀的方法

对于有表前缀的表,我们并不想每次都写一个前缀,这样会导致前缀更改后,应用层要跟着修改。所以我们将表前缀作为一个配置参数传入查询构造器,在查询构造器的底层进行自动前缀添加。

表前缀的配置,假设表前缀为 'test_' :

// 以 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

推荐阅读更多精彩内容