使用PHP查询生成器处理复杂的WHERE子句

时间:2009-12-17 23:50:16

标签: php sql mysql postgresql activerecord

有几个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()子句在不同的当前库中使用了许多常见的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 (...)以及准备好的语句绑定参数(“?”&amp;“: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)

:UPDATE:

在我的问题过程中,我逐渐意识到WHERE和HAVING条件只会越走越复杂。试图抽象甚至80%的功能将导致一个庞大的库只为WHERE和HAVING。比尔指出,对于像PHP这样的脚本语言来说,这是不合理的。

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

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

5 个答案:

答案 0 :(得分:8)

我在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中的代码完成。但是,当许多运算符和操作数使用'>='之类的字符串常量时,就会破坏任何代码完成情报。


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

你也可以看一下Perl的DBIx :: Class,但它最终会很难看。

答案 1 :(得分:2)

这是我的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 :(得分:2)

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

在调查之后,我发现Zend-Db和其他类似引擎的问题在于它们试图成为所有人的所有事情。为了吸引最大的受众,他们需要提供最一般的功能,就我所见而言,这已成为他们自己的撤销功能(并且由Bill Karwin熟练解释)。

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

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

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

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

答案 3 :(得分:1)

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

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

答案 4 :(得分:0)

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

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