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

关联查询是关系型数据库典型的查询语句,根据两个或多个表中的列之间的关系,从这些表中查询数据。在 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

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

推荐阅读更多精彩内容