问题 使用PHP Query Builder处理复杂的WHERE子句


那里有几个ActiveRecord样式的查询构建器库。有些是 独立 有些人来了 内置于框架中。但是,当涉及到复杂的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)

有许多常见的ActiveRecord格式,where()子句在不同的中使用 当前 库。

$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 (...) 以及准备好的语句绑定参数(“?”和“:name”)。

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() 他们用勾号`)。

有一点,抽象伤害的程度超过它的帮助,条件就是这样一个地方。


5708
2017-12-17 23:50


起源



答案:


我做了很多工作 Zend_Db 库,包括PHP 用于构造SQL查询的类。我决定尝试处理所有可以想象的SQL语法 WHERE 和 HAVING 条款,原因如下:

  • PHP是一种脚本语言,可以在每个请求中解析和编译代码(除非您使用字节码缓存)。所以PHP环境对庞大的代码库很敏感 - 比Java或C#或Python更重要,或者你有什么。因此,尽可能保持图书馆的精益是一个高度优先事项。

    全部 Zend_Db 我工作的库大约有2000行PHP代码。相比之下,Java Hibernate大约有118K行代码。但这不是一个问题,因为Java库是预编译的,不必在每个请求上加载。

  • SQL表达式遵循生成语法,这种语法比您展示的任何基于PHP的构造更紧凑,更易于阅读和维护。学习SQL表达式语法比学习可以模拟它的API要容易得多。你最终支持“简化语法”。否则你就是这样开始的,并发现自己被用户社区强迫进入 特征蠕变 直到你的API变得异常复杂。

  • 要调试使用这种API的应用程序,您不可避免地需要访问最终的SQL表达式,因此它是关于 最漏洞的抽象 你可以有。

  • 为SQL表达式使用基于PHP的接口的唯一好处是它可以帮助智能编辑器和IDE中的代码完成。但是,当许多运算符和操作数使用字符串常量时 '>=',你破坏任何代码完成情报。


更新: 我刚读了一篇很好的博客文章“与ORM告别“作家Aldo Cortesi建议使用 SQL表达式语言 在Python的SQLAlchemy中。 Python中标准的语法糖和运算符重载(但PHP不支持)使这成为一种非常有效的查询生成解决方案。

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


8
2017-12-18 00:12



我觉得你的权利。我们越是尝试抽象一些东西,我们的工作就越难。一个典型的例子是强制使用BBCode,纺织品或降价,因为程序员不想正确过滤HTML。 - Xeoncross
我认为存在简单的标记格式,因为典型的用户无法平衡他们的HTML标记。 :) - Bill Karwin
其实,你 *have* 至 [b]balance[/b] 你的 <tags>anyway</tags>。只是早期的程序员不理解编码和/或转义输出,所以他们不知道在评论中如何处理HTML - 所以他们删除了它并让用户学到别的东西! - Xeoncross
我现在想知道你对使用pecl运算符扩展和语法糖来处理sql语法的php库的想法。我现在面临类似的问题,决定使用其中任何一个 where 要么 having 处理 every imaginable SQL syntax,他们的表现受到了打击。你能帮我解决这个问题吗? - khael
@khael,语法糖问题与性能优化问题不同。前者可能有助于提高开发人员的工作效率(并与ORM解决方案共享此目标),但后者侧重于运行时效率。这是两个单独的目标,很难找到一个满足这两个目标的解决方案。 - Bill Karwin


答案:


我做了很多工作 Zend_Db 库,包括PHP 用于构造SQL查询的类。我决定尝试处理所有可以想象的SQL语法 WHERE 和 HAVING 条款,原因如下:

  • PHP是一种脚本语言,可以在每个请求中解析和编译代码(除非您使用字节码缓存)。所以PHP环境对庞大的代码库很敏感 - 比Java或C#或Python更重要,或者你有什么。因此,尽可能保持图书馆的精益是一个高度优先事项。

    全部 Zend_Db 我工作的库大约有2000行PHP代码。相比之下,Java Hibernate大约有118K行代码。但这不是一个问题,因为Java库是预编译的,不必在每个请求上加载。

  • SQL表达式遵循生成语法,这种语法比您展示的任何基于PHP的构造更紧凑,更易于阅读和维护。学习SQL表达式语法比学习可以模拟它的API要容易得多。你最终支持“简化语法”。否则你就是这样开始的,并发现自己被用户社区强迫进入 特征蠕变 直到你的API变得异常复杂。

  • 要调试使用这种API的应用程序,您不可避免地需要访问最终的SQL表达式,因此它是关于 最漏洞的抽象 你可以有。

  • 为SQL表达式使用基于PHP的接口的唯一好处是它可以帮助智能编辑器和IDE中的代码完成。但是,当许多运算符和操作数使用字符串常量时 '>=',你破坏任何代码完成情报。


更新: 我刚读了一篇很好的博客文章“与ORM告别“作家Aldo Cortesi建议使用 SQL表达式语言 在Python的SQLAlchemy中。 Python中标准的语法糖和运算符重载(但PHP不支持)使这成为一种非常有效的查询生成解决方案。

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


8
2017-12-18 00:12



我觉得你的权利。我们越是尝试抽象一些东西,我们的工作就越难。一个典型的例子是强制使用BBCode,纺织品或降价,因为程序员不想正确过滤HTML。 - Xeoncross
我认为存在简单的标记格式,因为典型的用户无法平衡他们的HTML标记。 :) - Bill Karwin
其实,你 *have* 至 [b]balance[/b] 你的 <tags>anyway</tags>。只是早期的程序员不理解编码和/或转义输出,所以他们不知道在评论中如何处理HTML - 所以他们删除了它并让用户学到别的东西! - Xeoncross
我现在想知道你对使用pecl运算符扩展和语法糖来处理sql语法的php库的想法。我现在面临类似的问题,决定使用其中任何一个 where 要么 having 处理 every imaginable SQL syntax,他们的表现受到了打击。你能帮我解决这个问题吗? - khael
@khael,语法糖问题与性能优化问题不同。前者可能有助于提高开发人员的工作效率(并与ORM解决方案共享此目标),但后者侧重于运行时效率。这是两个单独的目标,很难找到一个满足这两个目标的解决方案。 - Bill Karwin


这是我的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()方法。


2
2017-12-18 04:47



感谢分享。但是,像 BETWEEN ? AND ?,功能,分组操作 (? OR (? AND ?)),也不行。 - Xeoncross
看看我有些相关的问题: stackoverflow.com/questions/1332217/backticking-mysql-entities 也许你可以为Quote()方法实现类似的东西。 - Alix Axel


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

在研究之后,我发现Zend-Db和其他类似引擎的问题在于它们试图成为所有人的一切。为了吸引最大的受众,他们需要提供最一般的功能,据我所知,这将成为他们自己的撤销(并由Bill Karwin专业解​​释)。

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

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

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

到目前为止我写的库大约有600行代码(大约170行代码是错误处理的)。它处理ISO连接,子语句(在 SELECTFROM 和 WHERE 条款),任何双边比较条款, INEXISTS 和 BETWEEN (使用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)

2
2018-05-13 23:34





SQLAlchemy的我的API是迄今为止我用过的最好的API。它是一个Python库,但你仍然可以从中受到启发。它不仅适用于WHERE子句 - 整个SQL查询(无论是选择还是DML)都是用易于修改的数据结构表示的。

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


1
2017-12-18 00:01



不是Python开发人员当涉及到[WHERE子句结构] [1]时,我在API上遇到了一些麻烦。你介意在你的问题中添加一个例子吗? [1]: sqlalchemy.org/docs/05/reference/sqlalchemy/... - Xeoncross
sqlalchemy.org/docs/06/sqlexpression.html 涵盖它。 - Alex Brasetvik


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

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


0
2017-12-09 08:46