资讯专栏INFORMATION COLUMN

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

rainyang / 963人阅读

摘要:虽然现在这样的情况已经很少,但是对于查询构造器而言,还是要提供一个方便的方法来对表前缀进行设置,特别是当你没有权限修改表名的时候。所以我们将表前缀作为一个配置参数传入查询构造器,在查询构造器的底层进行自动前缀添加。

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

$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

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/30825.html

相关文章

  • 一个特殊查询造器 - (前言)

    摘要:而在项目开发中,我们想要的是一个更好用的可维护的工具,此时,对代码的封装模块化就显得尤为重要,于是出现了两种方案查询构造器,对象关系映射。典型环境下按照一般的查询构造器处理就行。 文章目录 写一个特殊的查询构造器 - (前言) 写一个特殊的查询构造器 - (一、程序结构,基础封装) 写一个特殊的查询构造器 - (二、第一条语句) 写一个特殊的查询构造器 - (三、条件查询) 写一个特殊...

    GitChat 评论0 收藏0
  • 一个特殊查询造器 - (二、第一条语句)

    摘要:注在常驻内存单例模式下,这种多次用一个类进行查询的情形很常见。断线重连对于典型环境而言,一次的查询已经随着的请求而结束,的垃圾回收功能会回收一次请求周期内的数据。但在常驻内存的环境下,尤其是单例模式下,数据库驱动类可能一直在内存中不被销毁。 构造、执行第一条语句 上一篇完成了代码结构的搭建和 PDO 的基础封装,这一篇我们来讲如何构造一个最基本的 SQL 语句,并执行得到结果。 que...

    dadong 评论0 收藏0
  • Java多线程学习()Lock锁使用

    摘要:返回与此锁相关联的给定条件等待的线程数的估计。查询是否有线程正在等待获取此锁。为公平锁,为非公平锁线程运行了获得锁定运行结果公平锁的运行结果是有序的。 系列文章传送门: Java多线程学习(一)Java多线程入门 Java多线程学习(二)synchronized关键字(1) java多线程学习(二)synchronized关键字(2) Java多线程学习(三)volatile关键字 ...

    Caicloud 评论0 收藏0
  • Spring Boot 2.x():优雅统一返回值

    摘要:下面我们来测试一下,访问我们经过修改后的编写的接口这里我将返回值统一为,以便数据存入,实际类型应是接口的返回类型。如果没有返回值的话,那就可以一个对象直接通过构造方法赋值即可。 为什么要统一返回值 在我们做后端应用的时候,前后端分离的情况下,我们经常会定义一个数据格式,通常会包含code,message,data这三个必不可少的信息来方便我们的交流,下面我们直接来看代码 ReturnV...

    shaonbean 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<