“中国要复兴、富强,必须在开源软件领域起到主导作用,为了国家安全和人类发展,责无旁贷,我们须为此而奋斗”——By:云客
11. 数据库系统及其使用
在开始本主题前请允许一点点题外话:
在我写这个博客的时候(2016年10月28日),《Begining Drupal 8》这本书已经翻译完成并做成了PDF格式供给大家免费下载,这是一本引导新人学习drupal8的入门级教程,由drupal中文社区站http://drupalchina.cn/的站长龙马组织翻译,有20位奉献者进行了大半年的工作得以完成,很荣幸我也是其中之一,用以进行这项工作的qq群号是:342823468,在这个群里诞生了第一本drupal8中文教程,这件事真的很赞!群里的20位翻译者真的很赞!目前国内没有一个由社区开发的php内容管理系统,而建立一个社区cms对大众又是多么有益,drupal在国际上如此流行,众人聚焦精力对它精雕细琢造就了不错的品质,延展使用范围,快速迭代,以至于许多知名机构和公司用它做官网,而在国内尽管发展速度还不错,但中文资料匮乏和缺乏系统整理严重影响了很多新人的步伐,这也是20位翻译者无偿劳动的意义所在,希望国内社区越来越大,这样大家都有益处,一个人是创作不了LINUX那样的伟业的,人多才能有生态,有生态才能反哺大家,这也是我写云客drupal8源码分析的一个愿望,希望越来越多人加入这个社区。
好了,下面开始本篇的主题,主要讲解drupal8数据库系统的实现和使用方面的知识:
Symfony没有数据库组件,drupal8完全自己实现了一个基于php的pdo扩展的数据库系统,它提供了一个数据库抽象层,让你可以使用统一的方式去操作数据库,而不用管底层使用的是什么数据库,只需要使用好它提供的接口(对象方法或函数)就行,当需要更换另外类型的数据库时,比如由MySQL换成Oracle或MS SQL Server,无需修改应用层代码。在发布版中默认提供了mysql、pgsql、sqlite支持,
它也提供多台数据库服务器支持,简单的负载均衡很容易实现,在学习它之前建议你对以下内容有所了解:
1:php的PDO扩展,官方地址是:http://php.net/manual/zh/intro.pdo.php
这是php层面提供的数据库抽象层,用以统一各类数据库的操作,drupal8的数据库系统是基于它实现的,了解它后在学习的过程中就可以清楚知道什么事情是谁做的以及它们是怎么配合的
2:SQL标准
SQL99也叫作SQL:1999、SQL3 、 SQL-99. 是sql语句的标准https://en.wikipedia.org/wiki/SQL:1999,还有ANSI SQL:2003,他们定义SQL语句的标准和数据库应该具有的行为,了解这以后在看drupal8数据库的sql语句构建时就不会迷惑了
3:软件设计模式
在drupal8的设计中到处是模式,数据库系统中装饰者模式尤为突出,了解这后看代码会轻车熟路,会有很熟悉轻松的感觉,但不建议买大部头的书看,网上很多帖子足以,几个列子就能让你明白,节省时间
drupal官方的数据库文档:https://www.drupal.org/developing/api/database,注意D7和D8版本大同小异,看完本文有不清楚的请到那里补充。
下面先看一看数据库连接信息的定义:定义位于站点配置文件中/sites/default/settings.php,你可以看到类似下面的定义:
$databases['default']['default'] = array (
'database' => 'drupal',
'username' => 'yunke',
'password' => 'yunke',
'prefix' => 'yunke_',
'host' => 'localhost',
'port' => '3306',
'namespace' => 'Drupal\\Core\\Database\\Driver\\mysql',
'driver' => 'mysql',
);
$databases['default']['default']的值是一个数组,这个数组称之为DSN(数据源名称Data Source Name),它代表一个数据库,包含连接所需的所有信息
为叙述简单后面我们将这个数组表示成$dsn
像上面的代码:$databases['default']['default']=$dsn;可能会让你疑惑,为什么是嵌套数组?有什么含义?
这就是drupal提供多数据库支持的体现,以下我们用$databases[$key][$target]=$dsn;来表示
$key指一个数据库,代表一个单独系统,不同的$key通常是不同结构的数据库,当和drupal以外的第三方系统协作时其他系统的数据库就应该是不同的$key
默认的drupal只有一个$key,它被命名为default,它是drupal使用的数据库
$target是什么呢?它主要用于负载均衡,一个$key对应的数据库,他们有相同的结构和数据,可以被分布在多台服务器上面,以减轻压力
那么每一台服务器就对应一个$target,比如MySQL数据库内建了一个主从备份的功能,在一个服务器上面的MySQL实例可以快速同步到其他服务器
那么就可以有多个服务器拥有相同的数据库结构和数据,在非写入查询的时候能分摊访问实现负载均衡,但写入更新一类的查询只能在主服务器上面运行
(关于Mysql数据库的主从复制推荐大家看《高性能MySQL》一书Baron Schwartz,Peter Zaitsev,Vadim Tkachenko 著;中文有售)
明白$databases['default']['default']的意思了吧,这也是大多数小型网站所需要的配置
表示有一个叫做default的数据库,这个数据库只有一个叫做default目标实例的服务器在运行,所有读写操作都在这个服务器上面
那么怎么配置多数据库呢?可以这样:
$databases['default']['default']=$dsn_1; //主服务器
$databases['default']['replica']=$dsn_2; //用于只读的从服务器
上面实现了两台服务器,那么要实现多个从服务器该怎么操作?可以这样:
$databases['default']['default']=$dsn_1; //主服务器
$databases['default']['replica']=$dsn_2; //用于只读的从服务器
$databases['default']['yunke']=$dsn_3; //用于只读的从服务器
这样虽然可以,但是每次获得链接时都要指定$target很不方便,
况且同一个$key下面只有主服务器可以写,多个$target从服务器是一样的,且都是只读,所以基本使用下面的方式:
$databases['default']['default']=$dsn_1; //主服务器
$databases['default']['replica'][]=$dsn_2; //用于只读的从服务器
$databases['default']['replica'][]=$dsn_3; //用于只读的从服务器
$databases['default']['replica'][]=$dsn_4; //用于只读的从服务器
drupal在一个$target下有多个$dsn时,它会随机选择一个,就实现了负载分摊,如无特别需要基本使用以上方式
注意:在一个$key下必须要有一个$target被命名为default,且它作为主服务器,在程序内部当其他$target不可用时默认回退到default
关于写查询的负载均衡比较复杂,比如数据分片技术,需要应用层规划,定义额外的键,请看上面推荐的书
明白了$key和$target后我们看一看$dsn这个数组是怎么定义的:
$dsn= array (
'database' => 'drupal',
'username' => 'yunke',
'password' => 'yunke',
'prefix' => 'yunke_',
'host' => 'localhost',
'port' => '3306',
'namespace' => 'Drupal\\Core\\Database\\Driver\\mysql',
'driver' => 'mysql',
);
以上是最基本的信息,大部分你应该能看懂,主要讲以下内容:
表前缀只有一个字符串值的时候,表示所有数据表均使用该前缀,不用请为空或者省略该数组元素
很赞的是drupal支持为不同表运用不同前缀,使用如下:
'prefix' => array("default"=>"默认前缀","基本表名"=>"特定前缀","基本表名"=>"特定前缀"),
在程序内部就被转化成这样的格式,其中default必不可少,用于指定没有特定前缀的表默认使用的前缀,不需要请='',
指定名字空间:'namespace'=> 'Drupal\\Core\\Database\\Driver\\mysql',
这个表示drupal数据库抽象层使用特定数据库驱动程序的名字空间,不加也可以,但最好加上,可提高程序速度,避免自动通过反射机制得到
在$dsn中还可以指定其他配置项,通常不同数据库有不同配置,但也有一些通用配置,下面说说常用的MySQL可使用的配置项:
$dns['_dsn_utf8_fallback'] = TRUE
drupal默认使用utf8mb4数据库字符编码,它是utf8的超集,此指令表示回退到utf8编码,不使用utf8mb4
默认使用utf8mb4,如果已经使用utf8mb4后不能回退到低版本字符编码的数据库,这样会丢失超集部分的数据
详见:http://dev.mysql.com/doc/refman/5.5/en/charset-unicode-utf8mb4.html
$dns['unix_socket']使用一个unix_socket
$dns['pdo']允许的pdo选项,用于控制pdo的行为
$dns['collation']运行设置mysql的字符集语句时:SET NAMES ' . $charset . ' COLLATE ' . $dns['collation'],没有这个指令则设置:'SET NAMES ' . $charset
$dns['init_commands']执行mysql的初始化命令
$dns['transactions']表示是否支持事务,没有设置则默认支持事务,除非明确设置为false以使强制不支持事务
以上就是关于如何定义数据库连接选项的内容,下面说说drupal允许使用的数据库命名规则:
drupal使用的数据库名、表名、字段名必须是字母、数字、下划线和点号
判定规则是:preg_replace('/[^A-Za-z0-9_.]+/', '', $database);
他们的别名不能有点号(别名就是查询语句as后面的名字),规则是preg_replace('/[^A-Za-z0-9_]+/', '', $field);
接下来看看怎么进行数据库查询:
在整个drupal程序运行开始阶段就把数据库的配置信息注入到了数据库控制类中,由DrupalKernel完成调用
具体是在Drupal\Core\Site\Settings 的 initialize方法调用Database::setMultipleConnectionInfo($databases);
当需要查询的时候首先需要获得数据库连接类,在模块中你可以这样操作:
\Drupal::database(); //获取配置中$databases['default']['default']表示的链接,这对于大多数只有一个数据库的站点而言是最常用的,全局获取
\Drupal::service("database"); //完全等同于\Drupal::database();
$container()->get("database"); //效果同上,在容器对象可用时使用
\Drupal::service("database.replica"); //获取配置中$databases['default']['replica']表示的备用数据库链接,无设置将回退到主库
$container()->get("database.replica"); //效果同上,在容器对象可用时使用
以上是常用的快捷方法,更加灵活的方法是:
\Drupal\Core\Database\Database::getConnection($target, $key); //这样可以指定任意目标数据库
在得到链接对象后就可以使用它做查询了,在官方文档中查询分为两大类:静态查询、动态查询
其实这个名字有些迷惑人,所谓静态查询就是直接写SQL语句查询,而动态查询是调用系统提供的语句构建方法逐步构建SQL语句再查询
先看一看所谓的静态查询,也就是直接写SQL语句的查询怎么做:
$con=\Drupal::database();
$con->query("SELECT nid, title FROM {node} WHERE type = :type", array(':type' => 'page'));
$con->query("SELECT * FROM {node} WHERE nid IN (:nids[])", array(':nids[]' => array(13, 42, 144)));
在上面的SQL中表名均是被放在花括号里面,是不带前缀的表名,系统依据这样的语法自动添加表前缀,无{}不会添加前缀
花括号里面不能有空格,两侧需要留至少一个空格,在SQL语句中其他部分不能使用{},仅仅被表名使用
也看到可以使用参数传递,参数占位符名以冒号开始不加引号,在关联数组中元素顺序没有关系,这样的设计系统自动防止注入攻击,无需转义参数值
drupal8可以让我们使用数组方式的占位符,看上面最后一条语句,这在PDO中是不允许的,drupal会为我们自动展开数组
静态查询虽然直截了当,但有两个缺点:
第一:它无法让模块查询钩子对SQL语句进行修改
第二:直接写的SQL语句可能不会兼容所有的数据库,那么在更换数据库类型的时候带来麻烦
所以基于上面的原因,推荐使用动态查询,drupal为每一种类型的查询都准备了一个类,它有许多方法来帮助构建查询语句,比如:
$con=\Drupal::database();
$query = $con->select('users', 'u')
->condition('u.uid', 0, '<>')
->fields('u', array('uid', 'name', 'status', 'created', 'access'))
->range(0, 50);
$result = $query->execute();
foreach ($result as $record) {
// Do something with each $record
}
动态查询类似于CI框架的活动记录类,它不需要懂得SQL怎么写,根据提供的方法构建即可
在这个例子中$con->select()方法返回一个select查询构建对象,这个对象采用链式调用自己的方法帮助最终产生一个查询sql
如果方法返回的是对象本身则可使用链式调用,数据库连接对象可以返回多种查询构建对象,他们有不同的帮助方法,如下:
$con->select($table, $alias = NULL, array $options = array()); //构建查询对象
$con->insert($table, array $options = array()); //构建插入SQL语句对象
$con->merge($table, array $options = array()); //构建合并查询
$con->upsert($table, array $options = array()); //构建upset查询对象,数据库不支持则模拟
$con->update($table, array $options = array()); //构建更新查询对象
$con->delete($table, array $options = array()); //构建删除查询对象
$con->truncate($table, array $options = array()); //构建清空数据表查询对象
这些查询构建对象的定义在这里:\core\lib\Drupal\Core\Database\Driver\,
它们继承自母类\core\lib\Drupal\Core\Database\Query,母类提供各数据库相同功能,子类解决不同数据库的特殊性
这些查询构建类各自定义了很多方法去构建自己类型的sql查询,这些sql语句是满足数据库类型的,
上面的这些查询构建对象都可以通过强制类型转换(string)$var来得到构建的SQL语句,在他们内部用php魔术方法__toString()来实现此目的,实际上最终就是使用该方法得到SQL语句并传递给Connection连接对象的中心查询方法去执行。
要把每个构建对象及他们的构建方法介绍完,需要很大篇幅,这里不做介绍,你可以到官网文档查看,如需深入理解直接看类定义代码吧,它有详细的注释,官网的API文档就提取自这些注释。
动态查询除了帮助构建兼容的SQL外还可以添加查询标签,这个功能可以让drupal模块据此修改相应的查询SQL
创建数据库及表结构:
要创建一个数据库,以及定义里面的表结构,在drupal中往往不是通过静态查询功能实现的,drupal数据库链接对象提供了一个方法来返回schema对象:
schema对象操作数据库结构定义,这是一块很重要的内容,由于篇幅有限,将在下一篇源码分析中专门介绍,基本使用如下:
$con=\Drupal::database();
$con->schema();
开启数据库事务:
数据库事务让查询具备原子性、一致性,在drupal中默认是支持事务的,除非在链接选项中明确禁止事务,mysql默认使用支持事务处理的InnoDB储存引擎
$con=\Drupal::database();
$yunke=$con->startTransaction($name = ''); //开始事务,参数指回滚点,只要变量$yunke不被销毁那么事务持续开启,一旦销毁即被提交,原理是事务对象失去变量引用时,被php销毁,执行了析构函数。
$con->rollback($savepoint_name = 'drupal_transaction') //回滚事务到某个回滚点
$yunke=NULL;//变量被销毁,事务被提交
不要使用$con->commit();去提交一个事务,这会抛出异常,系统会隐式自动提交;
关于回滚点需要注意:
在首次开启事务时,系统会强制采用系统定义的回滚点(在嵌套中后续再启动才会接受开发者自定义的回滚点),此时如果你采用自定义的回滚点回滚,那么会抛出错误,保险的做法是完全不要使用自定义的回滚点,而全部用自动生成的回滚点,就像如下代码所示:
//开启数据库事务
$con = \Drupal::database();
$yunke = $con->startTransaction();
try {
//code
} catch (\Exception $e) {
$msg = $e->getMessage();
\Drupal::logger('database')->error($msg);
$con->rollback($yunke->name()); //回滚事务到回滚点
}
unset($yunke);//变量被销毁,事务被提交
需要注意的是DDL语句(数据库定义语句)在大多数数据库中是不支持事务的,包括MYSQL
使用结果集:
SELECT查询返回一个或多个数据,这些结果集被包装在Drupal\Core\Database\Statement中,它继承自PDO的PDOStatement类,可以使用它的全部方法,行为受到查询选项的控制,通常使用foreach循环去获取结果,如下:
$con=\Drupal::database();
$result = $con->query("SELECT nid, title FROM {node}");
foreach ($result as $record) {
// Do something with each $record
$node = node_load($record->nid);
}
$record = $result->fetch(); // 使用默认fetch模式获取下一行数据.
$record = $result->fetchObject(); // 以stdClass对象形式取回
$record = $result->fetchAssoc(); //以数组方式取回
$record = $result->fetchField($column_index); //获取一行中的一个字段,$column_index是列索引值,以0开始
$number_of_rows = $result->rowCount(); //计算 DELETE、INSERT 、UPDATE 影响的行数,SELECT不应该使用这个方法
$con->select('users')->countQuery()->execute()->fetchField(); //SELECT应该使用这个方法,在countQuery()调用前需要构建好查询
更多结果集的操作请查看:https://www.drupal.org/docs/7/api/database-api/result-sets
数据库操作的过程式包装:
php的函数是全局可用的,为了方便操作,drupal把许多数据库操作功能封装到函数中,这样就可以在任何地方调用了
这些函数定义在/core/includes/database.inc和/core/includes/schema.inc中,它们在HTTP核心堆栈的预处理层被载入
你可以在模块中直接使用,详情见这两个文件
上面基本讲到使用层面的内容,下面我们看看源码布局:
在drupal中数据库源码位于:\core\lib\Drupal\Core\Database,
其中Driver子目录存放不同数据库的驱动,解决差异问题(数据库方言),Query子目录存放SQL查询语句构建类,专门构建SQL语句,所有的执行汇总到链接类Connection的query方法上,而此方法还不是真正执行查询的方法,查看源码:
$stmt = $this->prepareQuery($query);
$stmt->execute($args, $options);
这个$stmt其实是Drupal\Core\Database\Statement对象,为什么是这个?在链接对象构造函数中有:
$connection->setAttribute(\PDO::ATTR_STATEMENT_CLASS, array($this->statementClass, array($this)));
这个$this->statementClass就指定了Drupal\Core\Database\Statement,这个设置就是让pdo返回这个对象
如果还不明白,请看:http://php.net/manual/zh/pdo.setattribute.php
所以最终执行drupal所有查询的是Drupal\Core\Database\Statement的execute方法
查询日志记录就设置在这个方法里面,如果需要开启查询日志记录可以这样:
\Drupal\Core\Database\Database::startLog($logging_key, $key = 'default');
//开始日志记录 配置中的一个$key对应一个日志记录器,$logging_key可以是$target也可以自己随意指定
\Drupal\Core\Database\Database::getLog($logging_key, $key = 'default'); //得到查询日志
查询日志是一个数组,内容如下:
array(
'query' => "查询语句",
'args' => "参数",
'target' => $target,
'caller' => "调用者",
'time' => "查询这条语句执行的时间",
);
在调试的时候使用它非常方便,可以用drupal的日志系统去储存这个日志
如何将drupal的数据库系统提取出来?
如果你觉得drupal的数据库系统很好,想提取出来用在自己其他的项目上面,你将需要处理下面的工作:
1:drupal数据库代码在数据库中建立了一个sequences表,用于满足nextId()的功能,提供一个比之前返回的数大的唯一整数
2:在durpal中模块可以修改查询,数据库代码对查询提供标签管理并触发模块修改,在preExecute方法中调用
\Drupal::moduleHandler()->alter($hooks, $query);修改查询语句,模块通过这些标签修改查询,
处理好这些和drupal系统紧密耦合的地方就可以提取出来单独使用了
补充知识点:
1:可以使用db_ignore_replica()函数禁用从库,此函数位于core\includes\database.inc,在配置文件中可以设置maximum_replication_lag来指定从库的延迟时间,不设置默认为300秒,这个函数会建立$_SESSION['ignore_replica_server'],在后续页面中根据此判断是否禁用从库,它的值是设置时的请求时间加延迟时间,在这个时间内系统不会使用从库,此判断是在核心派发kernel.request事件时进行
2:现在已经不再使用术语"master/slave"而使用"primary/replica",由于文化原因,请看:https://www.drupal.org/node/2275877
以上就是drupal8数据库代码的所有知识,不尽之处相信已经可以轻松通过源代码或官方文档查清楚了
下一篇将介绍数据库的Schema,它是一个API,介绍如何在drupal的应用层定义数据库,如何建库建表
反馈互动