原文:http://drupal.org/node/310075
动态查询(Dynamic queries)指的是通过Drupal动态构建出来的查询方式,而不是通过直接提供查询字符串。所有的插入(Insert)、更新(Update)、删除(Delete)以及合并(Merge)查询都必须是动态的,选择(Select)查询可以是静态的也可以是动态的。所以,“动态查询”一般指的就是动态选择查询。
所有的动态查询都是通过调用适当的连接对象,使用查询对象来构建的。和静态查询一样,在绝大多数情况下,可以通过内隐的包装取得这个对象,接下来所进行的查询指令,都是通过调用这个查询对象的方法的形式来实现的。
动态选择查询通过db_select()函数开始,例子如下:
$query = db_select('node', 'n', $options);
?>
在这个例子中,“node”是查询的主表,也就是在FROM语句后的第一个数据表。注意这里不再需要用括号包含,查询构造器会自动的处理。第二个参数是这个数据表的别名,如果不指定则使用完整表名。$options数组是可选的,其意义和静态查询中的$options参数相同。
动态选择查询可以非常简单也可以非常复杂,我们在这里将概要的介绍下它工作的基本原理,详细的用法恐怕得要写一本书才能尽述了。
概览
这是一个关于用户数据表的相对简单的查询,下面我们将看看构成这个查询的各个部分,并且介绍更多技术比如连接(join)。
上面的语句可以粗略的等价于:
$result = db_query(“SELECT uid, name, status, created, access FROM {users} u WHERE uid <> 0 LIMIT 50 OFFSET 0″);
这是在用户管理页面中使用的查询的一个简化形式,用来给下面的学习提供参考。
连接查询(Joins)
要连接到其它数据表,可以使用join()、innerJoin()、leftJoin()、或者rightJoin() 方法,如:
上面的语句将会加入一个INNER JOIN(缺省的连接形式)到“user”数据表后,“user”数据表会使用别名“u”。连接的条件是“n.uid = u.uid AND u.uid = :uid”,以及:uid的值等于5的时候。请注意占位符在这里的使用方法,这可以更安全的添加更多的连接语句。永远不要直接把值或者变量放到查询语句中去,就像静态查询语句中不能直接放入变量和数值一样(可能会导致SQL注入攻击漏洞)。innerJoin()、leftJoin()、rightJoin()方法分别用于添加各自对应的连接查询。
join()方法返回的值是指定连接的数据表的别名,如果显式的制定了别名则会直接使用,除非某些特殊情况下这个别名已经被其它表使用了。在这种情况下,系统会赋予表另一个别名。
注意,除了使用一个名称比如“user”作为表名之外,所有的连接方法都可以用一个选择查询作为第一个参数,比如:
$myselect = db_select('mytable')
->fields('mytable')
->condition('myfield', 'myvalue');
$alias = $query->join($myselect, 'myalias', 'n.nid = myalias.nid');
?>
字段(Fields)
在选择查询中添加字段,使用addField()方法:
$title_field = $query->addField('n', 'title', 'my_title');
?>
上面的代码指定查询选择了别名为“n”的数据表中的“title”字段,并且赋予别名“my_title”。如果这里没有指定别名则会自动生成一个,在绝大多数情况下这个自动生成的别名就是字段的名称。这个例子中,就是“title”。如果这个别名已经存在,那么就会用表名和字段名组合来生成,这个例子中就是“n_title”。如果那个别名也已经存在,则会自动加上一个数字直到别名没有被使用而已,比如“n_title_2”。
注意,如果你自己创建和维护查询,并且没有指定别名,而缺省的别名又不可用,这几乎肯定就是你程序中的一个bug。如果你打算实现hook_query_alter()钩子的话,你不可能确切的知道那些别名已经被使用了,所以你只能一直使用生成的别名。
要选择多个字段,只需要按照需要的顺序多次调用addFiled()方法就可以了。注意大多数情况下字段的顺序是无关紧要的,但如果这样的话可能会是一个模块中业务逻辑的缺陷。
作为一个替代的快捷方式,你可以使用fields()方法同时添加多个字段。
上面的代码等效于调用addFields方法四次,每次添加一个字段。不过,fields()方法不支持为字段指定别名,也不会返回产生的别名,而返回的是查询对象自身,这样可以链式的调用查询方法。如果你希望知道生成的别名,可以使用addField()方法或者getFields()方法来访问原始的内部字段结构。
调用fields()方法而不指定任何字段将会导致一个全选择查询“SELECT *”。如:
$query->fields('n');
?>
将会导致一个“n.*”字段被包含在查询列表里。注意这里不会生成任何别名,如果一个表使用SELECT *包含了一个已经从另一个表里指定的字段,那么字段名在结果集中很可能就会发生冲突,在这种情况下,结果集将只会包含一个字段的值。因此,不推荐使用 SELECT * 的用法。
唯一查询(Distinct)
有些SQL查询可能会产生重复的结果,这种情况下,可以在静态查询中使用“DISTINCT”关键词过滤掉这些重复的行。动态查询中,使用distinct()方法:
// 强制过滤掉结果集中的重复记录。
$query->distinct()
?>
注意DISCTINCT可能影响性能,如果除非没有别的方法来约束结果集避免重复结果,不要使用它。
表达式(Expressions)
选择查询构造器支持在字段列表中使用表达式,比如“年龄字段的一倍”、“所有名字字段的总数”,以及标题字段的子字符串等。显然很多的表达式会使用SQL函数,并且不是所有的SQL函数都是跨数据库通用的,这就要求模块开发者必须保证只使用跨数据库兼容表达式。(参考这个列表:http://drupal.org/node/773090)
添加表达式到查询中,使用addExpression()方法。
$count_alias = $query->addExpression('COUNT(uid)', 'uid_count');
$count_alias = $query->addExpression('created - :offset', 'uid_count', array(':offset' => 3600));
?>
上面的第一行语句添加了一段“COUNT(uid) AS uid_count”到查询中,第二个参数是这个字段的别名。极少情况下这个别名已经被使用了,则会自动生成新的别名并且通过addExpression()的返回值返回。如果没有指定别名,缺省的别名将会是“expression”开头的字符串(比如expression_2、expression_3等等)。
可选的第三个参数是一个为占位符提供值的关联数组。
注意有些表达式只有和“Group By”字句一起使用时才会工作,这就要求开发者必须确保生成的查询能够切实有效的工作。
排序(Ordering)
要在动态查询中添加排序字句,使用orderBy()方法。
$query->orderBy('title', 'DESC');
?>
上面的代码指定查询要按照title字段降序排序。第二个参数可以是“ASC”或者“DESC”表示升序或降序排序,缺省为“ASC”。注意这里的字段名必须是通过addField()或者addEXpression()方法创建的字段别名,所以大多数情况下你应该使用上述方法的返回值以确保别名是正确的。要排序多个字段,只要按照需要的顺序多次调用orderBy()方法就可以了。
随机排序(Random Ordering)
在不同的数据库中要对查询进行随机排序使用的语法有轻微的不同,因此最好通过动态查询来实现。
要让指定的查询按照随机顺序排序,只要调用orderRandom()方法就可以了。
$query->orderRandom();
?>
注意oderRandom()方法是可以链式调用,并且可以和orderBy()结合使用。所以下面的语句是很安全的:
$query->orderBy('term')->orderRandom()->execute();
?>
上面的语句首先对查询按照“term”字段排序,然后对于同样的term的记录,进行随机排序。
分组(Grouping)
要对某一给定字段进行分组,使用groupBy()方法。
$query->groupBy('uid');
?>
上面的代码指定查询按照uid字段分组。注意这个的字段名应该是通过addField()或者addExpression()方法创建的别名,所以大多数情况下你应该使用上述方法的返回值以确保别名是正确的。要分组多个字段,只要按照需要的顺序多次调用groupBy()方法就可以了。
范围和限制(Range and Limits)
查询可以限制到所有查询结果的一个确定子集,通常这叫做“范围查询”。在MySQL中,这通常是通过Limit子句实现的。要限制查询的范围,使用range()方法。
上面的代码表示结果集从第五个记录开始而不是第一个,并且值返回10个记录。在大多数情况下我们只要“最开始的n个记录”。所以,指定0作为第一个参数,指定n作为第二个参数即可。
调用range()方法两次将会覆盖之前的值,不带参数的调用将会移除查询中的所有范围限制。
表排序(Table sorting)
要生成一个可以对任意列进行排序的结果表,使用TableSort扩展器并且加入表格标题。注意扩展器将会返回一个新查询对象,之后也应当使用这个新的查询对象。
$query = $query
->extend('TableSort')
->orderByHeader($header);
?>
条件式(Contitionals)
条件式是一个复杂的主题,在选择、更新和删除查询中都会用到,因此需要单独解释。不像更新和删除查询,选择查询拥有两种类型的条件:WHERE子句和HAVING子句。HAVING子句的行为等同于WHERE子句,除非使用havingCondition()和having()方法替代condition()和where()方法的时候。
执行查询
查询构建完成之后,调用excute()方法来编译和运行这个查询。
$result = $query->execute();
?>
执行查询将会返回结果集或者语句对象,和db_query()的返回值完全一样,并且使用同样的方式取值。
$result = $query->execute();
foreach ($result as $record) {
// Do something with each $record
}
?>
注意:当在多列动态查询中使用下列的方法时请小心:
- fetchField()
- fetchAllKyed()
- fetchCol()
这些方法当前需要数字的列索引(0、1、2等)而不是表别名,但是,目前查询构造器并不能保证返回的字段有确定的顺序,所以数据列可能不会是你所期望的顺序。详细的说,表达式总是添加到字段之后,不管你是否把它们添加到查询前面。(这个说明不针对静态查询,静态查询总是会按照你指定的顺序返回数据列。)
统计查询(Count queries)
每一个查询都可以有相应的“统计查询”,从而得到原查询的结果行数。要得到统计查询,使用countQuery()方法。
$count_query = $query->countQuery();
?>
$count_query变量当前是一个新的动态选择查询,没有任何排序限制,执行的时候返回的结果集只包含一个数值,也就是匹配原始查询的记录条数。因为PHP支持返回对象的链式方法,下面的形式更为常用:
$num_rows = $query->countQuery()->execute()->fetchField();
?>
调试(Debugging)
要检查一个查询对象在它的生命周期中某一时刻的SQL查询,只需要调用__toString()方法。
扩展器(Extender)
选择查询支持名为“扩展器”的概念,扩展器提供了一种在运行时为选择查询添加功能的方式,这些功能包括添加额外的方法或者修改已有方法的行为。
如果熟悉面向对象的设计模式,扩展器就是对装饰模式的一种实现。它通过提供一种灵活的替代方法进行子类化以扩展功能,从而动态的给对象添加一些额外的职责。
使用扩展器
要使用扩展器,你必须首先拥有一个查询对象。然后通过这个查询对象调用extend()方法可以得到一个新的对象以替换原有的对象。例如:
$query = $query->extend('PagerDefault');
?>
上面的代码使用了一个选择查询,创建了一个包含原始选择查询的新的“PagerDefault”查询对象,并且返回了这个新对象。$query变量现在可以像原始查询对象那样使用,同时拥有了一些额外的方法。
注意$query变量没有被替换,新的对象是extend()返回的,如果没有保存到一个变量中就会丢失。比如,下面的操作将得不到你期望的结果:
$query = db_select('node', 'n');
$query
->fields('n', array('nid', 'title')
->extend('PagerDefault') // 这一行返回了一个新的PagerDefault对象。
->limit(5); // 这一行能工作,因为调用的是PagerDefault对象。
// extend()方法的返回值没有保存到任何变量中,所以$query变量仍然是原来的选择查询变量。
$query->orderBy('title');
// 这一行执行了选择查询对象,而不是扩展器对象,扩展器对象已经不存在了。$result = $query->execute();
?>
要避免这个问题,推荐在这个选择查询第一次被定义时就进行转换。
$query = db_select('node', 'n')->extend('PagerDefault')->extend('TableSort');
$query->fields(...);
// ...
?>
这样确保了$query对象从一开始就是完全扩展了的对象。
还要注意当扩展器可以叠加(如上例所示),不是所有的扩展器都能和其它扩展器兼容,并且顺序也可能造成问题。比如,一个查询同时扩展了分页和表格排序行为的话,必须首先使用分页扩展器。
创建新扩展器
一个扩展器只是一个实现了SelectQueryInterface接口的类,并且在其构造函数中包含两个参数,一个是选择查询对象(或者另一个实现了SelectQueryInterface接口的对象),一个是DatabaseConnection对象。然后必须实现SelectQueryInterface的方法并传递给构造函数中指定的查询对象,最后返回对象本身。
在绝大多数情况下,所有这些都可以通过扩展SelectQueryInterface接口在内部实现。因此实际上任何实现了SelectQueryInterface接口的类都是扩展器,类的名称就是在extend()方法中指定的参数。
对于扩展器类,之后可以根据需要添加或者重载方法,任何没有重载的方法将会透明的通过包装好的查询对象传递。当重载一个方法的时候,扩展器可能会调用底层查询对象也可能不会,但是必须返回与SelectQuery接口所期望的同样的值。多数情况下,就是查询对象本身,对于扩展器,就是扩展器对象本身。
下面的例子会更清楚的说明:
class ExampleExtender extends SelectQueryExtender {
/**
* 重载通常的排序行为。
*/
public function orderBy($field, $direction = 'ASC') {
return $this;
}
/**
* 添加我们自己的新方法。
*/
public function orderByForReal($field, $direction = 'ASC') {
$this->query->orderBy($field, $direction);
return $this;
}
}
?>
上面的例子重载了orderBy()方法但是没有进行任何操作,而是添加了另一个方法orderByForReal(),通过它来执行实际的排序操作。(当然这是一个相当没有意义的例子,不过主要是用来阐述扩展器的工作机制。)注意在两个方法中,返回的$this变量都是扩展器对象自身。这就确保了扩展器不会在查询对象返回时“弄丢”。
任何模块都可以定义扩展器,Drupal核心定义了两个很有用的扩展器:PagerDefault和TableSort。关于如何在你的代码中利用这些类请参考API文档。
查询修改(Query alteration)
动态选择查询的一个重要的特性是能够被其它模块自由的修改,这就意味着其它模块可以插入自己的限制条件到查询中,包括修改模块的行为或者对查询进行运行时的限制,比如节点访问权限限制。查询修改有三个组件:标签、元数据和hook_query_alter();
标签(Tagging)
每个动态选择查询都可以用一个或多个字符串来标注“标签”。这些标签定义了查询的类型,以便让修改钩子(hooks)决定是否要进行操作。标签应该是一个小写字母数字字符串,和php变量名的命名规则相同。(也即,只允许字母、数字和下划线,并且必须以字母开头)要给查询添加标签,使用addTag()方法:
$query->addTag('node_access');
?>
要检查查询是否已经被某个标签所标记,可以使用下面三种办法:
// 如果查询对象拥有标签返回TRUE。
$query->hasTag('example');
// 如果查询对象拥有下列每个标签返回TRUE。
$query->hasAllTags('example1', 'example2');
// 如果查询对象拥有下列任一标签返回TRUE。
$query->hasAnyTag('example1', 'example2');
?>
hasAllTags()和hasAnyTag()都可以带有任意数量的参数,每个参数代表一个查询的标签,顺序是无关紧要的。
对于标签使用的方式没有强制的规定,不过通常会使用某些标准标签。下面是一个标准化标签的部分列表:
node_access
该查询可以加上节点访问权限限制。
translatable
该查询列可以被翻译。
term_access
该查询可以被加上基于分类术语的权限限制。
views
该查询是通过views模块生成的。
元数据(Meta data)
查询也可以添加上元数据以便给修改钩子提供额外的上下文信息。元数据可以包含任何PHP变量并可以用字符串作键名。
$node = node_load($nid);
// ... Create a $query object here.
$query->addMetaData('node', $node);
?>
元数据并没有确定的含义,而且自身对查询对象没有任何影响。使用它只是为了给修改钩子提供额外的信息,而且只有当查询拥有特定的标签时才需要使用。
访问一个查询所包含的元数据,使用getMetaData()方法。
$node = $query->getMetaData('node');
?>
如果指定的键名不包含元数据,则会返回NULL。
hook_query_alter()
标签和元数据本身都不会做任何事,它们只是独立的提供信息给hook_query_alter()钩子,在这里才会对选择查询进行实际的操作。
通过执行execute()方法,在查询字串编译完成之后,所有的动态选择查询对象都会立即传递给hook_query_alter()钩子。这就提供给了模块一个按照自身需要操纵查询的机会。hook_query_alter()接受单个参数:选择查询对象自身。
/**
* Implementation of hook_query_alter().
*/
function example_query_alter(QueryAlterableInterface $query) {
// ...
}
?>
还可以使用特定于标签的修改钩子,hook_query_TAG_NAME_alter(),当基本的查询钩子调用完毕后,这个钩子会被任何打上标签的查询调用。下面的例子会被包含了“node_access”标签的查询所调用:
function example_query_node_access_alter(QueryAlterableInterface $query) {
// ...
}
?>
这里有关于hook_query_alter()钩子的两个重要观察:
- $query变量不需声明通过引用传递,因为这是一个对象,PHP5及以后版本在处理对象时不会对对象进行复制,所以声明引用传递是不必要的,修改钩子也没有返回值。
- 参数的类型明确指定为QueryAlterableInterface。虽然不是严格的必要,明确指定参数类型可以提供更好一些的运行时保护,以避免传递了错误类型的变量。类型也最好指定为QueryAlterableInterface而不是SelectQuery,这样可以提供提供更好的向上兼容性。
修改钩子可以对查询对象进行任何操作,除了再执行这个查询一次,这样会导致无限循环错误。修改钩子可以使用查询关联的标签和元数据信息,以决定要进行的操作。模块开发者也可以调用额外的方法来对查询添加额外的字段、连接、条件等等,还可以访问查询对象的内部结构来进行直接的操纵。最好首先对查询添加好新的信息,然后再顺序从查询中移除信息或操纵结构。
$fields =& $query->getFields();
$expressions =& $query->getExpressions();
$tables =& $query->getTables();
$order =& $query->getOrderBy();
$where =& $query->conditions();
$having =& $query->havingConditions();
?>
值得注意的一点是上面的语句必须返回引用(=&),这样修改钩子访问的才是和对象同样的数据结构。所有上面的方法都会返回数组,它们的基本结构包含在在SelectQuery的内部文档中:/database/select.inc。
来自 http://blog.ykfan.cn/blackhole/2011/09/18/drupal-7-x-%E5%8A%A8%E6%80%81%E6%9F%A5%E8%AF%A2/
This fails:下面代码不对
<?php
db_query("SELECT nid FROM node LIMIT :d",array(":d"=>3));
?>
Use this instead: 下面代码对
<?php
db_query_range("SELECT nid FROM node",0,3);
?>