Laravel Database-数据库服务的启动与连接

思考并回答以下问题:

前言

大致的讲,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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
namespace Illuminate\Database;

class DatabaseServiceProvider extends ServiceProvider
{
public function register()
{
Model::clearBootedModels();

$this->registerConnectionServices();

$this->registerEloquentFactory();

$this->registerQueueableEntityResolver();
}
}

我们先来看这个注册函数的第一句:Model::clearBootedModels()。这一句其实是为了Eloquent服务的启动做准备。数据库的Eloquent Model有一个静态的成员变量数组$booted,这个静态数组存储了所有已经被初始化的数据库model,以便加载数据库模型时更加迅速。因此,在Eloquent服务启动之前需要初始化静态成员变量$booted:

Illuminate\Database\Eloquent\Model.php

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
namespace Illuminate\Database\Eloquent;

abstract class Model implements Arrayable, ArrayAccess, Jsonable, JsonSerializable, QueueableEntity, UrlRoutable
{
/**
* 已经初始化的数据库
*
* @var array
*/
protected static $booted = [];

/**
* The array of global scopes on the model.
*
* @var array
*/
protected static $globalScopes = [];

/**
* 清除已经初始化的数据库的列表,以便将其重新初始化。
*
* @return void
*/
public static function clearBootedModels()
{
static::$booted = [];

static::$globalScopes = [];
}
}

接下来我们就开始看数据库服务的注册最重要的两部分:ConnectionServices与Eloquent。

ConnectionServices注册

Illuminate\Database\DatabaseServiceProvider.php

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
namespace Illuminate\Database;

class DatabaseServiceProvider extends ServiceProvider
{
/**
* 注册主数据库绑定。
*
* @return void
*/
protected function registerConnectionServices()
{
// ConnectionFactory用于在数据库上创建实际的连接实例。
// 我们会将工厂注入到manager中,以便它可以在实际需要时(而不是以前)建立连接。
$this->app->singleton('db.factory', function ($app) {
return new ConnectionFactory($app);
});

// DatabaseManager用于解析各种连接,因为可以管理多个连接。
// 它还实现了连接解析器接口,该接口可以由需要连接的其他组件使用。
$this->app->singleton('db', function ($app) {
return new DatabaseManager($app, $app['db.factory']);
});

$this->app->bind('db.connection', function ($app) {
return $app['db']->connection();
});
}
}

可以看出,数据库服务向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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
namespace Illuminate\Database;

class DatabaseServiceProvider extends ServiceProvider
{
/**
* 在容器中注册Eloquent工厂实例。
*
* @return void
*/
protected function registerEloquentFactory()
{
$this->app->singleton(FakerGenerator::class, function ($app, $parameters) {
return FakerFactory::create($parameters['locale'] ?? $app['config']->get('app.faker_locale', 'en_US'));
});

$this->app->singleton(EloquentFactory::class, function ($app) {
return EloquentFactory::construct(
$app->make(FakerGenerator::class), $this->app->databasePath('factories')
);
});
}
}

EloquentFactory用于创建Eloquent Model,用于全局函数factory()来创建数据库模型。

数据库服务的启动

1
2
3
4
5
6
7
8
9
10
11
/**
* 初始化应用程序事件。
*
* @return void
*/
public function boot()
{
Model::setConnectionResolver($this->app['db']);

Model::setEventDispatcher($this->app['events']);
}

数据库服务的启动主要设置Eloquent Model的connection resolver,用于数据库模型model利用db来连接数据库。还有设置数据库事件的分发器dispatcher,用于监听数据库的事件。

DatabaseManager-数据库的接口

如果我们想要使用任何数据库服务,首先要做的事情当然是利用用户名与密码来连接数据库。在Laravel中,数据库的用户名与密码一般放在.env文件中或者放入nginx配置中,并且利用数据库的接口DB来与pdo进行交互,利用pdo来连接数据库。

DB即是类Illuminate\Database\DatabaseManager,首先我们来看看其构造函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
namespace Illuminate\Database;

class DatabaseManager implements ConnectionResolverInterface
{
public function __construct($app, ConnectionFactory $factory)
{
$this->app = $app;
$this->factory = $factory;

$this->reconnector = function ($connection) {
$this->reconnect($connection->getName());
};
}
}

我们称DB为一个接口,或者是一个门面模式,是因为数据库操作,例如数据库的连接或者查询、更新等操作均不是DB的功能,数据库的连接使用类Illuminate\Database\Connectors\Connector完成,数据库的查询等操作由类Illuminate\Database\Connection完成,因此,我们不必直接操作connector或者connection,仅仅会操作DB即可。

那么DB是如何实现connector或者connection的功能的呢?关键还是这个ConnectionFactory类,这个工厂类专门为DB来生成connection对象,并将其放入DB的成员变量数组$connections中去。connection中会包含connector对象来实现数据库的连接工作。

1
2
3
4
5
6
7
8
9
10
11
12
13
namespace Illuminate\Database;

class DatabaseManager implements ConnectionResolverInterface
{
protected $app;
protected $factory;
protected $connections = [];

public function __call($method, $parameters)
{
return $this->connection()->$method(...$parameters);
}
}

魔术函数实现了DB与connection的无缝连接,任何对数据库的操作,例如DB::select()、DB::table(‘user’)->save(),都会被转移至connection中去。

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
27
28
29
30
31
32
33
34
35
36
37
38
39
namespace Illuminate\Database;

use Illuminate\Database\Connectors\ConnectionFactory;
use Illuminate\Support\Arr;
use Illuminate\Support\ConfigurationUrlParser;
use Illuminate\Support\Str;
use InvalidArgumentException;
use PDO;

/**
* @mixin \Illuminate\Database\Connection
*/
class DatabaseManager implements ConnectionResolverInterface
{

/**
* 获取数据库连接实例。
*
* @param string|null $name
* @return \Illuminate\Database\Connection
*/
public function connection($name = null)
{
[$database, $type] = $this->parseConnectionName($name);

$name = $name ?: $database;

// 如果尚未创建此连接,则将基于应用程序中提供的配置来创建它。
// 创建连接后,我们将为PDO设置“fetch mode”,该模式将确定查询返回类型。
if (! isset($this->connections[$name]))
{
$this->connections[$name] = $this->configure(
$this->makeConnection($database), $type
);
}

return $this->connections[$name];
}
}

具体流程如下:

DB的connection函数可以传入数据库的名字,也可以不传任何参数,此时会连接默认数据库,默认数据库的设置在config/database文件中。

connection函数流程:

  • 解析数据库名称与数据库类型,例如只读、写。
  • 若没有创建过与该数据库的连接,则开始创建数据库连接。
  • 返回数据库连接对象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
27
28
29
30
31
32
33
34
35
36
37
38
39
namespace Illuminate\Database;

use Illuminate\Database\Connectors\ConnectionFactory;
use Illuminate\Support\Arr;
use Illuminate\Support\ConfigurationUrlParser;
use Illuminate\Support\Str;
use InvalidArgumentException;
use PDO;

/**
* @mixin \Illuminate\Database\Connection
*/
class DatabaseManager implements ConnectionResolverInterface
{

/**
* 将连接解析为名称和读/写类型的数组。
*
* @param string $name
* @return array
*/
protected function parseConnectionName($name)
{
$name = $name ?: $this->getDefaultConnection();

return Str::endsWith($name, ['::read', '::write'])
? explode('::', $name, 2) : [$name, null];
}

/**
* 获取默认的连接名称。
*
* @return string
*/
public function getDefaultConnection()
{
return $this->app['config']['database.default'];
}
}

可以看出,若没有特别指定连接的数据库名称,那么就会利用文件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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* 获取连接的配置。
*
* @param string $name
* @return array
*
* @throws \InvalidArgumentException
*/
protected function configuration($name)
{
$name = $name ?: $this->getDefaultConnection();

// 要获取数据库连接配置,我们将仅提取每个连接配置并获取给定名称的配置。
// 如果配置不存在,我们将抛出异常并撒手不管。
$connections = $this->app['config']['database.connections'];

if (is_null($config = Arr::get($connections, $name)))
{
throw new InvalidArgumentException("Database connection [{$name}] not configured.");
}

return (new ConfigurationUrlParser)
->parseConfiguration($config);
}

也是非常简单,直接从配置文件中获取当前数据库的配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
'connections' => [
'mysql' => [
'driver' => 'mysql',
'host' => env('DB_HOST', '127.0.0.1'),
'port' => env('DB_PORT', '3306'),
'database' => env('DB_DATABASE', 'forge'),
'username' => env('DB_USERNAME', 'forge'),
'password' => env('DB_PASSWORD', ''),
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
'prefix' => '',
'strict' => true,
'engine' => null,
'read' => [
'database' => env('DB_DATABASE', 'forge'),
],
'write' => [
'database' => env('DB_DATABASE', 'forge'),
],
],
],

$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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 准备数据库连接实例的读/写模式。
*
* @param \Illuminate\Database\Connection $connection
* @param string|null $type
* @return \Illuminate\Database\Connection
*/
protected function setPdoForType(Connection $connection, $type = null)
{
if ($type === 'read')
{
$connection->setPdo($connection->getReadPdo());
}
elseif ($type === 'write')
{
$connection->setReadPdo($connection->getPdo());
}

return $connection;
}

ConnectionFactory-数据库连接对象工厂

make函数-工厂接口

获取到了数据库的配置参数之后,就要利用ConnectionFactory来获取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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<?php

namespace Illuminate\Database\Connectors;

use Illuminate\Contracts\Container\Container;
use Illuminate\Database\Connection;
use Illuminate\Database\MySqlConnection;
use Illuminate\Database\PostgresConnection;
use Illuminate\Database\SQLiteConnection;
use Illuminate\Database\SqlServerConnection;
use Illuminate\Support\Arr;
use InvalidArgumentException;
use PDOException;

class ConnectionFactory
{
/**
* 根据配置建立PDO连接。
*
* @param array $config
* @param string|null $name
* @return \Illuminate\Database\Connection
*/
public function make(array $config, $name = null)
{
$config = $this->parseConfig($config, $name);

if (isset($config['read']))
{
return $this->createReadWriteConnection($config);
}

return $this->createSingleConnection($config);
}

/**
* 解析并准备数据库配置。
*
* @param array $config
* @param string $name
* @return array
*/
protected function parseConfig(array $config, $name)
{
return Arr::add(Arr::add($config, 'prefix', ''), 'name', $name);
}
}

在建立连接之前,要先向配置参数中添加默认的prefix属性与name属性。

接着,就要判断我们在配置文件中是否设置了读写分离。如果设置了读写分离,那么就会调用createReadWriteConnection函数,生成具有读、写两个功能的connection;否则的话,就会调用createSingleConnection函数,生成普通的连接对象。

createSingleConnection函数-制造数据库连接对象

createSingleConnection函数是类ConnectionFactory的核心,用于生成新的数据库连接对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 创建一个数据库连接单例。
*
* @param array $config
* @return \Illuminate\Database\Connection
*/
protected function createSingleConnection(array $config)
{
$pdo = $this->createPdoResolver($config);

return $this->createConnection(
$config['driver'], $pdo, $config['database'], $config['prefix'], $config
);
}

ConnectionFactory也很简单,只做了两件事情:制造pdo连接的闭包函数、构造一个新的connection对象。

createPdoResolver-数据库连接器闭包函数

根据配置参数中是否含有host,创建不同的闭包函数:

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 创建一个新的Closure,解析为PDO实例。
*
* @param array $config
* @return \Closure
*/
protected function createPdoResolver(array $config)
{
return array_key_exists('host', $config)
? $this->createPdoResolverWithHosts($config)
: $this->createPdoResolverWithoutHosts($config);
}

不带有host的pdo闭包函数:

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 创建一个新的Closure,解析为没有配置主机的PDO实例。
*
* @param array $config
* @return \Closure
*/
protected function createPdoResolverWithoutHosts(array $config)
{
return function () use ($config) {
return $this->createConnector($config)->connect($config);
};
}

可以看出,不带有pdo的闭包函数非常简单,仅仅创建connector对象,利用connector对象进行数据库的连接。

带有host的pdo闭包函数:

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
/**
* 创建一个新的Closure,解析为具有特定主机或主机阵列的PDO实例。
*
* @param array $config
* @return \Closure
*/
protected function createPdoResolverWithHosts(array $config)
{
return function () use ($config) {
foreach (Arr::shuffle($hosts = $this->parseHosts($config)) as $key => $host)
{
$config['host'] = $host;

try
{
return $this->createConnector($config)->connect($config);
}
catch (PDOException $e)
{
continue;
}
}

throw $e;
};
}

/**
* 将主机配置项解析为一个数组。
*
* @param array $config
* @return array
*
* @throws \InvalidArgumentException
*/
protected function parseHosts(array $config)
{
$hosts = Arr::wrap($config['host']);

if (empty($hosts))
{
throw new InvalidArgumentException('Database hosts array is empty.');
}

return $hosts;
}

带有host的闭包函数相对比较复杂,首先程序会随机选择不同的数据库依次来建立数据库连接,若均失败,就会报告异常。

createConnector-创建连接器

程序会根据配置参数中driver的不同来创建不同的连接器,每个连接器都继承自connector类,用于连接数据库。

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
/**
* 根据配置创建连接器实例。
*
* @param array $config
* @return \Illuminate\Database\Connectors\ConnectorInterface
*
* @throws \InvalidArgumentException
*/
public function createConnector(array $config)
{
if (! isset($config['driver']))
{
throw new InvalidArgumentException('A driver must be specified.');
}

if ($this->container->bound($key = "db.connector.{$config['driver']}"))
{
return $this->container->make($key);
}

switch ($config['driver'])
{
case 'mysql':
return new MySqlConnector;
case 'pgsql':
return new PostgresConnector;
case 'sqlite':
return new SQLiteConnector;
case 'sqlsrv':
return new SqlServerConnector;
}

throw new InvalidArgumentException("Unsupported driver [{$config['driver']}]");
}

createConnection-创建连接对象

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
/**
* 创建一个新的连接实例。
*
* @param string $driver
* @param \PDO|\Closure $connection
* @param string $database
* @param string $prefix
* @param array $config
* @return \Illuminate\Database\Connection
*
* @throws \InvalidArgumentException
*/
protected function createConnection($driver, $connection, $database, $prefix = '', array $config = [])
{
if ($resolver = Connection::getResolver($driver))
{
return $resolver($connection, $database, $prefix, $config);
}

switch ($driver)
{
case 'mysql':
return new MySqlConnection($connection, $database, $prefix, $config);
case 'pgsql':
return new PostgresConnection($connection, $database, $prefix, $config);
case 'sqlite':
return new SQLiteConnection($connection, $database, $prefix, $config);
case 'sqlsrv':
return new SqlServerConnection($connection, $database, $prefix, $config);
}

throw new InvalidArgumentException("Unsupported driver [{$driver}]");
}

创建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
<?php

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
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
/**
* 从配置中创建一个DSN字符串。
*
* 根据“ unix_socket”配置值选择socket或host/port。
*
* @param array $config
* @return string
*/
protected function getDsn(array $config)
{
return $this->hasSocket($config)
? $this->getSocketDsn($config)
: $this->getHostDsn($config);
}

/**
* 确定给定的配置数组是否具有UNIX套接字值。
*
* @param array $config
* @return bool
*/
protected function hasSocket(array $config)
{
return isset($config['unix_socket']) && ! empty($config['unix_socket']);
}

/**
* 获取套接字配置的DSN字符串。
*
* @param array $config
* @return string
*/
protected function getSocketDsn(array $config)
{
return "mysql:unix_socket={$config['unix_socket']};dbname={$config['database']}";
}

/**
* 获取主机/端口配置的DSN字符串。
*
* @param array $config
* @return string
*/
protected function getHostDsn(array $config)
{
extract($config, EXTR_SKIP);

return isset($port)
? "mysql:host={$host};port={$port};dbname={$database}"
: "mysql:host={$host};dbname={$database}";
}

mysql数据库的连接有两种:tcp连接与socket连接。

socket连接更快,但是它要求应用程序与数据库在同一台机器,更普通的是使用tcp的方式连接数据库。框架根据配置参数来选择是采用socket还是tcp的方式连接数据库。

getOptions-pdo属性设置

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
/**
* 默认的PDO连接选项。
*
* @var array
*/
protected $options = [
PDO::ATTR_CASE => PDO::CASE_NATURAL,
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_ORACLE_NULLS => PDO::NULL_NATURAL,
PDO::ATTR_STRINGIFY_FETCHES => false,
PDO::ATTR_EMULATE_PREPARES => false,
];

/**
* 根据配置获取PDO选项。
*
* @param array $config
* @return array
*/
public function getOptions(array $config)
{
$options = $config['options'] ?? [];

return array_diff_key($this->options, $options) + $options;
}

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
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
52
53
54
55
/**
* 创建一个新的PDO连接。
*
* @param string $dsn
* @param array $config
* @param array $options
* @return \PDO
*
* @throws \Exception
*/
public function createConnection($dsn, array $config, array $options)
{
[
$username,
$password
]
=
[
$config['username'] ?? null,
$config['password'] ?? null,
];

try
{
return $this->createPdoConnection(
$dsn, $username, $password, $options
);
}
catch (Exception $e)
{
return $this->tryAgainIfCausedByLostConnection(
$e, $dsn, $username, $password, $options
);
}
}

/**
* 创建一个新的PDO连接实例。
*
* @param string $dsn
* @param string $username
* @param string $password
* @param array $options
* @return \PDO
*/
protected function createPdoConnection($dsn, $username, $password, $options)
{
if (class_exists(PDOConnection::class)
&& ! $this->isPersistentConnection($options))
{
return new PDOConnection($dsn, $username, $password, $options);
}

return new PDO($dsn, $username, $password, $options);
}

当pdo对象成功的建立起来后,说明我们已经与数据库成功地建立起来了一个连接,接下来我们就可以利用这个pdo对象进行查询或者更新等操作。

当创建pdo的时候抛出异常时:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* 处理在查询执行期间发生的查询异常。
*
* @param \Illuminate\Database\QueryException $e
* @param string $query
* @param array $bindings
* @param \Closure $callback
* @return mixed
*
* @throws \Illuminate\Database\QueryException
*/
protected function tryAgainIfCausedByLostConnection(QueryException $e, $query, $bindings, Closure $callback)
{
if ($this->causedByLostConnection($e->getPrevious()))
{
$this->reconnect();

return $this->runQueryCallback($query, $bindings, $callback);
}

throw $e;
}

Illuminate\Database\DetectsLostConnections.php

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
<?php

namespace Illuminate\Database;

use Illuminate\Support\Str;
use Throwable;

trait DetectsLostConnections
{
/**
* 确定给定的异常是否由丢失的连接引起。
*
* @param \Throwable $e
* @return bool
*/
protected function causedByLostConnection(Throwable $e)
{
$message = $e->getMessage();

return Str::contains($message, [
'server has gone away',
'no connection to the server',
'Lost connection',
'is dead or not enabled',
'Error while sending',
'decryption failed or bad record mac',
'server closed the connection unexpectedly',
'SSL connection has been closed unexpectedly',
'Error writing data to the connection',
'Resource deadlock avoided',
'Transaction() on null',
'child connection forced to terminate due to client_idle_limit',
'query_wait_timeout',
'reset by peer',
'Physical connection is not usable',
'TCP Provider: Error code 0x68',
'ORA-03114',
'Packets out of order. Expected',
'Adaptive Server connection failed',
'Communication link failure',
'connection is no longer usable',
'Login timeout expired',
'Connection refused',
'running with the --read-only option so it cannot execute this statement',
]);
}
}

当判断出的异常是上面几种情况时,框架会再次尝试连接数据库。

configureEncoding-设置字符集与校对集

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
/**
* 设置连接字符集和排序规则。
*
* @param \PDO $connection
* @param array $config
* @return void
*/
protected function configureEncoding($connection, $config)
{
if (! isset($config['charset']))
{
return;
}

$connection->prepare("set names '{$config['charset']}'")->execute();
}

/**
* 获取配置的排序规则。
*
* @param array $config
* @return string
*/
protected function getCollation(array $config)
{
return isset($config['collation']) ? " collate '{$config['collation']}'" : '';
}

如果配置参数中设置了字符集与校对集,程序会利用配置的参数对数据库进行相关设置。

所谓的字符集与校对集设置,可以参考mysql中character set与collation的点滴理解

configureTimezone-设置时间区

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 在连接上设置时区。
*
* @param \PDO $connection
* @param array $config
* @return void
*/
protected function configureTimezone($connection, array $config)
{
if (isset($config['timezone'])) {
$connection->prepare('set time_zone="'.$config['timezone'].'"')->execute();
}
}

setModes-设置SQL_MODE模式

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
52
53
54
55
/**
* 设置连接模式。
*
* @param \PDO $connection
* @param array $config
* @return void
*/
protected function setModes(PDO $connection, array $config)
{
if (isset($config['modes']))
{
$this->setCustomModes($connection, $config);
}
elseif (isset($config['strict']))
{
if ($config['strict'])
{
$connection->prepare($this->strictMode($connection))->execute();
}
else
{
$connection->prepare("set session sql_mode='NO_ENGINE_SUBSTITUTION'")->execute();
}
}
}

/**
* 在连接上设置自定义模式。
*
* @param \PDO $connection
* @param array $config
* @return void
*/
protected function setCustomModes(PDO $connection, array $config)
{
$modes = implode(',', $config['modes']);

$connection->prepare("set session sql_mode='{$modes}'")->execute();
}

/**
* 获取查询以启用严格模式。
*
* @param \PDO $connection
* @return string
*/
protected function strictMode(PDO $connection)
{
if (version_compare($connection->getAttribute(PDO::ATTR_SERVER_VERSION), '8.0.11') >= 0)
{
return "set session sql_mode='ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION'";
}

return "set session sql_mode='ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION'";
}

以下内容参考: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
    2
    SET 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后,不能用双引号来引用字符串,因为它被解释为识别符
0%