作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
Drupal 7数据库API提供了一个标准的、与数据库供应商无关的抽象层,用来访问数据库服务器。该API尽可能的保留了SQL的语法和功能,同时还提供:
· 轻松支持多个数据库服务器;
· 允许开发者使用更复杂的功能,比如事务;
· 为查询语句的动态构建提供了一个结构化接口;
· 执行安全检查和其它良好习惯;
· 为模块提供了一个干净的接口,用来拦截和修改站点的查询语句。
数据库API文档(http://api.drupal.org/api/group/database/7)直接来自于代码中的注释。这个手册对标准文档进一步的做了补充,为模块开发者提供了与数据库交互的教程,同时方便其他人员从概貌上把握Drupal的数据库系统。注意本手册没有涵盖数据库API的所有特性。
数据库API采用了面向对象的设计理念,因此本文档假定大家对面向对象有所了解。不过,大多数操作都有面向过程的形式,程序员为了代码的可读性,仍然可以使用面向过程的形式。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
Drupal数据库层是建立在PHP的PDO基础之上的。PDO提供了一个统一的、面向对象的API,用来访问不同的数据库,但是它没有对不同数据库的方言提供抽象。
由于Drupal的数据库层依赖于PHP的PDO库,所以你在选用主机时,确保你的PHP是支持PDO的。
Drupal中最常见的查询方式就是静态查询。静态查询通常会被一字不差的传递给数据库。通常只有选择查询才可以是静态的。
对于特别简单的查询,通常应该适用静态查询机制。对于更加复杂的查询,比如动态生成的查询或者其它类似查询,则应该使用动态查询。
静态查询的内部方式是使用query方法:
<?php
$result = $conn->query("SELECT nid, title FROM {node}");
?>
经过过程化包装的方式,则更常用:
<?php
$result = db_query("SELECT nid, title FROM {node}");
?>
上面db_query()函数的调用,等价于下面的语句:
<?php
$result = Database::getConnection()->query("SELECT nid, title FROM {node}");
?>
为什么过程化的版本更受欢迎?其中因为Drupal本身主要采用过程化的方式,此外使用db_query是为了保持对Drupal6的兼容,还有就是这样语法更加简洁。
db_query()有3个参数。第一个参数就是查询字符串,在这里所有的数据库表名需要使用花括号进行封装,此外在合适的地方可以使用占位符。第2个参数是包含占位符值的数组。第3个参数是一个可选的包含配置指令的数组,用来指示查询的执行方式。
<?php
$result = db_query("SELECT nid, title FROM {node} WHERE created > :created", array(
':created' => REQUEST_TIME - 3600,
));
?>
上面的代码,将选择过去一小时(3600秒)内创建的所有节点。在查询运行时,占位符:created将会被动态的替换为REQUEST_TIME – 3600的当前值。一个查询可以包含任意多个占位符,但是占位符之间,即便是它们的值相同,但是它们的名字必须是唯一的。根据具体情况,占位符数组可以内联指定(如上例所示),也可以先构建好,然后再传递过来。数组内,元素的顺序对结果并不影响。
以"db_"开头的占位符是为内部系统所保留的,绝不应该在自定义模块中使用。
无论占位符的类型如何,它们都不能被转义或者添加引号。这是因为它们是单独传递给数据库的,数据库服务器可以区分查询字符串和占位符的对应值。
<?php
// 错误:
$result = db_query("SELECT nid, title FROM {node} WHERE type = ':type'", array(
':type' => 'page',
));
// 正确:
$result = db_query("SELECT nid, title FROM {node} WHERE type = :type", array(
':type' => 'page',
));
?>
Drupal的数据库层,包含了有关占位符的一个附加特性。对于一个占位符,如果传递给它的值是一个数组,它就会自动地将占位符扩展为逗号分隔的占位符列表,分别对应于数组中的元素。这意味着开发者不需要考虑他们需要多少个占位符。
我们通过实例来学习一下这一特性:
<?php
// This code:
db_query("SELECT * FROM {node} WHERE nid IN (:nids)", array(':nids' => array(13, 42, 144));
// Will get turned into this prepared statement equivalent automatically:
db_query("SELECT * FROM {node} WHERE nid IN (:nids_1, :nids_2, :nids_3)", array(
':nids_1' => 13,
':nids_2' => 42,
':nids_3' => 144,
));
// Which is equivalent to the following literal query:
db_query("SELECT * FROM {node} WHERE nid IN (13, 42, 144)");
?>
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
db_query()的第3个参数,也就是连接对象query方法的第3个参数,是一个选项数组,用来指示查询应该如何执行。对于大多数的查询来说,常用的指令只有两个。其它值则大多数时候在内部使用。
"target"键用来指定所要使用的目标数据库。如果没有指定,则使用默认的"default"。在下面的例子中,有效值只有一个"slave",这表示如果存在一个从数据库,查询则运行在它的上面。
"fetch"键用来指定用何种方式,来获取查询返回的结果记录。有效的值包括PDO::FETCH_OBJ、PDO::FETCH_ASSOC、PDO::FETCH_NUM、PDO::FETCH_BOTH,或者是一个表示类名的字符串。如果指定的是一个字符串,取回的每条记录都会被放在该类的一个新创建的对象中。其它值对应的行为是由PDO定义的,将返回的记录相应的存放在stdClass对象中、关联数组中、数值数组中、或者数值和关联数组中。有关PDO的相关信息可参考http://php.net/manual/en/pdostatement.fetch.php。默认值为PDO::FETCH_OBJ,通常情况下,尽可能的使用默认值就可以了,在特定情况下,也可以改用其它值。
下面的例子,将在从服务器上(如果存在的话)执行一个查询,并将从结果集中返回的记录存放在关联数组中。
<?php
$result = db_query("SELECT nid, title FROM {node}", array(), array(
'target' => 'slave',
'fetch' => PDO::FETCH_ASSOC,
));
?>
最常用的就是使用foreach()循环,对结果集进行迭代处理。
<?php
$result = db_query("SELECT nid, title FROM {node}");
foreach ($result as $record) {
// Do something with each $record
}
?>
由于所需返回结果的不同,除此以外,还有其它一些方式来获取记录。
为了明确的获取下一条记录,可以使用:
<?php
$record = $result->fetch(); // Use the default fetch mode.
$record = $result->fetchObject(); // Fetch as a stdClass object.
$record = $result->fetchAssoc(); // Fetch as an associative array.
?>
如果没有下一条记录,此时则返回FALSE。通常情况下,应该尽可能的避免使用fetch(),而是选择fetchObject()和fetchAssoc(),后两个语义明确,便于理解。如果你需要使用PDO所支持的其它获取模式,则可以使用fetch()。
为了从结果集中获取单个字段,可以使用:
<?php
$record = $result->fetchField($column_index);
?>
$column_index的默认值为0,表示第一个字段。
为了计算返回记录的总数,可以使用:
<?php
$number_of_rows = $result->rowCount();
?>
为了将所有记录放在单个数组中,可以使用下面的任意一种方式:
<?php
// Retrieve all records into an indexed array of stdClass objects.
$result->fetchAll();
// Retrieve all records into an associative array keyed by the field in the result specified.
$result->fetchAllAssoc($field);
// Retrieve a 2-column result set as an associative array of field 1 => field 2.
$result->fetchAllKeyed();
// You can also specify which two fields to use by specifying the column numbers for each field
$result->fetchAllKeyed(0,2); // would be field 0 => field 2
$result->fetchAllKeyed(1,0); // would be field 1 => field 0
// Retrieve a 1-column result set as one single array.
$result->fetchCol();
// Column number can be specified otherwise defaults to first column
$result->fetchCol($column_index);
?>
注意,fetchAll() 和fetchAllAssoc()在默认情况下,使用在查询上设置的获取模式(数值数组、关联数组、或对象)。通过向其传递一个新的获取模式常量,就可以修改默认的获取模式了。对于fetchAll(),它是第一个参数。对于fetchAllAssoc(),它是第二个参数。
由于PHP 支持对返回的对象调用链式方法,所以通常情况下,会完全跳过$result变量,采用下面的简洁方式:
<?php
// Get an associative array of nids to titles.
$nodes = db_query("SELECT nid, title FROM {node}")->fetchAllKeyed();
// Get a single record out of the database.
$node = db_query("SELECT * FROM {node} WHERE nid = :nid", array(':nid' => $nid))->fetchObject();
// Get a single value out of the database.
$title = db_query("SELECT title FROM {node} WHERE nid = :nid", array(':nid' => $nid))->fetchField();
?>
也可以把查询的结果存放在自定义类的对象中去。例如,如果我们有一个名为ExampleClass的类,下面的查询则会返回类型为exampleClass的对象。
<?php
$result = db_query("SELECT id, title FROM {example_table}", array(), array(
'fetch' => 'ExampleClass',
));
?>
如果该类拥有构造函数__construct(),那么将会创建相应的对象,并会向对象中添加相应的属性,接着将会调用__construct()方法。例如,如果你有下面的类和查询:
<?php
class exampleClass {
function __construct() {
// Do something
}
}
$result = db_query("SELECT id, title FROM {example_table}", array(), array(
'fetch' => 'ExampleClass',
));
?>
将会创建相应的对象,并向该对象中添加属id和title,接着执行__construct()。这些事件的执行顺序,是由低于PHP5.2的PHP版本中的一个bug(http://bugs.php.net/bug.php?id=46139)引起的。
如果该对象上有一个__construct()方法,并且该方法需要在属性添加到对象上的前面执行,则可以使用下面例子中所给的方法:
<?php
$result = db_query("SELECT id, title FROM {example_table}");
foreach ($result->fetchAll(PDO::FETCH_CLASS | PDO::FETCH_PROPS_LATE, 'ExampleClass') as $record) {
// Do something
}
?>
传递给fetchAll的参数,也可以原封不动的用在fetch上。PDO::FETCH_CLASS告诉fetchAll获取返回的结果集,并将值作为属性添加到ExampleClass类型(第二个参数)的对象上。PDO::FETCH_PROPS_LATE告诉fetchAll,先调用__construct(),然后再将结果集作为属性添加到对象上。
动态查询指的是由Drupal动态的构建查询,而不是直接提供一个查询字符串。所有的插入、更新、删除、和合并查询都应该是动态的。选择查询可以是静态的,也可以是动态的。因此,一般动态查询指的就是动态的选择查询。
所有的动态构建的查询都是使用查询对象构建的,而查询对象则源自于对应的连接对象。和静态查询一样,在绝大多数的情况下,我们使用过程化的方式来获取该对象。然而,随后的查询指令,都是采用触发查询对象上的方法的形式,进行调用。
1. 概貌
2. 关联
3. 字段
4. 唯一
5. 表达式
6. 排序
7. 随机排序
8. 分组
9. 范围和限制
10. 表排序
11. 条件语句
12. 执行查询
13. 总计查询
14. 调试
动态的选择查询由db_select()函数开始,如下所示:
<?php
$query = db_select('node', 'n', $options);
?>
在这个例子中,"node"是查询的基表;也就是说,FROM语句中紧跟着FROM的第一个表。注意,这里没有使用花括号;查询构建器会自动地处理这一点。第二个参数是表的别名;如果没有指定的话,将会使用表名。$options数组是可选的,它与静态查询中的$options数组完全相同。
动态选择查询可以非常简单,也可以非常复杂。我们在这里只讲述它的基本原理,如果完全展开的话,一本书也未必写的完。
由于数据库的不同,需要的交互方式也不相同,所以Drupal的数据库层需要为每个数据库类型提供一个驱动。驱动对应文件都放在includes/database/driver中,在这里driver就是表示该驱动的唯一字符串。大多数情况,驱动的键,就是数据库名字的小写版,比如"mysql", "pgsql",或"mycustomdriver"。
每个驱动都包含多个类,它们继承自核心数据库系统中的父类。这些特定于驱动的类,可以根据数据库类型的需要,来覆写各种特性。这些特定于驱动的类,它们的命名方式采用“父类名字”+“_”+”驱动名字”形式。例如,InsertQuery在MySQL下的版本的名字就是InsertQuery_mysql。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
下面是一个有关users表的相对简单的查询。接下来我们会分析这个查询中的每个构成部分,以及会学习一些高级技巧比如表之间的关联。
<?php
$query = db_select('users', 'u');
$query
->condition('u.uid', 0, '<>')
->fields('u', array('uid', 'name', 'status', 'created', 'access'))
->range(0, 50);
$result = $query->execute();
?>
上述代码,大致等价于下面的语句:
$result = db_query("SELECT uid, name, status, created, access FROM {users} u WHERE uid <> 0 LIMIT 50 OFFSET 0");
这个语句,源自于用户的管理页面所对应的代码,我们在这里做了简化处理,我们在后面的学习中,会进一步的用到这一语句。
为了向选择查询添加一个字段,可以使用addField()方法:
<?php
$title_field = $query->addField('n', 'title', 'my_title');
?>
上述代码将会指示查询,从别名为"n"的表中选择字段"title",并为其设定一个别名"my_title"。如果没有指定别名,那么就会自动生成一个。在大多数情况下,自动生成的别名就是字段名字本身。在这个例子中,如果没有指定别名,自动生成的别名将会是"title"。如果该别名已经存在,那么别名就是表名+字段名。在这个例子中,它将会是"n_title"。如果这个别名也存在了,那么就会在这个别名后面追加一个计数器,直到找到一个未使用的别名,比如"n_title_2"。
注意,如果你自己创建并填充查询,并且没有指定别名,而默认的别名又不可用,通常此时你的代码中肯定存在一个bug。然而,如果你正在实现hook_query_alter()钩子,由于你不确定都已经使用了哪些别名,所以你应该总是使用自动生成的别名。
注意,为了选择多个字段,只需要简单得依次调用addField()多次就可以了。注意,在大多数的情况下,字段之间的顺序并不影响结果;如果它影响了,通常意味着该模块中存在一个漏洞。
我们也可以使用fields()方法,一次添加多个字段:
<?php
$query->fields('n', array('nid', 'title', 'created', 'uid'));
?>
上述方法等价于调用四次addField(),一次对应一个字段。不过,fields()中不支持为字段指定别名。并且它返回的是查询对象本身,而不是返回生成的各种别名,此时可以对其继续使用链式方法。如果你需要知道生成的别名,你可以使用addField(),也可以使用getFields()来访问原始的内部字段结构。
调用fields()方法,而没有列出字段,此时相当于使用了一个"SELECT *"查询。
<?php
$query->fields('n');
?>
这将会在查询的字段列表中包含一个"n.*"。此时不会自动生成别名。如果使用SELECT *的表包含了一个字段,而这个字段又恰好在另一个表中被指定,此时,在结果集中,就有可能出现字段名字冲突。在这种情况下,结果集中,只会包含一个同名字段。因此,尽量不要使用SELECT *。
有些查询肯能回产生一些重复结果。在静态查询中,可以使用关键字"DISTINCT"来过滤重复的记录。在动态查询中,可以使用distinct()方法。
<?php
//强制过滤结果集中的重复记录。
$query->distinct()
?>
注意,DISTINCT可能会带来性能上的损失,只有在已经没有其它方法可用的情况下,才会考虑这种方式。
选择查询构建器支持在字段列表中使用表达式。表达式的例子包括“年龄字段的两倍”,“所有名字字段的总计”,或者是标题字符串的一个子集字符串。注意,很多表达式使用SQL函数,并不是所有的函数都可以跨数据库。因此模块开发者,需要自己确保只使用跨数据库的表达式。相关信息可参考:http://drupal.org/node/773090。
为了向一个查询添加表达式,可以使用addExpression()方法。
<?php
$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语句存在的情况下才起作用。对于查询本身的有效性,需要程序员自己来确保。
为了给动态查询添加一个order by语句,可以使用orderBy()方法:
<?php
$query->orderBy('title', 'DESC');
?>
上面的代码将会指示查询,按照标题字段的降序排列。第二个参数的可选值有"ASC"和"DESC",分别表示升序和降序,默认值为"ASC"。注意,这里的字段名字,应该是addField() 或addExpression()方法中创建的别名,所在,在大多数情况下,你会在这里使用这些方法的返回值,从而确保使用了正确的别名。为了按照多个字段排序,简单的依次调用orderBy()多次就可以了。
查询的随机排序,在不同的数据库系统上,所需要的语法也有所不同。因此,对于随机排序,最好使用动态查询。
为了让给定查询采用随机排序,需要在查询上面调用orderRandom()方法。
<?php
$query->orderRandom();
?>
注意,orderRandom()是一个链式方法,可以与orderBy()一起堆叠使用。也就是说,下面的代码是成立的:
<?php
$query->orderBy('term')->orderRandom()->execute();
?>
在上面的例子中,首先按照"term"字段排序,对于拥有同一术语的记录,则采用随机顺序。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
为了按照给定字段分组,可以使用groupBy()方法。
<?php
$query->groupBy('uid');
?>
上述代码将会指示查询按照uid字段分组。注意,这里的字段名,应该是由addField()、addExpression()等方法创建的别名,所以大多数情况下,你需要使用这些方法的返回值,从而确保使用了正确的别名。为了按照多个字段分组,只需要简单得依次调groupBy()多遍就可以了。
有时候,我们需要对查询返回的结果集做一下限制,取其特定的子集。通常这被称作“范围查询”。在MySQL中,通过使用LIMIT语句来实现这一点。为了限制一个动态查询的范围,可以使用range()方法:
<?php
$query->range(5, 10);
?>
上述代码指示结果集从第6个记录开始,而不是从第一个,并且只返回10条记录。通常我们需要的是返回“前n个记录”,为了实现这一点,需要把第一个参数设置为0,把第二个参数设置为n。
如果调用了range()两次,那么最后一次的调用,会覆写掉前面的。如果调用了这个方法,而不带任何参数,这意味着删除查询上面的所有范围限制。
为了生成一个可以按照任意一列排序的结果集表,可以使用TableSort扩展器,接着添加表头。注意,扩展器会返回一个新的查询对象,从这一点起,你使用的就是这个新的查询对象了。
<?php
$query = $query
->extend('TableSort')
->orderByHeader($header);
?>
一个连接就是类DatabaseConnection的一个对象,这个类继承自PDO类。Drupal要连接的每个数据库,都有一个唯一的连接对象与之关联。对于每个独立的驱动,该连接对象必须是一个子类。
为了访问(并打开,如果需要的话)一个连接对象,使用:
<?php
$conn = Database::getConnection($target, $key);
?>
为了访问当前连接,使用:
<?php
$conn = Database::getConnection();
?>
这将得到当前连接的默认目标。
注意,大多数情况下,你不需要直接请求连接对象。因为很多过程语句都帮你封装好了。除非你要连接多个数据库,并且需要做复杂的操作而又不想改变当前活动的数据库,此时你可以考虑直接访问连接对象。
为设置活动连接,使用:
<?php
db_set_active($key);
?>
关于连接的键和目标,参看下面的一节,数据库配置。
条件语句是一个很复杂的课题,在选择、更新、删除查询中,都会用到条件语句。因此,我们把它独立出来,单独讲解。与更新和删除查询不同,选择查询有两种类型的条件语句:WHERE语句和HAVING语句。Having语句效果和WHERE语句完全相同,唯一的区别是它使用方法havingCondition()和having(),而不是condition()和where()。
一旦构建好了查询,就可以调用execute()来编译和运行查询了。
<?php
$result = $query->execute();
?>
execute()方法将会返回一个结果集/语句对象,这与db_query()返回的结果完全一致,此外,它的迭代和获取方式也完全相同:
<?php
$result = $query->execute();
foreach ($result as $record) {
// Do something with each $record
}
?>
任何查询都有一个对应的“总计查询”。总计查询返回的是源查询的结果集记录的总数。为了获得一个总计查询,可以使用countQuery()方法。
<?php
$count_query = $query->countQuery();
?>
$count_query现在是一个新的动态选择查询,它没有排序限制,当执行时,它返回的结果只有一个值,也就是源查询匹配记录的总数。由于PHP支持在返回的对象上使用链式方法,下面的代码是常见的调用方式:
<?php
$num_rows = $query->countQuery()->execute()->fetchField();
?>
为了检查查询对象在其生命周期中的特定一点所构建的SQL查询,可以调用它的__toString()方法:
<?php
print_r($query->__toString());
?>
· 扩展器
· 修改查询
选择查询支持“扩展器”这一概念。一个扩展器,就是在运行时向选择查询添加功能的一种方式。添加功能的方式有两种,可以是附加新的方法,也可以是改变已有方法的行为。
对于熟悉面向对象设计模式的人来说,扩展器就是装饰模式的一个实现。它通过一种灵活的方式,而不是采用子类继承的方式,从而向对象动态的追加额外的扩展功能。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
为了使用一个扩展器,你首先需要有一个查询对象。extend()方法由查询对象返回一个新的对象,来替换原有的查询对象。例如:
<?php
$query = $query->extend('PagerDefault');
?>
上面的代码中,查询对象调用extend()方法,创建了一个新的PagerDefault查询对象,新对象包含了原来的选择查询,最后返回这个新创建的对象。现在,$query除了具有原来查询对象的各种功能以外,现在又有了新的附加方法可用。
注意,$query的变更不是当即生效的。从extend()中返回了一个新的对象,我们需要把这个对象存放在一个变量中,否则它就会丢失。例如,下面的代码就不会按照你的期望执行:
<?php
$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();
?>
为了避免这一问题,推荐大家在初次定义查询时,就对选择查询进行扩展。
<?php
$query = db_select('node', 'n')->extend('PagerDefault')->extend('TableSort');
$query->fields(...);
// ...
?>
这就确保了,从一开始,$query就是完全扩展后的对象。
还有需要注意的是,尽管扩展器可被堆叠调用(如上例所示),但是并不是所有的扩展器都相互兼容,并且扩展的先后顺序也有可能会对结果产生影响。例如,同时扩展了分页和表排序行为的查询,必须要先扩展PagerDefault。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
一个扩展器,就是一个简单的实现了SelectQueryInterface接口的类;并且在它的构造函数中,有两个参数:一个选择查询(或者是一个实现了SelectQueryInterface接口的对象)和一个DatabaseConnection对象。它必须重新实现SelectQueryInterface接口中的方法,并把它们传递给构造函数中指定的查询对象,在合适的时候返回自己。
在大多数情况下,所有的这些工作都可以通过继承SelectQueryExtender类来实现,因为后者帮我们处理了这些工作。因此,在实践中,一个扩展器就是SelectQueryExtender的一个子类。类的名字,就是在查询对象的extend()方法中指定的字符串。
那么,也就是扩展类负责添加或者覆写方法。对于它没有覆写的方法,将会透明的传递给包装后的查询对象。当覆写一个方法时,扩展器可能会也可能不会调用底层的查询对象,但是它必须返回SelectQuery接口规定的相同值。在大多数情况下,也就是查询对象本身,或者是扩展器对象本身。
下面的例子,可能帮我们更好的理解这一点。
<?php
class ExampleExtender extends SelectQueryExtender {
/**
* Override the normal orderBy behavior.
*/
public function orderBy($field, $direction = 'ASC') {
return $this;
}
/**
* Add a new method of our own.
*/
public function orderByForReal($field, $direction = 'ASC') {
$this->query->orderBy($field, $direction);
return $this;
}
}
?>
在上面的例子中,我们覆写了orderBy()方法,使其不做任何工作;同时添加了另一个方法,orderByForReal(),它实现了实际的排序行为。当然,这完全是一个例子,仅仅用来说明扩展器是如何工作的,它本身没有任何实际意义。注意,在两个方法中,被返回的$this就是扩展器对象本身。通过返回查询对象,就确保了扩展器没有“迷失”。
任何模块都可以声明一个扩展器。Drupal核心自带了两个非常有用的扩展器:PagerDefault 和TableSort。对于如何在你的代码中使用这些扩展器,可以参看对应的API文档。
动态选择查询的一个重要特性,就是其它模块能够修改它们。这就允许了其它模块把它们自己的限制注入到查询中,这可以是修改一个模块的行为,也可以是在查询运行时应用限制条件,比如节点访问限制。查询变更包含三个组成部分:tagging(标签化)、元数据(meta data)和hook_query_alter()。
任何动态的选择查询,都可以在它上面添加一个或多个标签。这些标签用来标示查询的类型,它允许hook_query_alter()钩子了来判定它们是否需要采取行动。标签应该是小写的字母数字字符,命名规则和PHP变量相同。也就是说,只能包含字母,数字,下划线,并且必须以字母开头。为了向查询中添加一个标签,可以使用addTag()方法:
<?php
$query->addTag('node_access');
?>
为了判定给定查询对象是否具有给定标签,有三种方法可用:
<?php
//如果这个查询对象拥有这个标签,返回TRUE。
$query->hasTag('example');
//如果查询对象具有所有的指定标签,返回TRUE。
$query->hasAllTags('example1', 'example2');
//如果查询对象具有任意一个指定的标签,返回TRUE。
$query->hasAnyTag('example1', 'example2');
?>
hasAllTags() 和hasAnyTag()可以带有任意数量的参数,每个参数表示一个标签。在这里,参数的位置对结果没有影响。
这里没有硬性规定使用哪些标签,但通常使用一些标准标签。下面是部分标准标签:
node_access
这个查询应该包含基于节点访问控制的限制。
translatable
这个查询应该包含可被翻译的列。
term_access
这个查询应该包含基于分类术语的限制。
views
这个查询由views模块生成
还可以为查询添加一些元数据,从而为alter钩子提供附加的上下文信息。元数据可以是任意的PHP变量,也可以用一个字符串作为键。
<?php
$node = node_load($nid);
// ... Create a $query object here.
$query->addMetaData('node', $node);
?>
元数据没有内在的意义,它本身对查询对象没有任何影响。它的存在仅仅是为了向alter钩子提供附加信息,并且通常只有当查询具有特定标签时才起作用。
为了访问一个查询上的给定元数据,使用getMetaData()方法。
<?php
$node = $query->getMetaData('node');
?>
如果没有为该键指定元数据,那么将会返回NULL。
一个查询就是传递给数据库连接的SQL语句。Drupal的数据库系统支持6种类型的查询:静态、动态、插入、更新、删除,还有合并。有些查询采用SQL字符串模板(准备语句)的形式,有些则采用面向对象的查询构建器。“查询对象”表示一个查询构建器的实例,适用于各种查询类型。
标签化和元数据本身并不起任何作用。它们的存在就是为hook_query_alter()提供信息,在hook_query_alter()中,可对选择查询作任何修改。
所有的动态选择对象都是通过execute()方法,在查询字符串被编译之前,传递给hook_query_alter()的。这就为模块提供了一个机会,方便它们按照自己的需要来操作查询。hook_query_alter()接收单个参数:选择查询对象本身。
<?php
/**
* 实现hook_query_alter()
*/
function example_query_alter(QueryAlterableInterface $query) {
// ...
}
?>
还有一个特定于标签的alter钩子,hook_query_TAG_NAME_alter(),它会在通用的alter钩子调用后,对于给定查询具有的所有标签,调用这个钩子。对于拥有标签'node_access'的查询,会为其调用下面的代码:
<?php
function example_query_node_access_alter(QueryAlterableInterface $query) {
// ...
}
?>
对于hook_query_alter(),有两点需要明确一下。
1. $query参数不是通过引用传递的。由于它本身是一个对象,由于PHP5及后续版本处理对象的特定方式,所以我们这里不需要使用引用。因此这里使用引用是没有必要的。另外alter钩子没有返回值。
2. 参数类型被明确指定为了QueryAlterableInterface。虽然不是必须的,但是明确的指定参数类型,可以更好的避免在运行时传递过来错误的变量类型。还有类型被指定为了QueryAlterableInterface,而不是简单的使用SelectQuery,这是为了提供更好的向前兼容性。
在alter钩子函数中,除了避免执行查询本身以外(因为这样会造成无限循环),我们可以对查询对象采取任何操作。 alter钩子可以根据查询上的标签和元数据来决定执行哪些动作。模块开发者,可以在查询对象上调用前面所列的各种附加方法,从而为查询添加额外的字段、关联、条件语句等等;也可以请求访问查询对象的内部数据结构,从而对它们进行直接操作。第一种方法主要用来向查询添加新的信息,而后一种方法允许alter钩子从查询中删除信息,或者操作已经排好队列的指令。
<?php
$fields =& $query->getFields();
$expressions =& $query->getExpressions();
$tables =& $query->getTables();
$order =& $query->getOrderBy();
$where =& $query->conditions();
$having =& $query->havingConditions();
?>
这里特别提醒的一点是,在上面的代码中,所有返回的值都是引用形式(=&),这样就确保了alter钩子访问的数据结构和对象的完全一致。上面的所有方法,返回的都是一个数组,它们的结构,在includes/database/select.inc中,SelectQuery的内联文档中有具体介绍。
插入查询必须使用一个查询构建器对象。对于LOB (大对象, 比如MySQL中的TEXT) 和BLOB(二进制大对象)字段,某些数据库需要特殊处理,所以我们需要一个抽象层,从而允许独立的数据库驱动,按照它们自己的要求实现自己的特殊处理。
插入查询使用db_insert()函数作为开始,如下所示:
<?php
$query = db_insert('node', $options);
?>
上述代码创建了一个插入查询对象,它将向节点表中插入一个或多个记录。注意这里没有为表名使用花括号,这是因为查询构建器能够自动的处理这一点。
插入查询对象使用的API具有链式特性。也就是说,所有的方法(除了execute()),返回的都是查询对象本身,这样,这些方法调用就可以采用链式结构了。在大多数情况下,这也就意味着,我们不需要将查询对象保存在一个变量中了。
插入查询对象支持多个不同的用法模式,用来满足不同的需求。在一般情况下,它的工作流程包括,指定需要插入到的字段,为这些字段指定将要插入的对应值,执行查询。下面列出了最常见的使用模式。
对于大多数插入查询,推荐的形式就是紧凑形式:
<?php
$nid = db_insert('node')
->fields(array(
'title' => 'Example',
'uid' => 1,
'created' => REQUEST_TIME,
))
->execute();
?>
上面的代码就等价于执行下面的查询:
INSERT INTO {node} (title, uid, created) VALUES ('Example', 1, 1221717405);
上面的代码,把插入流程的关键部分,链到了一起。
<?php
db_insert('node')
?>
这一行代码为节点表创建了一个新的插入查询对象。
<?php
->fields(array(
'title' => 'Example',
'uid' => 1,
'created' => REQUEST_TIME,
))
?>
fields()方法的参数有多种形式,但是最常见的就是单个关联数组。数组的键,就是要插入的列名,数组的值就是要插入的对应值。这将会为指定的表生成一个插入查询对象。
<?php
->execute();
?>
execute()方法告诉查询开始运行。除非调用这个方法,否则查询就不会执行。
对于查询对象上的其它方法,返回的都是查询对象本身,但是execute()方法与此不同,它返回的是插入查询向数据库中插入记录的自增字段(如果存在的话)的值。这也就是为什么,在上面的例子中,我们把它的返回值,分配给了$nid。如果没有自增字段,那么execute()的返回值就未被定义,此时就不应该使用它的返回值。
在大多数情况下,这就是插入查询的推荐形式。
<?php
$nid = db_insert('node')
->fields(array('title', 'uid', 'created'))
->values(array(
'title' => 'Example',
'uid' => 1,
'created' => REQUEST_TIME,
))
->execute();
?>
这段代码和前面的代码相比,输出的结果完全相同,就是罗嗦了一点。
<?php
->fields(array('title', 'uid', 'created'))
?>
当调用fields()时,使用的参数是一个索引数组而不是关联数组,此时它只设置了在查询中用到的字段(数据库列),而没有为它们设置值。这在插入多值的查询中非常有用,我们在后面会讲到这一点。
<?php
->values(array(
'title' => 'Example',
'uid' => 1,
'created' => REQUEST_TIME,
))
?>
这个方法指定一个关联数组,数组的结构就是左边的为要插入的字段名字,右边为对应的值。values()也可以使用索引数组。如果使用的是索引数组,那么值的顺序必须要与fields()方法中字段顺序保持一致。如果用的是关联数组,则数组内元素的顺序可以随意。一般情况下,为了代码的可读性,尽量选用关联数组的形式。
由于大家大多数时候都使用紧凑形式,所以这种查询形式很少使用。只有当运行多值插入查询时,才会将fields()和values()分开使用。
插入查询对象上还可以使用多值集合。也就是说,values()可被调用多次,从而将多个插入语句排在一起调用。这种形式的具体执行情况,则取决于数据库自身的能力。对于大多数的数据库,多值插入语句都会放在一个事务中一起执行,这样能够保证更好的数据一致性和执行效率。对于MySQL,它将会使用MySQL的多值插入语法。
<?php
$values = array(
array(
'title' => 'Example',
'uid' => 1,
'created' => REQUEST_TIME,
),
array(
'title' => 'Example 2',
'uid' => 1,
'created' => REQUEST_TIME,
),
array(
'title' => 'Example 3',
'uid' => 2,
'created' => REQUEST_TIME,
),
);
$query = db_insert('node')->fields(array('title', 'uid', 'created'));
foreach ($values as $record) {
$query->values($record);
}
$query->execute();
?>
在上面的例子中,我们把三个插入查询放在一个单元中执行,对于不同的数据库驱动,将会为其使用最有效的方法。注意,在这里我们把查询对象保存在了一个变量中,这样我们就可以对$values进行循环并重复的调用values()方法了。
在退化形式下,上面的例子等价于执行下面3条查询:
INSERT INTO {node} (title, uid, created) VALUES ('Example', 1, 1221717405);
INSERT INTO {node} (title, uid, created) VALUES ('Example2', 1, 1221717405);
INSERT INTO {node} (title, uid, created) VALUES ('Example3', 2, 1221717405);
注意,对于一个多值插入查询,execute()的返回值未被定义,因此不要调用它的返回值,数据库驱动的不同,也会对返回值产生影响。
如果你想使用其它数据库表中的结果来填充当前的数据库表,你可以使用SELECT语句将其取出,然后在PHP中对其进行迭代并将其保存到新表中去,另外你还可以使用INSERT INTO...SELECT FROM查询,在这里从SELECT查询返回的每条记录都会送入到INSERT查询中。
在这个例子中,我们想构建一个表"mytable",它包含两个字段节点ID和用户名,我们从数据库中取出所有page类型节点的对应信息,然后插入到表"mytable"中。
Drupal 6
<?php
db_query('INSERT INTO {mytable} (nid, name) SELECT n.nid, u.name FROM {node} n LEFT JOIN {users} u on n.uid = u.uid WHERE n.type = "%s"', array ('page'));
?>
Drupal 7
<?php
//构建选择查询
$query = db_select('node', 'n');
//关联users表
$query->join('users', 'u', 'n.uid = u.uid');
//添加我们想要的字段
$query->addField('n','nid');
$query->addField('u','name');
//添加一个条件,把节点限制在类型page上去。
$query->condition('type', 'page');
//执行插入
db_insert('mytable')
->from($query)
->execute();
?>
在正常情况下,如果你没有为给定字段指定一个值,而表的模式(schema)定义了默认值,那么数据库会自动的为你插入默认值。然而,在大多数的情况下,你需要明确的指示数据库使用默认值。它包括你是否想为整个记录使用所有的默认值。为了明确的告诉数据库,为给定的字段使用默认值,我们可以使用useDefaults()方法。
<?php
$query->useDefaults(array('field1', 'field2'));
?>
这段代码指示查询,为字段field1和field2使用数据库定义的默认值。注意,如果你指定的字段,已经在fields()或values()中使用,此时就会抛出一个异常,并提示你存在一个SQL错误。
更新查询必须使用一个查询构建器对象。对于LOB (大对象, 比如MySQL中的TEXT) 和BLOB(二进制大对象)字段,某些数据库需要特殊处理,所以我们需要一个抽象层,从而允许独立的数据库驱动,按照它们自己的要求实现自己的特殊处理。
更新查询使用db_update()函数作为开始,如下所示:
<?php
$query = db_update('node', $options);
?>
上述代码创建了一个更新查询对象,它将修改节点表中的一个或多个记录。注意这里没有为表名使用花括号,这是因为查询构建器能够自动的处理这一点。
更新查询对象使用的API具有链式特性。也就是说,所有的方法(除了execute()),返回的都是查询对象本身,这样,这些方法调用就可以采用链式结构了。在大多数情况下,这也就意味着,我们不需要将查询对象保存在一个变量中了。
更新查询的结构是非常简单的,它包含一个要设置的键值对儿集合,还有一个WHERE语句。有关WHERE语句的结构,我们在这里只是简单的说明,更详细的介绍可参看后面的条件语句一节。
典型的更新查询如下所示。
<?php
$num_updated = db_update('node')
->fields(array(
'uid' => 5,
'status' => 1,
))
->condition('created', REQUEST_TIME - 3600, '>=')
->execute();
?>
上面的查询将会更新节点表中最近一小时内创建的所有记录,把它们的uid字段设置为5,把status字段设置为1。fields()方法中的参数是单个关联数组,用来指定要设置哪些字段以及对应的值,当指定的条件满足时,就会进行相应更新。注意,与插入查询不同,UpdateQuery::fields()只接收一个关联数组。还有,数组中字段的顺序,以及方法的调用顺序,与结果无关。
上面的代码就等价于执行下面的查询:
UPDATE {node} SET uid=5, status=1 WHERE created >= 1221717405;
execute()将返回查询影响到的记录总数。注意,影响到的与匹配到的通常不一致。在上面的查询中,如果一个已有记录,它的uid为5,status为1,并且满足条件语句,此时它就是匹配的,但是由于它里面的数据没有被修改,也就是它没有被查询影响到,因此不会记入到返回值中。作为一个副作用,这使得更新查询无法判定一个记录是否存在。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
删除查询必须使用一个查询构建器对象。它们使用db_delete()函数作为开始,如下所示:
<?php
$query = db_delete('node', $options);
?>
上述代码创建了一个删除查询对象,它将会删除节点表中的对应记录。注意这里没有为表名使用花括号,这是因为查询构建器能够自动的处理这一点。
删除查询对象使用的API具有链式特性。也就是说,所有的方法(除了execute()),返回的都是查询对象本身,这样,这些方法调用就可以采用链式结构了。在大多数情况下,这也就意味着,我们不需要将查询对象保存在一个变量中了。
删除查询的结构是非常简单的,它只包含一个WHERE语句。有关WHERE语句的结构,我们在这里只是简单的说明,更详细的介绍可参看后面的条件语句一节。
下面是一个完整的删除查询示例。
<?php
$num_deleted = db_delete('node')
->condition('nid', 5)
->execute();
?>
上面的查询将会删除{node}表中nid为5的所有记录。它等价于执行下面的查询:
DELETE FROM {node} WHERE nid=5;
execute()方法将会返回被删除的记录数量,作为查询的结果。
合并查询是一种特殊的混合查询类型。虽然在SQL2003规范中为它们定义了语法,但实际上几乎没有任何数据库支持该标准语法。然而,大多数数据库通过使用特定于数据库的语法,都提供了一些替代实现。在Drupal中,合并查询构建器把合并查询的概念抽象成为了一个结构化的对象,根据数据库的不同,它们会被编译成特定于数据库的语法。
在一般意义上,合并查询就是插入查询和更新查询的联合体。如果满足了给定条件,比如包含给定主键的记录已经存在,此时运行一个更新查询。如果给定条件未满足,则运行其它查询,比如插入查询。在大多数情况下,它等价于:
<?php
if (db_query("SELECT COUNT(*) FROM {example} WHERE id=:id", array(':id' => $id))->fetchField()) {
// Run an update using WHERE id = $id
}
else {
// Run an insert, inserting $id for id
}
?>
实际的具体实现,在不同的数据库之间,大不相同。注意,尽管合并查询在概念上是一个原子操作,但它们是否真的是原子操作,则取决于具体数据库的实现。例如,在MySQL中,它实现为了单个原子查询,不过上面的退化情况则不是原子操作。
下面列出了合并查询的常见的习语。
语句对象就是选择(Select)查询的结果。它的类型应该是DatabaseStatement,或者是DatabaseStatement的子类。DatabaseStatement扩展了PDOStatement类。
Drupal为所有的查询使用预处理语句(prepared statements)。一个预处理语句就是一个查询的模板,将会向模板中填充数值然后执行。我们可以把预处理语句看作是SQL的函数形式,需要向里面传递一些参数才可使用。
在正常的PDO中,首先必须明确的准备一个语句对象,然后把相应的值绑定到查询中的占位符上,并执行查询。接着,语句可以作为结果集被迭代处理。也就是说,在语句执行以后,语句和结果集就成为了同义词。
Drupal没有直接暴露预处理语句对象。模块开发者需要使用一个查询对象或者一个SQL字符串来执行查询,接着该查询就会返回相应的语句对象。因此,术语“语句对象”和“结果集对象”表示的含义基本相同。
<?php
db_merge('example')
->key(array('name' => $name))
->fields(array(
'field1' => $value1,
'field2' => $value2,
))
->execute();
?>
在上面的例子中,我们指示查询在"example"表上进行操作。我们接着指定了一个主键字段,'name',它的值为$name。我们接着指定了要设置的值的数组。
如果存在这样一个记录,它的"name"字段的值为$name,那么在这个记录中,字段field1和field2将会被设置为对应的值。如果不存在这样的记录,那么就会创建一个,其中"name"的值为$name,"field1"的值为$value1,"field2"的值为$value2。因此,在查询的最后,无论记录是否已经存在,最终的结果都是相同的。
在有些情况下,我们可能会需要根据记录是否存在(由key()中的字段标识),来有条件的设置值。这样做有两种方式。
<?php
db_merge('example')
->key(array('name' => $name))
->fields(array(
'field1' => $value1,
'field2' => $value2,
))
->update(array(
'field1' => $alternate1,
))
->execute();
?>
上面的例子和最初的例子相比,在对应记录不存在时,结果完全相同;在记录已经存在时,在这里我们更新它,把field1字段的值设置为$alternate1,而不是$value1,对于field2字段则不作任何操作。update()方法的参数可以是一个关联数组,也可以是两个数值数组,一个字段一个值,并且必须使用相同的顺序。
<?php
db_merge('example')
->key(array('name' => $name))
->fields(array(
'field1' => $value1,
'field2' => $value2,
))
->expression('field1', 'field1 + :inc', array(':inc' => 1))
->execute();
?>
在这个例子中,如果记录已经存在,那么field1的值将被设置为当前值加1。如果当特定事件发生时你想为计数器作加法,对于这种计数器查询,上述方式非常有用。无论记录是否存在,field2仍然被设置为相同的值。
注意, expression()可被调用多次,每次对应一个需要设置为表达式的字段,如果记录存在,就会对该字段按表达式进行设置。第一个参数是字段;第二个参数是一个SQL片断,也就是表达式,它用来指示应该如何设置字段;第三个参数是可选的数组,里面包含了占位符的值,这些值将插入到表达式中。
如果一个字段已经出现在了fields()当中,这个字段仍然可以用在expression()中,两者之间没有冲突关系。
<?php
db_merge('example')
->key(array('name' => $name))
->fields(array(
'field1' => $value1,
'field2' => $value2,
))
->updateExcept('field1')
->execute();
?>
updateExcept()方法,可以使用由字段构成的数组作为参数,也可以使用一列字段分别作为单独的参数。对于updateExcept()中指定的字段,如果记录已经存在,那么它们就不受影响。也就是说,如果存在一个name = $name的记录,那么field2将被设置为$value2,而field1将被完全忽略,原来什么值就是什么值,但是如果记录不存在,那么它会被设置为$value1。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
使用上面的API,程序员很有可能定义出来没有任何意义的查询,比如一个字段,在假定记录存在的情况下,既被设置为了忽略,又被设置为了表达式。下面的规则,可以帮助减少潜在的错误:
· 如果一个字段设置成一个expression(),那么它的优先级高于update()和updateExcept()。
· 如果在update()中已经指定了值,那么updateExcept()将被忽略。也就是update()的优先级高于updateExcept()。
· 如果值被指定在update()中,那么在记录已存在时,只有这些字段才会被修改。没有在update()中指定的字段将不受影响。
即便是遵守这些规则,仍然有可能定义出来没有意义的查询。查询的有效性,需要开发者自己来掌握,尽量避免定义出没有意义的查询。
“条件语句”是查询的一部分,它通过特定的条件,来限制匹配的记录。在SQL中,它就是SELECT、 UPDATE、或DELETE查询中的WHERE或HAVING部分。在Drupal的所有动态查询中,条件语句的实现机制都相同。如果没有特别说明,下面所讲的适用于所有的查询类型。
条件语句片断
条件语句片断是条件语句自包含的一部分。
连接词
在条件语句中,条件语句片断使用连接词连接。连接词通常为AND或OR,它能够把两个语句连到一块。
条件语句对象
Drupal把所有的条件语句片断都处理成了QueryConditional类的实例。一个条件语句对象就是该类的一个实例。
作为一个例子,下面的查询可以这样分解:
查询:
SELECT FROM {mytable} WHERE (a = 1 AND b = 'foo' OR (c = 'bar'))
条件语句:
WHERE (a = 1 AND b = 'foo' OR (c = 'bar'))
条件语句片断:
(a = 1 AND b = 'foo' OR (c = 'bar'))
(c = 'bar')
连接词:
AND, OR
选择、更新、和删除查询对象都实现了QueryConditionalInterface接口,这样它们在条件语句上的接口是完全相同的。在内部,它们封装了一个QueryConditional对象。也可以直接实例化QueryConditional类。
条件语句中的每一片断都是使用连接词连在一起的。如果一个条件语句包含多个片断,那么片断之间将会使用指定的连接词。默认情况下,连接词为AND。每个条件语句片断本身就是一个带有不同连接词的条件语句对象,这样就允许条件语句中片断之间的相互嵌套。这样,就可以构建任意复杂的条件语句了。
对于所有的条件语句对象,主要使用的方法有两个:
$query->condition($field, $value = NULL, $operator = '=')
condition()方法允许添加一个标准的条件语句片断,其形式为$field $value $operator。它包含了二进制比较的各种情况,比如=、 <,、>=、 LIKE、等等。如果没有指定操作符,则使用默认的=。这意味着,最常见的情况的就是condition('myfield', $value),它生成一个条件语句片断myfield = :value;其中,当查询运行时,:value将被替换为$value。
$query->where($snippet, $args = array())
where()方法允许使用任意的SQL作为条件语句片断。$snippet可以包含任何合法的SQL片断,如果它里面包含了变量内容,那么必须使用命名占位符的形式。$args是占位符数组,它里面的值将会替换到SQL片断中去。开发者需要自己确保SQL的有效性。不要对SQL片断作任何特定于数据库的修改。
在大多数的情况下,推荐使用condition()方法,除非出现$field $value $operator形式不再适用的情况,比如当你需要使用表达式时,或者一个条件作用于两个字段时。两个方法返回的都是对应的条件语句对象,所以它们可被无限的链式调用。
condition()还可以用于一些其它特殊情况。
一些运算符用于value参数为数组时。最常用的就是IN 和BETWEEN。那么$value应该是一个数组,它包含了字段可能等于的值。因此,下面的调用将会这样解析:
<?php
$query->condition('myfield', array(1, 2, 3), 'IN');
//变成了: myfield IN (:db_placeholder_1, :db_placeholder_2, :db_placeholder_3)
?>
如果运算符为BETWEEN,那么$value是一个包含两个元素的数组,字段位于两者之间。例如:
<?php
$query->condition('myfield', array(5, 10), 'BETWEEN');
//变成了: myfield BETWEEN :db_placeholder_1 AND :db_placeholder_2
?>
condition()的第一个参数,也可以是一个条件语句对象。内部的条件语句对象将被纳入到外部的条件语句中去,并放在括号中间。内部对象所使用的连接词,可以与外部不同。这样,就可以通过“自下而上”的方式,来创建条件语句对象,从而就可以构建一个复杂的嵌套的条件语句了。
db_condition()帮助函数将返回一个新的条件语句对象。它只有一个参数,就是对象使用的连接词。一般情况下,帮助方法db_and()、db_or()、和db_xor()就可以涵盖大多数情况了。它允许条件语句以内联的方式插入到查询中,这样代码看起来更加紧凑。
例如,看一下下面的结构体:
<?php
$query
->condition('field1', array(1, 2), 'IN')
->condition(db_or()->condition('field2', 5)->condition('field3', 6))
// 对应结果:
// (field1 IN (:db_placeholder_1, :db_placeholder_2) AND (field2 = :db_placeholder3 OR field3 = :db_placeholder_4))
?>
在有些情况下,我们可能会根据一个字段的值是不是NULL来进行过滤。当然,此时也可以使用condition(),不过我们更推荐使用下面的实用方法,因为它们更易于理解:
<?php
$query->isNull('myfield');
//结果为:(myfield IS NULL)
$query->isNotNull('myfield');
//结果为:(myfield IS NOT NULL)
?>
两个方法都可被链式调用,根据需要可以和condition()、where()结合使用。
定义一个数据库连接的主要方式,是使用settings.php中的$databases数组。从它的名字,我们就可以看出,$databases允许定义多个数据库连接。它还支持定义多个目标。只有在对该数据库运行查询时,才会打开对应的数据库连接,或者说是创建相应的连接对象。
condition()中的$value还可以使用子查询的形式。为了使用子查询,首先需要使用db_select()构建一个SelectQuery对象。接着,我们不执行这个Select查询,而是将其作为condition()的value参数传递给另一个查询。当主查询执行时,它将被自动的集成过来。
子查询通常只在两种情况下使用:当子查询生成的结构只包含一行并且只有一个值时,同时运算符为=、<、>、<=、>=;或者当子查询只返回单列信息,而运算符为IN。除此之外,大多数其它的关联方式都会造成语法错误。
注意,在一些数据库上,特别是MySQL,条件语句中的子查询,运行效率有点慢。如果可能的话,尽量在FROM语句中使用关联、子查询,或者使用多个简单的条件语句片断来代替子查询。
下面有几个例子,希望能够更好的帮助理解条件语句。为了清晰起见,我们在注释中给出等价的查询字符串,当然在实际的应用中,将会使用占位符和预备语句(prepared statements)。
<?php
db_delete('sessions')
->condition('timestamp', REQUEST_TIME - $lifetime, '<')
->execute();
// DELETE FROM {sessions} WHERE (timestamp < 1228713473)
?>
<?php
db_update('sessions')
->fields(array(
'sid' => session_id()
))
->condition('sid', $old_session_id)
->execute();
// UPDATE {sessions} SET sid = 'abcde' WHERE (sid = 'fghij');
?>
<?php
// From taxonomy_term_save():
$or = db_or()->condition('tid1', 5)->condition('tid2', 6);
db_delete('term_relation')->condition($or)->execute();
// DELETE FROM {term_relation} WHERE ((tid1 = 5 OR tid2 = 6))
?>
这部分内容很快就会加上!webchick坚持让我的文档写了多少,发布多少。到目前为止,有关数据库驱动的还没有写好。我将尽快地把它补上来。在此以前,大家可以参考对应的单元测试(http://cvs.drupal.org/viewvc.py/drupal/drupal/modules/simpletest/tests/database_test.test?&view=markup)。
Drupal7使用PDO(PHP数据对象)来访问数据库。更多信息可参看:http://drupal.org/node/549702。
数据库API在遇到错误时,就会抛出异常,我们可以把数据库操作放在try {} catch() {}区块中,这样异常就会被我们抓住,相关信息可参看http://api.drupal.org/api/group/database/7中的最后一个例子。
Drupal还支持事务,对于那些不支持事务的数据库,Drupal还包含了一个透明的回退。然而,当你在同一时间,尝试并启动两个事务时,事务就会变得复杂起来。数据库不同,此时的行为也不相同。
在C/C++里面的嵌套锁中,也存在类似的问题。如果代码已经获得锁A,并尝试去获取锁A时,代码就会进入死循环。你可以在代码中添加检查语句,如果已经获得了锁,那么就不再尝试获取它了,这样就可以避免死循环;但是你也可以提前释放锁。
在SQL中,我们也存在同样的问题。如果你的代码已经处于一个事务中了,那么启动一个新的事务,这会给你带来非常惊讶和不幸的后果。
Java通过支持类似于我们下面测试所用的嵌套结构,成功解决了它的锁嵌套问题。Java允许你把函数标记为“同步”("synchronized"),函数在运行前,首先等待锁的释放;当它获得了锁以后,就会执行;当它不再需要时,就会把锁释放。如果在同一个类中,一个同步函数调用另一个,Java将会追踪锁的嵌套。外部的函数获取锁,内部函数执行无锁操作;当外部函数返回时,释放锁。
尽管我们不能在PHP中把函数声明为“事务的”,但是通过使用带有构造函数和析构函数的对象,我们就可以模拟Java的嵌套逻辑。在一个函数中,在它的第一个操作或者接近于第一个操作的地方,简单的调用"$txn = db_transaction();",就使得函数具有事务特性了。如果一个事务函数调用了另一个,我们的事务抽象层通过在内部嵌套层执行非事务操作(至少在数据库看来是这样),来实现嵌套。
为了启动一个新事务,在你的代码中简单的调用$txn = db_transaction();即可。只要变量$txn仍然在范围内,那么事务就会保持打开的状态。当$txn被销毁时,事务将被提交。如果你的事务嵌套在另一个里面,那么Drupal会追踪每一个事务,只有当最后一个事务对象超出范围,也就是所有相关的查询都成功执行完毕时,Drupal才会提交最外面的事务。
例如:
<?php
function my_transaction_function() {
//在这里打开事务
$txn = db_transaction();
$id = db_insert('example')
->fields(array(
'field1' => 'mystring',
'field2' => 5,
))
->execute();
my_other_function($id);
return $id;
//在这里,$txn超出了范围,整个事务被提交。
}
function my_other_function($id) {
//此处,事务仍然是开着的。
if ($id % 2 == 0) {
db_update('example')
->condition('id', $id)
->fields(array('field2' => 10))
->execute();
}
}
?>
我们已经讲了很多有关数据库API函数的链式调用了,例如:
<?php
$result = db_select('mytable')
->fields('mytable')
->condition('myfield', 'myvalue')
->execute();
?>
然后,并不是所有的函数都可以采用这种链式结构调用,有些函数不能采用链式调用,否则就会导致你的代码不能正常工作。
对于不能使用链式形式的函数,只能按照下面的方式调用:
<?php
$query = db_select('mytable');
$query->addField('mytable', 'myfield', 'myalias');
$query->addField('mytable', 'anotherfield', 'anotheralias');
$result = $query->condition('myfield', 'myvalue')
->execute();
?>
如果一个函数是链式的,那么它的返回值,必须是查询对象本身。为了查看任意数据库API函数的返回值,可以参看http://api.drupal.org。
在这个页面,我们列出了一些常用的链式函数,和一些非链式函数,方便大家快速查看,不过这里面并没有涵盖所有的函数。如果你觉得哪个函数比较常用,也可以将其添加进来。
注意:如果一个函数不是链式的,这只意味着你不可以在它的后面链接更多的函数,但是你仍然可以在它前面使用链式函数。
下面是链式函数:
· fields()
· where()
· isNull()
· having()
· union()
· exists()
· range()
· addTag()
下面是非链式函数:
· join()
· extend()
Drupal的数据库层,没有为SQL函数提供跨数据库的抽象。为了让你的代码更好的兼容所支持的数据库引擎,你使用的函数应该符合ANSI标准,并且在Drupal支持的所有数据库中都能正常工作。下面是一个正在完善的列表。这里所列的,是推荐大家使用的,其它的语法变体可能不垮数据库。
注意,数据库层没有提供运算符的白名单,所以你可以使用非标准的函数,比如REPLACE(),对于支持该语法的数据库,代码是可以正常工作的。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
连接键是给定数据库连接的唯一标识符。对于给定站点,连接键必须是唯一的,并且必须有一个"default"连接,用作Drupal的主数据库。对于大多数站点,这可能也是唯一定义的连接。
CONCAT(string1, string2)
SUBSTRING(string, from, length)
SUBSTRING_INDEX(string, delimiter, count)
LENGTH(string)
GREATEST(num1, num2)
POW(num1, num2)
LOG(base, value)
RAND()
COUNT(expression)
SUM(expression)
AVG(expression)
MIN(expression)
MAX(expression)
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
通过本章的学习,你应该可以
了解数据库API的一般概念
熟悉数据库的配置
能够熟练的掌握静态查询,
能够熟练的掌握动态查询,插入、更新、删除、合并查询
熟悉条件语句
了解事务机制和链式结构
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
一个给定的连接键,必须有一个或者多个目标。一个目标就是一个可选的备用数据库。如果请求的目标没有定义,系统就会自动采用“默认”目标,这是必须要定义的。
目标的主要用途就是主从数据库。默认目标是主SQL服务器。接着可以定义一个或多个“从”目标。对于标记为尝试使用从服务器的查询,如果从服务器可用,那么它们就会尝试访问“从”目标。如果有一个从服务器可用,那么就会打开对应的连接,并运行相应的查询。如果没有从服务器可用,那么查询就会运行在主服务器上。这样,就有了一个透明的回退,如果有从服务器可用,代码就会利用从服务器,如果没有从服务器可用,代码无需修改,仍然能够正常工作。
作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com
$databases数组是一个至少包含3层的嵌套数组。第一层定义了数据库的键。第二层定义了数据库目标。每个目标的值就是对应的连接信息。通过实例我们可以更好的理解这一点。
<?php
$databases['default']['default'] = array(
'driver' => 'mysql',
'database' => 'drupaldb',
'username' => 'username',
'password' => 'secret',
'host' => 'localhost',
);
?>
上面的$databases数组定一个单个的连接键("default"),单个的目标("default")。连接采用的数据库为MySQL,数据库名字为“drupaldb”,访问数据库的用户名密码分别为“username”、“secret”,数据库所在的主机为“localhost”。上面的例子代表着Drupal单SQL服务器安装的典型情况,对于大多数站点,这就足够了。
对于主/从服务器的配置,我们则需要这样定义:
<?php
$databases['default']['default'] = array(
'driver' => 'mysql',
'database' => 'drupaldb1',
'username' => 'username',
'password' => 'secret',
'host' => 'dbserver1',
);
$databases['default']['slave'][] = array(
'driver' => 'mysql',
'database' => 'drupaldb2',
'username' => 'username',
'password' => 'secret',
'host' => 'dbserver2',
);
$databases['default']['slave'][] = array(
'driver' => 'mysql',
'database' => 'drupaldb3',
'username' => 'username',
'password' => 'secret',
'host' => 'dbserver3',
);
?>
这个定义提供了一个“主”服务器和两个“从”服务器。注意,“从”服务器的键是一个数组。如果有目标被定义成为连接信息的数组形式,那么对于该目标的每个页面请求,系统会随机的从中选择一个服务器。也就是说,对于一个页面请求,所有的从查询会发送给dbserver2,而对于下一个请求,则可能都会发送给dbserver3。
<?php
$databases['default']['default'] = array(
'driver' => 'mysql',
'database' => 'drupaldb1',
'username' => 'username',
'password' => 'secret',
'host' => 'dbserver1',
);
$databases['extra']['default'] = array(
'driver' => 'sqlite',
'database' => 'files/extradb.sqlite',
);
?>
这个配置定义了一个单独的主Drupal数据库,和一个键为"extra"的附加数据库,后者采用SQLite。注意SQLite的连接信息的结构和MySQL的大不相同。对于每个驱动,由于情况不同,所以可以有不同的配置。
注意,无论你定义了多少个连接,Drupal只有在该连接被用到时才会打开它。