【问题标题】:Handling Complex WHERE clauses with a PHP Query Builder使用 PHP Query Builder 处理复杂的 WHERE 子句
【发布时间】:2010-12-27 21:06:29
【问题描述】:

那里有几个 ActiveRecord 样式的查询构建器库。有些是stand alone,有些是built into frameworks。但是,当涉及到复杂的 SQL 时,他们确实遇到了 WHERE 和 HAVING 子句的问题。将其他数据库放在一边——我正在尝试提出一种与 MySQL 和 PostgreSQL 兼容的 WHERE() 方法,该方法可以修复这些当前方法的缺陷。

接下来是一长串想法和示例,展示了我迄今为止所能想到的最好的方法。但是,我似乎无法解决所有用例,而且我觉得我的部分解决方案很草率。任何能够回答解决所有这些问题的东西的人不仅会回答这个问题,而且还要负责解决一个困扰 PHP 实现好几年的问题。

常用运算符

    =   Equal
    <>  Not Equal
    >   Greater Than
    <   Less Than
    >=  Greater Than Or Equal
    <=  Less Than Or Equal
    BETWEEN between values on right 
    NOT logical NOT 
    AND logical AND 
    OR  logical OR

示例 Where 子句

SELECT ... FROM table...
    WHERE column = 5
    WHERE column > 5
    WHERE column IS NULL
    WHERE column IN (1, 2, 3)
    WHERE column NOT IN (1, 2, 3)
    WHERE column IN (SELECT column FROM t2)
    WHERE column IN (SELECT c3 FROM t2 WHERE c2 = table.column + 10)
    WHERE column BETWEEN 32 AND 34
    WHERE column BETWEEN (SELECT c3 FROM t2 WHERE c2 = table.column + 10) AND 100
    WHERE EXISTS (SELECT column FROM t2 WHERE c2 > table.column)

where() 子句在不同的 current 库中使用了许多常见的 ActiveRecord 格式。

$this->db->where(array('session_id' => '?', 'username' => '?'));
$this->db->fetch(array($id, $username));

// vs with is_int($key)
$this->db->where(array('session_id', 'username'));
$this->db->fetch(array($id, $username));

// vs with is_string($where)
$this->db->where('session_id', '?');
$this->db->where('username');
$this->db->fetch(array($id, $username));

// vs with is_array($value)
$this->db->where('session_id', '?');
$this->db->where('username', array('Sam', 'Bob'));
$this->db->fetch(array($id));

这是我到目前为止的最终格式。它应该处理分组(...) AND (...) 以及准备好的语句绑定参数(“?”和“:名称”)。

function where($column, $op = '=', $value = '?', $group = FALSE){}


// Single line

$this->db->where('column > 5');
$this->db->where('column IS NULL');

// Column + condition

$this->db->where('column', '=');
// WHERE column = ?     (prepared statement)
$this->db->where('column', '<>');
// WHERE column <> ?    (prepared statement)

// Column + condition + values

$this->db->where('column', '=', 5);
// // WHERE column = 5
$this->db->where('column', 'IN', '(SELECT column FROM t2)');
// WHERE column IN (SELECT column FROM t2)
$this->db->where('column', 'IN', array(1,2,3));
// WHERE column IN (1, 2, 3)
$this->db->where('column', 'NOT IN', array(1,2,3));
// WHERE column NOT IN (1, 2, 3)

// column + condition + values + group
$this->db->where(
    array(
        array('column', '<', 20), 
        array('column', '>', 10)
    ),
    NULL,
    NULL,
    $group = TRUE
);
// WHERE (column < 20 AND column > 10)

:更新:

在我提出问题的过程中,我开始意识到 WHERE 和 HAVING 条件只会越深入越复杂。尝试抽象 80% 的特征将导致一个庞大的库,仅用于 WHERE 和 HAVING。正如比尔指出的那样,这对于像 PHP 这样的脚本语言是不合理的。

解决方案就是手工制作查询的 WHERE 部分。只要您在列周围使用",您就可以在 Postgre、SQLite 和 MySQL 中使用相同的 WHERE 查询,因为它们使用几乎相同的 SQL 语法。 (对于 MySQL,您必须在 str_replace() 他们打勾`)。

有一点是抽象的伤害大于它的帮助,条件就是这样一个地方。

【问题讨论】:

    标签: php sql mysql postgresql activerecord


    【解决方案1】:

    SQLAlchemy 的 API 是迄今为止我使用过的最好的 API。这是一个 Python 库,但您仍然可以从中获得灵感。它不仅适用于 WHERE 子句 --- 整个 SQL 查询(无论是 select 还是 DML)都用易于修改的数据结构表示。

    (我指的是它的 SQL 工具包,而不是 ORM 部分。:-)

    【讨论】:

    【解决方案2】:

    我在 Zend_Db 库上做了很多工作,其中包括一个 PHP class for constructing SQL queries。我决定尝试处理 WHEREHAVING 子句中所有可以想象的 SQL 语法,原因如下:

    • PHP 是一种脚本语言,它会针对每个请求解析和编译代码(除非您使用字节码缓存)。所以 PHP 环境对庞大的代码库很敏感——比 Java 或 C# 或 Python 或你拥有的更敏感。因此,保持图书馆尽可能精简是当务之急。

      我使用的所有 Zend_Db 库大约有 2,000 行 PHP 代码。相比之下,Java Hibernate 大约有 118K 行代码。但这不是什么大问题,因为 Java 库是预编译的,不必在每个请求上加载。

    • SQL 表达式遵循更紧凑的生成式语法,与您展示的任何基于 PHP 的构造相比,更易于阅读和维护。学习 SQL 表达式语法比学习可以模拟它的 API 要容易得多。您最终支持“简化语法”。否则,您一开始就是这样,然后发现自己被您的用户社区强制使用Feature Creep,直到您的 API 变得无法使用。

    • 要调试使用此类 API 的应用程序,您不可避免地需要访问最终的 SQL 表达式,因此您可以使用 leakiest abstraction

    • 为 SQL 表达式使用基于 PHP 的接口的唯一优点是它有助于在智能编辑器和 IDE 中完成代码。但是当这么多的运算符和操作数使用字符串常量时,比如'&gt;=',你会破坏任何代码完成智能。


    更新:我刚刚阅读了一篇不错的博客文章“A Farewell to ORMs”。作者 Aldo Cortesi 建议在 Python 的 SQLAlchemy 中使用SQL Expression Language。 Python 中的标准语法糖和运算符重载(但 PHP 不支持)使其成为非常有效的查询生成解决方案。

    您可能还会查看 Perl 的 DBIx::Class,但它最终变得非常丑陋。

    【讨论】:

    • 我觉得你说得对。我们越是尝试抽象一些东西,我们的工作就会变得越困难。一个典型的例子是强制使用 BBCode、textile 或 markdown,因为程序员不想正确过滤 HTML。
    • 我认为存在简单的标记格式是因为一般用户无法平衡他们的 HTML 标记。 :)
    • 其实你*have*[b]balance[/b]你的&lt;tags&gt;anyway&lt;/tags&gt;。只是早期的程序员不了解编码和/或转义输出,所以他们不知道如何处理 cmets 中的 HTML - 所以他们将其删除并让用户学习其他内容
    • 我现在想知道您对使用 pecl 运算符扩展和语法糖处理 sql 语法的 php 库的想法的看法。我现在面临类似的问题,决定使用wherehaving 来处理every imaginable SQL syntax,并且它们的性能受到影响。你能帮我解决这个问题吗?
    • @khael,语法糖的问题不同于性能优化的问题。前者可能有助于提高开发人员的生产力(并与 ORM 解决方案共享此目标),但后者侧重于运行时效率。这是两个不同的目标,很难找到一种同时满足这两个目标的解决方案。
    【解决方案3】:

    这是我的 ActiveRecord 类的一部分,我不处理子查询(我什至不打扰):

    public function Having($data, $operator = 'LIKE', $merge = 'AND')
    {
        if (array_key_exists('query', $this->sql) === true)
        {
            foreach ($data as $key => $value)
            {
                $this->sql['having'][] = ((empty($this->sql['having']) === true) ? 'HAVING' : $merge) . ' ' . $this->Tick($key) . ' ' . $operator . ' ' . $this->Quote($value);
            }
        }
    
        return $this;
    }
    
    public function Where($data, $operator = 'LIKE', $merge = 'AND')
    {
        if (array_key_exists('query', $this->sql) === true)
        {
            foreach ($data as $key => $value)
            {
                $this->sql['where'][] = ((empty($this->sql['where']) === true) ? 'WHERE' : $merge) . ' ' . $this->Tick($key) . ' ' . $operator . ' ' . $this->Quote($value);
            }
        }
    
        return $this;
    }
    

    您可能会考虑的另一件事是使用 customHaving() 和 customWhere() 方法。

    【讨论】:

    【解决方案4】:

    我知道这是一个非常古老的帖子,但无论如何我都会回复它,因为我正在开发自己的课程以满足与问题提出的类似需求。

    经过研究,我发现 Zend-Db 和其他此类引擎的问题在于它们试图成为所有人的一切。为了吸引最大的受众,他们需要提供最通用的功能,据我所知(正如 Bill Karwin 的专业解释),这将成为他们自己的失败。

    许多引擎所做的最明显的过度复杂化之一就是将 SQL 代码的生成与其执行混淆(使编写脏 SQL 变得更容易)。在许多应用程序中,将这两者明确分开是个好主意,鼓励开发人员考虑注入攻击等。

    在构建 SQL 引擎时,首先要做的是限制引擎可以生成的 SQL 的范围。例如,您不应该让它产生select * from table;引擎应要求开发人员明确定义每个selectwherehaving 列。再举一个例子,要求每列都有一个别名通常很有用(数据库通常不需要)。

    请注意,以这些方式限制 SQL 并不会限制您实际从数据库中获取的内容。是的,它有时会使前期编码更加冗长,但它也使它更加结构化,并允许您转储数百行库代码,这些代码最初只是为了处理复杂的异常并提供 (嗯)“灵活性”。

    到目前为止,我编写的库大约有 600 行代码(其中约 170 行用于错误处理)。它处理 ISO 连接、子语句(在 SELECTFROMWHERE 子句中)、任何 2 面比较子句、INEXISTSBETWEEN(在WHERE 子句)。它还隐式创建绑定,而不是直接将值注入 SQL。

    限制(除了已经提到的):SQL 是专门为 Oracle 编写的。在任何其他数据库平台上未经测试。

    我愿意分享代码,假设任何改进都会发回。

    作为库让我生成的示例,我希望以下内容足够简单以直观,同时也足够复杂以显示可扩展性潜力:

    <?php
    $substmt = new OraSqlStatement;
    $substmt->AddVarcharCol ('value','VALUE')
            ->AddVarcharCol ('identity','UID',false)
            ->AddVarcharCol ('type','info_type',false)
            ->AddFrom ('schemaa.user_propertues','up')
            ->AddWhere ('AND')
            ->AddComparison ('UID', '=', 'e.identity', 'column')
            ->AddComparison ('info_type', '=', 'MAIL_ADDRESS');
    
    $stmt = new OraSqlStatement;
    $stmt->AddVarcharCol ('company_id', 'Company')
         ->AddVarcharCol ('emp_no',     'Emp Id')
         ->AddVarcharCol ('person_id',  'Pers Id')
         ->AddVarcharCol ('name',       'Pers Name')
         ->AddDateCol ('employed_date', 'Entry Date')
         ->AddDateCol ('leave_date', 'Leave Date')
         ->AddVarcharCol ('identity',   'User Id')
         ->AddVarcharCol ('active', 'Active')
         ->AddVarcharCol ($substmt, 'mail_addy')
         ->AddFrom ('schemab.employee_tab', 'e')
         ->AddFrom ('schemaa.users_vw','u','INNER JOIN','u.emp_no=e.emp_number')
         ->AddWhere ('AND')
         ->AddComparison ('User Id', '=', 'my_user_id')
         ->AddSubCondition ('OR')
         ->AddComparisonNull ('Leave Date', false)
         ->AddComparisonBetween ('Entry Date', '2011/01/01', '2011/01/31');
    
    echo $stmt->WriteSql();
    var_dump($stmt->GetBindArray());
    ?>
    

    产生:

    SELECT 
      company_id "Company", emp_no "Emp Id", person_id "Pers Id", name "Pers Name", 
      employed_date "Entry Date", leave_date "Leave Date", identity "User Id", active "Active", 
      ( SELECT value "VALUE" FROM schemaa.user_propertues up 
        WHERE  upper(identity) = upper(e.identity)
          AND  upper(TYPE) = upper (:var0) 
      ) "mail_addy" 
    FROM 
      schemab.employee_tab e 
          INNER JOIN schemaa.users_vw u ON u.emp_no = e.emp_number 
    WHERE 
            upper (identity) = upper (:var1)
      AND ( leave_date IS NOT NULL OR
            employed_date BETWEEN to_date (:var2,'YYYY/MM/DD') AND to_date (:var3,'YYYY/MM/DD') 
          )
    

    连同绑定数组:

    array
      0 => string 'MAIL_ADDRESS' (length=12)
      1 => string 'my_user_id' (length=10)
      2 => string '2011/01/01' (length=10)
      3 => string '2011/01/31' (length=10)
    

    【讨论】:

      【解决方案5】:

      您可以考虑使用PHP编写的SQLBuilder,它可以通过设置不同的查询驱动程序为MySQL和PostgreSQL生成跨平台的SQL。

      用例在这里: https://github.com/c9s/SQLBuilder/blob/2.0/tests/SQLBuilder/Query/SelectQueryTest.php

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2020-07-26
        • 2021-08-15
        • 2010-11-06
        • 2016-04-10
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多