ORM学习(二) - 数据库服务的启动与连接
前言
上一节,我们了解了ORM,而ORM到底是如何工作的,我们将在接下来的几节中,以Laravel Eloquent为研究对象,进行探讨,共同学习。首先是第一节,数据库服务的注册与启动
。
数据库服务的注册与启动
在laravel应用的生命周期里,数据库部分出现在第二阶段,容器启动阶段。
Laravel生命周期
一、注册
数据库服务提供者:
Illuminate\Database\DatabaseServiceProvider
public function register()
{
Model::clearBootedModels();
$this->registerConnectionServices();
$this->registerEloquentFactory();
$this->registerQueueableEntityResolver();
}
注册函数的源码如上,我们逐句来看其功能
1、Model::clearBootedModels()
public static function clearBootedModels()
{
static::$booted = [];
static::$globalScopes = [];
}
这一句是为了 Eloquent 服务的启动做准备的。数据库的 Eloquent Model 初始化了一个静态的成员变量数组 $booted,用于存储所有已经被初始化的数据库 model ,以便加载数据库模型时更加迅速。
2、$this->registerConnectionServices()
这一句是注册步骤的重点,源码如下:
protected function registerConnectionServices()
{
$this->app->singleton('db.factory', function ($app) {
return new ConnectionFactory($app);
});
$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.factory
负责为 DB 创建 connector 提供数据库的底层连接服务。
db.connection
是 laravel 用于与数据库 pdo 接口进行交互的底层类,实现具体的与数据库的交互操作,增删改查等。
3、$this->registerEloquentFactory()
第三句的源码如下,这一步主要是创建 Eloquent Model
,它的主要作用是创建数据库模型。
protected function registerEloquentFactory()
{
$this->app->singleton(FakerGenerator::class, function () {
return FakerFactory::create();
});
$this->app->singleton(EloquentFactory::class, function ($app) {
return EloquentFactory::construct(
$app->make(FakerGenerator::class), database_path('factories')
);
});
}
4、$this->registerQueueableEntityResolver()
最后一步源码如下, 这一步注册了可队列化实体解析器的实现,但是具体的作用还没有搞清楚
protected function registerQueueableEntityResolver()
{
$this->app->singleton('Illuminate\Contracts\Queue\EntityResolver', function () {
return new QueueEntityResolver;
});
}
二、启动
注册完毕之后,下面就是启动阶段了。启动阶段只有两个动作:
- 第一步是设置
Eloquent Mode
l 的connection resolver
,它的作用是让model可以通过db来连接数据库 - 第二步是设置数据库事件的分发器 dispatcher,用于监听数据库的事件。
public function boot()
{
Model::setConnectionResolver($this->app['db']);
Model::setEventDispatcher($this->app['events']);
}
我们与数据库的连接就这么结束了吗?当然不是,我们只是简述了注册、启动两个阶段,它们都是基础的准备阶段。还记得我们在注册阶段的第二步,往IOC容器注册的三个对象吗,下面我们来一个个剖析一下
三、DatabaseManager—— 数据库的接口
db这句代码中的db,就是DatabaseManager(Illuminate\Database\DatabaseManager
),用以和pdo打交道,间接操作数据库,下面是详细代码解析
1、构造函数 & __call
class DatabaseManager implements ConnectionResolverInterface
{
protected $app;
protected $factory;
protected $connections = [];
public function __construct($app, ConnectionFactory $factory)
{
$this->app = $app;
$this->factory = $factory;
}
public function __call($method, $parameters)
{
return $this->connection()->$method(...$parameters);
}
}
下面的应用大家应该不会陌生,就算没用过,也是可以一目了然的,但是其实,跟数据库的连接和查询,都不是DB本身的功能,而是Illuminate\Database\Connectors\Connector
(连接功能) 和Illuminate\Database\Connection
(查询功能)来完成的,为什么可以做到,来看上面的源码,构造函数的参数之一ConnectionFactory
类是一个生成connection的工厂,它为DB生产出一个个connection之后都会存入 $connections
当中,再佐以__call这个魔术方法,使得DB可以执行查询操作,而connector是包含在connection中的,所以DB还可以执行数据库的连接操作。
// 1. 静态调用
User::all();
User::find(1);
User::where();
// 2. 对象调用
$flight = App\Flight::find(1);
$flight->name = 'New Flight Name';
$flight->save();
$filght->delete();
2、connection()函数 - 获取数据库连接对象
那么问题来了,上面的魔术方法中是直接$this->connection()
就完成了connection的获取,而$connection
可是一个数组,那么,connection是如何选取的呢?上源码
public function connection($name = null)
{
list($database, $type) = $this->parseConnectionName($name);
$name = $name ?: $database;
if (! isset($this->connections[$name])) {
$this->connections[$name] = $this->configure(
$connection = $this->makeConnection($database), $type
);
}
return $this->connections[$name];
}
protected function parseConnectionName($name)
{
$name = $name ?: $this->getDefaultConnection();
return Str::endsWith($name, ['::read', '::write'])
? explode('::', $name, 2) : [$name, null];
}
public function getDefaultConnection()
{
return $this->app['config']['database.default'];
}
代码还是比较清晰的,有两个小点值得注意,
一个是三目运算符的用法,$name = $name ?: $database;
,这段代码的意思是$name = $name ? $name : $database;
,而不是 $name = $name ? $database: $database;
。
另一个是对于'::read'、'::write'后缀的处理,从这里可以得知,数据库的连接其实是有读写区分的,但是暂时是看不到什么其他信息的,以后用到了再细说。
3、makeConnection 函数 —— 创建新的数据库连接对象
完成了选取,下面的工作就是要实际的去建立连接了,当然,如果已经建立过,就会保存在$connections中,直接拿去用就好,如果没有建立过,那就到makeConnection()
上场了,去吧,皮卡。。。,emmm,去建立一个新的连接吧!
protected function makeConnection($name)
{
$config = $this->configuration($name);
// First we will check by the connection name to see if an extension has been
// registered specifically for that connection. If it has we will call the
// Closure and pass it the config allowing it to resolve the connection.
if (isset($this->extensions[$name])) {
return call_user_func($this->extensions[$name], $config, $name);
}
// Next we will check to see if an extension has been registered for a driver
// and will call the Closure if so, which allows us to have a more generic
// resolver for the drivers themselves which applies to all connections.
if (isset($this->extensions[$driver = $config['driver']])) {
return call_user_func($this->extensions[$driver], $config, $name);
}
return $this->factory->make($config, $name);
}
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 [$name] not configured.");
}
return $config;
}
代码还是比较清晰的,不过有一个点是我比较关注的,其中两条关于$this->extensions
的判断,图上源码中的注释解释了这里面是放着自己注册的函数,那么这个extensions什么时候会写入,又是为了什么需要用这种方式来注册函数呢?留做一个思考点,暂且记录下来
4、configure—— 连接对象读写配置
最后来简单看下配置相关代码
protected function configure(Connection $connection, $type)
{
$connection = $this->setPdoForType($connection, $type);
if ($this->app->bound('events')) {
$connection->setEventDispatcher($this->app['events']);
}
$connection->setReconnector(function ($connection) {
$this->reconnect($connection->getName());
});
return $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—— 数据库连接对象工厂
db.factory上面我们把‘db’部分基本过了一遍,下面开始‘db.factory’部分。我们上面已经看到,DatabaseManager初始化时,factory就已经注入了一个示例,并保存在了
$this->factory
中。实际的使用点就是makeConnection
函数最后一句$this->factory->make($config, $name)
,那我们就从这个make函数开始说起1、make 函数 —— 工厂接口
public function make(array $config, $name = null)
{
$config = $this->parseConfig($config, $name);
if (isset($config['read'])) {
return $this->createReadWriteConnection($config);
}
return $this->createSingleConnection($config);
}
protected function parseConfig(array $config, $name)
{
return Arr::add(Arr::add($config, 'prefix', ''), 'name', $name);
}
这里注意$config['read'],这里区分建立连接的类型,其实是为了明确创建读写分离链接还是普通链接,至于读写分离具体是怎么实现的,后续会学习到。
2、createSingleConnection 函数 —— 制造数据库连接对象
这里就是上面说到的创建普通连接的函数。
protected function createSingleConnection(array $config)
{
$pdo = $this->createPdoResolver($config);
return $this->createConnection(
$config['driver'], $pdo, $config['database'], $config['prefix'], $config
);
}
这个函数虽然只有两步,但是却是最核心的代码
1、$this->createPdoResolver($config)
,这一步创建了数据库连接器的闭包,并不是直接创建了$pdo对象,真正的创建连接器在下一步,另外,根据配置中的‘driver’决定创建哪种类型的connection也是在这一步确定的(Mysql、sqlite、pgsql、sqlsrv)。这里的代码往下追还是比较清晰的,但是有点多,贴个链接 github 代码文件
2、$this->createConnection
,这一步是真正的创建连接对象,会将该闭包函数放入 connection 对象当中去。以后我们利用 connection 对象进行查询或者更新数据库时,程序便会运行该闭包函数,与数据库进行连接。实际运行闭包的代码,在connection基类的getPdo方法中
/**
* Get the current PDO connection.
*
* @return \PDO
*/
public function getPdo()
{
if ($this->pdo instanceof Closure) {
return $this->pdo = call_user_func($this->pdo);
}
return $this->pdo;
}
3、createReadWriteConnection—— 创建读写连接对象
还记得之前说的创建读写分离的连接吗?它来了!
代码如下,直接创建了两个pdo,一个读用,一个写用,
protected function createReadWriteConnection(array $config)
{
$connection = $this->createSingleConnection($this->getWriteConfig($config));
return $connection->setReadPdo($this->createReadPdo($config));
}
protected function getWriteConfig(array $config)
{
return $this->mergeReadWriteConfig(
$config, $this->getReadWriteConfig($config, 'write')
);
}
protected function getReadWriteConfig(array $config, $type)
{
return isset($config[$type][0])
? $config[$type][array_rand($config[$type])]
: $config[$type];
}
protected function mergeReadWriteConfig(array $config, array $merge)
{
return Arr::except(array_merge($config, $merge), ['read', 'write']);
}
protected function createReadPdo(array $config)
{
return $this->createPdoResolver($this->getReadConfig($config));
}
protected function getReadConfig(array $config)
{
return $this->mergeReadWriteConfig(
$config, $this->getReadWriteConfig($config, 'read')
);
}
五、connector 连接
db.connection以Mysql为例,connector其实只做了一件事情 -- 创建pdo连接。当然这件事非常重要,也细分了很多步骤,我们重点来一起学习下前三项,后三项以补充形式给出。
class MySqlConnector extends Connector implements ConnectorInterface
{
public function connect(array $config)
{
$dsn = $this->getDsn($config);
$options = $this->getOptions($config);
$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;
}
}
1、getDsn—— 获取数据库连接 DSN 参数
如果在win下使用过SQLSERVER的话,应该会对DSN这个东西不陌生,这里其实也有,只是它藏在这里,对用户透明了。DSN分两种,tcp与socket,socket更快,但是只能在本机器使用,多数情况下,我们都是用的tcp形式的。
protected function getDsn(array $config)
{
return $this->hasSocket($config)
? $this->getSocketDsn($config)
: $this->getHostDsn($config);
}
protected function hasSocket(array $config)
{
return isset($config['unix_socket']) && ! empty($config['unix_socket']);
}
protected function getSocketDsn(array $config)
{
return "mysql:unix_socket={$config['unix_socket']};dbname={$config['database']}";
}
protected function getHostDsn(array $config)
{
extract($config, EXTR_SKIP);
return isset($port)
? "mysql:host={$host};port={$port};dbname={$database}"
: "mysql:host={$host};dbname={$database}";
}
2、getOptions——pdo 属性设置
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,
];
public function getOptions(array $config)
{
$options = Arr::get($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 服务器的连接是否是长连接.
3、createConnection—— 创建数据库连接
创建连接和错误处理的代码如下,你品,你细品,哈哈。
注意下错误处理中,几种错误出现时,是会尝试重新建立连接的哦。
public function createConnection($dsn, array $config, array $options)
{
list($username, $password) = [
Arr::get($config, 'username'), Arr::get($config, 'password'),
];
try {
return $this->createPdoConnection(
$dsn, $username, $password, $options
);
} catch (Exception $e) {
return $this->tryAgainIfCausedByLostConnection(
$e, $dsn, $username, $password, $options
);
}
}
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);
}
protected function tryAgainIfCausedByLostConnection(Exception $e, $dsn, $username, $password, $options)
{
if ($this->causedByLostConnection($e)) {
return $this->createPdoConnection($dsn, $username, $password, $options);
}
throw $e;
}
protected function causedByLostConnection(Exception $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',
]);
}
总结
数据库服务的启动与连接就到此为一个段落了。没有看之前,确实无法想象,那么常见的数据库的链接和启动过程,居然有这么多的内容和细节。加油吧!!!
终于的终于,下一篇,开始学习 数据库的 CRUD 操作源码[握拳]
参考文章
欢迎大家关注我的公众号
半亩房顶