思考并回答以下问题:
前言
大致的讲,Laravel的数据库功能可以分为两部分:数据库DB、数据库Eloquent Model。数据库的Eloquent是功能十分丰富的ORM,让我们可以避免写繁杂的SQL语句。数据库DB是比较底层的与PDO交互的功能,Eloquent的底层依赖于DB。本文将会介绍数据库DB中关于数据库服务的启动与连接部分。
在详细讲解数据库各个功能之前,我们先看看支撑着整个Laravel数据库功能的框架:
- DB也就是DatabaseManager,承担着数据库接口的工作,一切数据库相关的操作,例如查询、更新、插入、删除都可以通过DB这个接口来完成。但是,具体的调用PDO API的工作却不是由该类完成的,它仅仅是一个对外的接口而已。
- ConnectionFactory顾名思义专门为DB构造初始化Connector、Connection对象。
- Connector负责数据库的连接功能,为保障程序的高效,Laravel将其包装成为闭包函数,并将闭包函数作为Connection的一个成员对象,实现懒加载。
- Connection负责数据库的具体功能,负责底层与PDO API的交互。
数据库服务的注册与启动
数据库服务也是一种服务提供者。
Illuminate\Database\DatabaseServiceProvider.php
1 | namespace Illuminate\Database; |
我们先来看这个注册函数的第一句:Model::clearBootedModels()。这一句其实是为了Eloquent服务的启动做准备。数据库的Eloquent Model有一个静态的成员变量数组$booted,这个静态数组存储了所有已经被初始化的数据库model,以便加载数据库模型时更加迅速。因此,在Eloquent服务启动之前需要初始化静态成员变量$booted:
Illuminate\Database\Eloquent\Model.php
1 | namespace Illuminate\Database\Eloquent; |
接下来我们就开始看数据库服务的注册最重要的两部分:ConnectionServices与Eloquent。
ConnectionServices注册
Illuminate\Database\DatabaseServiceProvider.php
1 | namespace Illuminate\Database; |
可以看出,数据库服务向IoC容器注册了db、db.factory与db.connection。
- 最重要的莫过于db对象,它有一个Facade是DB,我们可以利用DB::connection()来连接任意数据库,可以利用DB::select()来进行数据库的查询,可以说DB就是我们操作数据库的接口。
- db.factory负责为DB创建connector提供数据库的底层连接服务,负责为DB创建connection对象来进行数据库的查询等操作。
- db.connection是Laravel用于与数据库pdo接口进行交互的底层类,可用于数据库的查询、更新、创建等操作。
Eloquent注册
1 | namespace Illuminate\Database; |
EloquentFactory用于创建Eloquent Model,用于全局函数factory()来创建数据库模型。
数据库服务的启动
1 | /** |
数据库服务的启动主要设置Eloquent Model的connection resolver,用于数据库模型model利用db来连接数据库。还有设置数据库事件的分发器dispatcher,用于监听数据库的事件。
DatabaseManager-数据库的接口
如果我们想要使用任何数据库服务,首先要做的事情当然是利用用户名与密码来连接数据库。在Laravel中,数据库的用户名与密码一般放在.env文件中或者放入nginx配置中,并且利用数据库的接口DB来与pdo进行交互,利用pdo来连接数据库。
DB即是类Illuminate\Database\DatabaseManager,首先我们来看看其构造函数:
1 | namespace Illuminate\Database; |
我们称DB为一个接口,或者是一个门面模式,是因为数据库操作,例如数据库的连接或者查询、更新等操作均不是DB的功能,数据库的连接使用类Illuminate\Database\Connectors\Connector完成,数据库的查询等操作由类Illuminate\Database\Connection完成,因此,我们不必直接操作connector或者connection,仅仅会操作DB即可。
那么DB是如何实现connector或者connection的功能的呢?关键还是这个ConnectionFactory类,这个工厂类专门为DB来生成connection对象,并将其放入DB的成员变量数组$connections中去。connection中会包含connector对象来实现数据库的连接工作。
1 | namespace Illuminate\Database; |
魔术函数实现了DB与connection的无缝连接,任何对数据库的操作,例如DB::select()、DB::table(‘user’)->save(),都会被转移至connection中去。
connection函数-获取数据库连接对象
1 | namespace Illuminate\Database; |
具体流程如下:
DB的connection函数可以传入数据库的名字,也可以不传任何参数,此时会连接默认数据库,默认数据库的设置在config/database文件中。
connection函数流程:
- 解析数据库名称与数据库类型,例如只读、写。
- 若没有创建过与该数据库的连接,则开始创建数据库连接。
- 返回数据库连接对象Connection。
1 | namespace Illuminate\Database; |
可以看出,若没有特别指定连接的数据库名称,那么就会利用文件config/database文件中设置的default数据库名称作为默认连接数据库名称。若数据库支持读写分离,那么还可以指定数据库的读写属性,例如mysql::read。
makeConnection函数-创建新的数据库连接对象
当框架从未连接过当前数据库的时候,就要对数据库进行连接操作,首先程序会调用makeConnection函数:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27/**
* make数据库连接实例。
*
* @param string $name
* @return \Illuminate\Database\Connection
*/
protected function makeConnection($name)
{
$config = $this->configuration($name);
// 首先,我们将通过连接名称进行检查,以查看是否已为该连接专门注册了扩展名。
// 如果有,我们将调用Closure并将其传递给配置,以使其能够解析连接。
if (isset($this->extensions[$name]))
{
return call_user_func($this->extensions[$name], $config, $name);
}
// 接下来,我们将检查是否已为驱动程序注册了扩展名,
// 如果已注册,则将调用Closure,这将使我们能够为驱动程序本身使用更通用的解析器,
//该解析器适用于所有连接。
if (isset($this->extensions[$driver = $config['driver']]))
{
return call_user_func($this->extensions[$driver], $config, $name);
}
return $this->factory->make($config, $name);
}
可以看出,连接数据库仅仅需要两个步骤:获取数据库配置、利用connection factory获取connection对象。
获取数据库配置:
1 | /** |
也是非常简单,直接从配置文件中获取当前数据库的配置:
1 | 'connections' => [ |
$this->factory->make($config, $name)函数向我们提供了数据库连接对象。
configure-连接对象读写配置
当我们从connection factory中获取到连接对象connection之后,我们就要根据传入的参数进行读写配置:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26/**
* 准备数据库连接实例。
*
* @param \Illuminate\Database\Connection $connection
* @param string $type
* @return \Illuminate\Database\Connection
*/
protected function configure(Connection $connection, $type)
{
$connection = $this->setPdoForType($connection, $type);
// 首先,我们将设置获取模式以及数据库连接的其他一些依赖性。
// 该方法基本上只是配置并准备好供应用程序使用。
// 完成后,我们将其退回。
if ($this->app->bound('events')) {
$connection->setEventDispatcher($this->app['events']);
}
// 在这里,我们将设置一个reconnector回调。
// 此重新连接器可以是任何可调用的,
// 因此我们将设置一个Closure以使用连接名从该管理器重新连接,
// 这将允许我们从连接中重新连接。
$connection->setReconnector($this->reconnector);
return $connection;
}
setPdoForType函数就是根据type来设置读写:
当我们需要read数据库连接时,我们将read-pdo设置为主pdo。当我们需要write数据库连接时,我们将读写pdo都设置为write-pdo:
1 | /** |
ConnectionFactory-数据库连接对象工厂
make函数-工厂接口
获取到了数据库的配置参数之后,就要利用ConnectionFactory来获取connection对象了:
1 |
|
在建立连接之前,要先向配置参数中添加默认的prefix属性与name属性。
接着,就要判断我们在配置文件中是否设置了读写分离。如果设置了读写分离,那么就会调用createReadWriteConnection函数,生成具有读、写两个功能的connection;否则的话,就会调用createSingleConnection函数,生成普通的连接对象。
createSingleConnection函数-制造数据库连接对象
createSingleConnection函数是类ConnectionFactory的核心,用于生成新的数据库连接对象。
1 | /** |
ConnectionFactory也很简单,只做了两件事情:制造pdo连接的闭包函数、构造一个新的connection对象。
createPdoResolver-数据库连接器闭包函数
根据配置参数中是否含有host,创建不同的闭包函数:
1 | /** |
不带有host的pdo闭包函数:
1 | /** |
可以看出,不带有pdo的闭包函数非常简单,仅仅创建connector对象,利用connector对象进行数据库的连接。
带有host的pdo闭包函数:
1 | /** |
带有host的闭包函数相对比较复杂,首先程序会随机选择不同的数据库依次来建立数据库连接,若均失败,就会报告异常。
createConnector-创建连接器
程序会根据配置参数中driver的不同来创建不同的连接器,每个连接器都继承自connector类,用于连接数据库。
1 | /** |
createConnection-创建连接对象
1 | /** |
创建pdo闭包函数之后,会将该闭包函数放入connection对象当中去。以后我们利用connection对象进行查询或者更新数据库时,程序便会运行该闭包函数,与数据库进行连接。
createReadWriteConnection-创建读写连接对象
当配置文件中有read、write等配置项时,说明用户希望创建一个可以读写分离的数据库连接,此时:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51/**
* 创建一个数据库连接单例。
*
* @param array $config
* @return \Illuminate\Database\Connection
*/
protected function createReadWriteConnection(array $config)
{
$connection = $this->createSingleConnection($this->getWriteConfig($config));
return $connection->setReadPdo($this->createReadPdo($config));
}
/**
* 获取读/写连接的读配置。
*
* @param array $config
* @return array
*/
protected function getWriteConfig(array $config)
{
return $this->mergeReadWriteConfig(
$config, $this->getReadWriteConfig($config, 'write')
);
}
/**
* 获取读/写级别配置。
*
* @param array $config
* @param string $type
* @return array
*/
protected function getReadWriteConfig(array $config, $type)
{
return isset($config[$type][0])
? Arr::random($config[$type])
: $config[$type];
}
/**
* 合并读/写连接的配置。
*
* @param array $config
* @param array $merge
* @return array
*/
protected function mergeReadWriteConfig(array $config, array $merge)
{
return Arr::except(array_merge($config, $merge), ['read', 'write']);
}
可以看出,程序先读出关于write数据库的配置,之后将其合并到总配置当中,删除关于read数据库的配置,然后进行createSingleConnection建立新的连接对象。
建立连接对象之后,再根据read数据库的配置,生成read数据库的pdo闭包函数,并调用setReadPdo将其设置为读库pdo。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23/**
* 创建一个新的PDO实例进行读取。
*
* @param array $config
* @return \Closure
*/
protected function createReadPdo(array $config)
{
return $this->createPdoResolver($this->getReadConfig($config));
}
/**
* 获取读/写连接的读配置。
*
* @param array $config
* @return array
*/
protected function getReadConfig(array $config)
{
return $this->mergeReadWriteConfig(
$config, $this->getReadWriteConfig($config, 'read')
);
}
Connector连接
我们以MySQL为例:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
namespace Illuminate\Database\Connectors;
use PDO;
class MySqlConnector extends Connector implements ConnectorInterface
{
/**
* 建立数据库连接。
*
* @param array $config
* @return \PDO
*/
public function connect(array $config)
{
$dsn = $this->getDsn($config);
$options = $this->getOptions($config);
// 我们需要获取在创建全新连接实例时应使用的PDO选项。
// PDO选项控制连接行为的各个方面,某些可能由开发人员指定。
$connection = $this->createConnection($dsn, $config, $options);
if (! empty($config['database'])) {
$connection->exec("use `{$config['database']}`;");
}
$this->configureEncoding($connection, $config);
// 接下来,我们将检查是否在此配置中指定了时区,
// 如果已指定,则将发出一条语句来修改数据库的时区。
// 设置此数据库时区是可选配置项。
$this->configureTimezone($connection, $config);
$this->setModes($connection, $config);
return $connection;
}
}
getDsn-获取数据库连接DSN参数
1 | /** |
mysql数据库的连接有两种:tcp连接与socket连接。
socket连接更快,但是它要求应用程序与数据库在同一台机器,更普通的是使用tcp的方式连接数据库。框架根据配置参数来选择是采用socket还是tcp的方式连接数据库。
getOptions-pdo属性设置
1 | /** |
pdo的属性主要有以下几种:
- PDO::ATTR_CASE强制列名为指定的大小写。他的$value可为:
- PDO::CASE_LOWER:强制列名小写。
- PDO::CASE_NATURAL:保留数据库驱动返回的列名。
- PDO::CASE_UPPER:强制列名大写。
- PDO::ATTR_ERRMODE:错误报告。他的$value可为:
- PDO::ERRMODE_SILENT:仅设置错误代码。
- PDO::ERRMODE_WARNING:引发E_WARNING错误。
- PDO::ERRMODE_EXCEPTION:抛出exceptions异常。
- PDO::ATTR_ORACLE_NULLS(在所有驱动中都可用,不仅限于Oracle):转换NULL和空字符串。他的$value可为:
- PDO::NULL_NATURAL:不转换。
- PDO::NULL_EMPTY_STRING:将空字符串转换成NULL。
- PDO::NULL_TO_STRING:将NULL转换成空字符串。
- PDO::ATTR_STRINGIFY_FETCHES:提取的时候将数值转换为字符串。
- PDO::ATTR_EMULATE_PREPARES 启用或禁用预处理语句的模拟。有些驱动不支持或有限度地支持本地预处理。使用此设置强制PDO总是模拟预处理语句(如果为TRUE),或试着使用本地预处理语句(如果为FALSE)。如果驱动不能成功预处理当前查询,它将总是回到模拟预处理语句上。 需要bool类型。
- PDO::ATTR_AUTOCOMMIT:设置当前连接MySQL服务器的客户端的SQL语句是否自动执行,默认是自动提交。
- PDO::ATTR_PERSISTENT:当前对MySQL服务器的连接是否是长连接.
createConnection-创建数据库连接
1 | /** |
当pdo对象成功的建立起来后,说明我们已经与数据库成功地建立起来了一个连接,接下来我们就可以利用这个pdo对象进行查询或者更新等操作。
当创建pdo的时候抛出异常时:
1 | /** |
Illuminate\Database\DetectsLostConnections.php
1 |
|
当判断出的异常是上面几种情况时,框架会再次尝试连接数据库。
configureEncoding-设置字符集与校对集
1 | /** |
如果配置参数中设置了字符集与校对集,程序会利用配置的参数对数据库进行相关设置。
所谓的字符集与校对集设置,可以参考mysql中character set与collation的点滴理解
configureTimezone-设置时间区
1 | /** |
setModes-设置SQL_MODE模式
1 | /** |
以下内容参考:mysql的sql_mode设置简介:
SQL_MODE直接理解就是:sql的运作模式。官方的说法是:sql_mode可以影响sql支持的语法以及数据的校验执行,这使得MySQL可以运行在不同的环境中以及和其他数据库一起运作。
想设置sql_mode有三种方式:
- 在命令行启动MySQL时添加参数—sql-mode=”modes”
- 在MySQL的配置文件(my.cnf或者my.ini)中添加一个配置sql-mode=”modes”
- 运行时修改SQL mode可以通过以下命令之一:
1
2SET GLOBAL sql_mode = 'modes';
SET SESSION sql_mode = 'modes';
几种常见的mode介绍:
- ONLY_FULL_GROUP_BY:出现在select语句、HAVING条件和ORDER BY语句中的列,必须是GROUP BY的列或者依赖于GROUP BY列的函数列。
- NO_AUTO_VALUE_ON_ZERO:该值影响自增长列的插入。默认设置下,插入0或NULL代表生成下一个自增长值。如果用户希望插入的值为0,而该列又是自增长的,那么这个选项就有用了。
- STRICT_TRANS_TABLES:在该模式下,如果一个值不能插入到一个事务表中,则中断当前的操作,对非事务表不做限制
- NO_ZERO_IN_DATE:这个模式影响了是否允许日期中的月份和日包含0。如果开启此模式,2016-01-00是不允许的,但是0000-02-01是允许的。它实际的行为受到strict mode是否开启的影响1。
- NO_ZERO_DATE:设置该值,mysql数据库不允许插入零日期。它实际的行为受到strict mode是否开启的影响。
- ERROR_FOR_DIVISION_BY_ZERO:在INSERT或UPDATE过程中,如果数据被零除,则产生错误而非警告。如果未给出该模式,那么数据被零除时MySQL返回NULL
- NO_AUTO_CREATE_USER:禁止GRANT创建密码为空的用户
- NO_ENGINE_SUBSTITUTION:如果需要的存储引擎被禁用或未编译,那么抛出错误。不设置此值时,用默认的存储引擎替代,并抛出一个异常
- PIPES_AS_CONCAT:将”||”视为字符串的连接操作符而非或运算符,这和Oracle数据库是一样的,也和字符串的拼接函数Concat相类似
- ANSI_QUOTES:启用ANSI_QUOTES后,不能用双引号来引用字符串,因为它被解释为识别符