Laravel5.5源码详解 -- 数据库的启动与连接过程

Laravel5.5源码详解 – 数据库的启动与连接过程

整个laravel的操作,一般情况下,数据库的处理会占掉很大一部分。所以对数据 库处理的理解,显得尤为重要。关于其源码解析,网上有非常多的文献,但流程一般都含糊其辞,读完来龙去脉甚为不解。所以,我自己做了一次流程分析,并记录下全过程。

Laravel对不同数据库连接的实例封装了对应连接的PDO类,为上层使用数据库连接实例提供了统一的接口。我这里源码分析,都以以mySql为实例进行讲解,过程大致如下,

DB::table('users') --> 拿到PDO --> 调用核心类Illuminate\Database\Connection

这里先局部后全局,分三部分讲解。

  1. 数据库的连接与PHP-PDO的关系:解释laravel与数据库的接洽点;
  2. 数据库查询构造流程:理解是如何最后实现调用核心类Connection::table()函数的;
  3. 数据库启动大脉络分析:理解全流程,然后重点是如何拿到PDO的。

另外,例子中用到的laravel的门面(Facades)模式,原理比较简单,可以参考其官方文档。

说在前面的话

Laravel目前支持的有四类数据库,laravel中对应的名称分别为:mysql,pgsql,sqlite,sqlsrv,即MySQL、Postgres、SQLite和SQL Server;同时,laravel还支持用户算定的数据库和驱动程序。

当操作数据库的查询构造器时,可以使用类似

DB::table('users')->get();
DB::table('users')->select();
DB::table('users')->insert();
DB::update();

语法,其中

DB::table('users')

部分就是获取查询构造器,后面的“->get()”等调用查询构造的方法实现相应数据操作。后面我们会讲到(详见第三节),这些查询会通过DatabaseManager::connection()再调用各个$methods。

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

查询构造器的建立过程分为两个阶段:一个是数据库连接封装阶段,另一个是查询构造器生成阶段。

数据库连接封装又可以分为四个步骤:

一、数据库管理器阶段,在DatabaseServiceProvider类中的registerConnectionServices()函数中创建ConnectionFactory实例;

Laravel首先通过服务提供者“Illuminate\Database\DatabaseServiceProvider”注册了数据库管理服务(“DB”服务)和数据库连接工厂服务(“db.factory”服务),通过上述服务获取数据库管理DatabaseManager类和数据库连接工厂实例ConnectionFactory类的实例,其中数据库连接工厂实例作为数据库管理器实例的一个属性,在DatabaseServiceProvider类中的registerConnectionServices()函数中创建ConnectionFactory实例。

二、数据库连接工厂阶段,这一阶段主要是为连接数据库作配置准备,并生成连接器MySqlConnector;为了对上层提供统一的接口,Laravel在底层根据不同的配置调用了不同的数据库驱动扩展,框架上使用了简单工厂设计模式,用来根据配置文件获取不同的数据库连接实例。

三、数据库连接器阶段,连接器MySqlConnector会创建连接,并调用其子函数::createConnection() 和 ::createPdoConnection();Laravel针对不同的数据库有不同的实现,主要包括连接DSN名称及配置等。Laravel框架用四个类分别封装了默认支持的四个数据库连接的过程,通过connect()方法提供统一的接口。

四、数据库连接创建阶段,在这个阶段MySqlConnector的父类Connector会生成PDO实例,并完成连接。本质上,不同数据库连接的实例就是封装了对应连接的PDO类实例、请求语法类实例、和结果处理类实例,从而为上层使用数据库连接实例提供统一的接口。

第一节,数据库的连接与PHP-PDO的关系

首先,我们要知道,数据最终是在类Illuminate\Database\Connectors\Connector.php中完成链接的。我们先分析一下其源码:

class Connector
{
    use DetectsLostConnections;
    //下面是连接时默认用到的参数,当然你可以在创建联接时更改
    protected $options = [
        PDO::ATTR_CASE => PDO::CASE_NATURAL, // 保留数据库驱动返回的列名,不强制列名为指定的大小写
        PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,  // 抛出 exceptions 异常
        PDO::ATTR_ORACLE_NULLS => PDO::NULL_NATURAL,  // 不转换 NULL 和空字符串
        PDO::ATTR_STRINGIFY_FETCHES => false, // 提取的时候将数值转换为字符串?  => 不转换
        PDO::ATTR_EMULATE_PREPARES => false,  // 禁用预处理语句的模拟
    ];

   // 尝试建立一个连接
    public function createConnection($dsn, array $config, array $options)
    {
        // 先拿到连接数据库所需要的用户名和密码,这个一般在.env中设置,你可以看到有以下3项,
        // DB_DATABASE=laraveldb   DB_USERNAME=username   DB_PASSWORD=password
        list($username, $password) = [
            $config['username'] ?? null, $config['password'] ?? null,
        ];

        // 尝试调用实际建立连接的createPdoConnection函数,注意上面的$options已经作为设置参数传入
        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);
    }

大致上,createPdoConnection会检查有没有PDOConnection这个类,实际上在laravel提供的默认源码中这个类是不存在的,你可以检查一下你的composer.json文件。如果想安装使用doctrine,可以参考以下官网

https://www.laraveldoctrine.org/docs/1.3/orm/installation

言归正传,createPdoConnection找不到PDOConnection这个类,就会调用后面那句

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

其中$dsn就是我们在.env中设置的数据库地址

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306

如果打印出来,就是一段字符串,如下,

'mysql:host=127.0.0.1;:port=3306;dbname=laraveldb'

这里值得一提的是PDO,PDO是个什么东西?PDO是PHP提供的数据对象扩展(PHP Data Object),它为PHP访问数据库提供了一套轻量级的接口,从PHP5.1版以后开始提供。你可以参考官方网站,

http://php.net/manual/zh/book.pdo.php

因此不难明白,所谓的laravel连接数据库,只不过是调用了PHP中的PDO(或者说该类)的API函数,并进行一系列的操作的过程。同样,Qureybuilder的相关API,也只不过是PDO的一层封装外衣!

这里需要进一步说明的是,这个PDO创建之后,是直接返回给变量$connection的。以mySql为例,

namespace Illuminate\Database\Connectors;
use 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); // 在这里创建connection

        if (! empty($config['database'])) {
            $connection->exec("use `{$config['database']}`;");
        }

        $this->configureEncoding($connection, $config);

        $this->configureTimezone($connection, $config);

        $this->setModes($connection, $config);

        return $connection;
    }
    ...
}

这个类MySqlConnector是Illuminate\Database\Connectors\Connector的子类。

第二节 laravel中数据库查询构造流程

当 connection 对象构建初始化完成后,我们可以用 DB 来进行数据库的 增删改查(CRUD,即( Create、Retrieve、Update、Delete)等操作。laravel的查询构造器让我们避免使用原生的sql语句,而是用一种语法上更容易理解的方式操作(Laravel官方称这样可以避免漏洞),例如

DB::table('table')->select('*')->where('user_id', 1);

第一,查询构造的这个过程是如何实现的呢?

当然,这种看似静态调用的方法,其实是laravel里的门面模式,实际上调用的并不是静态方法。这个简单的模式只有几行代码,

<?php
namespace Illuminate\Support\Facades;

class DB extends Facade
{
    protected static function getFacadeAccessor()
    {
        return 'db';
    }
}

其原理只不过是通过调用其父类 Illuminate\Support\Facades\Facade中的PHP的魔术方法 __callStatic(),将请求转到了相应的方法上。这里的’db’,定义在

Illuminate\Foundation\Application.php

的 registerCoreContainerAliases()里面,如下

'db'                   => [\Illuminate\Database\DatabaseManager::class],

所以,在发出指令的时候,

DB::table('table')->select('*')->where('user_id', 1);

laravel会通过Facade,找到DatabaseManager里面的table()函数,本质上是这个魔术函数

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

这里的$this->connection()实质上是mySqlConnection对象,这个对象的父类正是Illuminate\Database\Connection,于是,DatabaseManager顺藤摸瓜,找到了mySqlConnection,并调用了其父类Connection中的table方法。

    public function table($table)
    {
        // 用其QueryBuilder进行查询
        return $this->query()->from($table);
    }

    public function query()
    {
        return new QueryBuilder(
            $this, $this->getQueryGrammar(), $this->getPostProcessor()
        );
    }

第二,Connection核心类业务

要知道,查询构造工作是在Illuminate\Database\Connection中完成的,这个类是我们要了解的核心,其构造函数如下,

    public function __construct($pdo, $database = '', $tablePrefix = '', array $config = [])
    {
        $this->pdo = $pdo;     // $pdo是通过MySqlConnection--MySqlConnector拿到的,参考第三节
        $this->database = $database;  
        $this->tablePrefix = $tablePrefix;
        $this->config = $config;
        $this->useDefaultQueryGrammar();   // Grammar SQL语法编译器实例 
        $this->useDefaultPostProcessor();  // Processor SQL结果处理器实例
    }

可见,除了$pdo,这里还在MySqlConnection构造函数中通过setter注入了

\Illuminate\Database\Query\Grammars\Grammar 
\Illuminate\Database\Query\Processors\Processor 

这里有三样东西要关注,PDO,Grammar和Processor。

不过,这些具体内容在网上已经写得比较详细,我这里不再重复,可以参考

https://segmentfault.com/a/1190000007267217

https://segmentfault.com/a/1190000007278819

https://segmentfault.com/a/1190000007315628

https://www.yanshuo.me/p/50386

https://www.yanshuo.me/p/53185

https://www.yanshuo.me/p/55168

第三节 数据库启动大脉络分析

我这里类(对象)的调用关系用===>表示,==>表示对象内部的函数调用,::表示属于该类的子函数,整个大脉络如下:

DB::table('users')->get();
DB::table('users')->select();
DB::table('users')->insert();
DB::update();

===>

DatabaseManager::connection() ==>  ::makeConnection() 

===>

ConnectionFactory::make() ==> ::createSingleConnection()  ==> ::pdoResolver() ==> ::createPdoResolverWithHosts()  ==>  ::createConnector()

===>

MySqlConnector::Connect()

===>

Connector::createConnection() ==> ::createPdoConnection()

===> 拿到MySqlConnection,并回到DatabaseManager,

DatabaseManager::__call() ==> $method = '$table'

===>

MySqlConnection::table()

===> 实际调用MySqlConnection父类的函数

Connection::table()

下面对源码进行详细剖析。讲过的部分不再重复,重点是理解如何拿到$pdo。

在Illuminate\Database\DatabaseManager中,connection是这样定义的,

public function connection($name = null)
{
    list($database, $type) = $this->parseConnectionName($name);
    $name = $name ?: $database;
    // 这里得到的$name就是$database,也就是'mysql', 上面得到的type则是空null   

    if (! isset($this->connections[$name])) {
        $this->connections[$name] = $this->configure(
            $this->makeConnection($database), $type
        );
    }

    // 拿到数据库连接后返回 (#connections["mysql"]=MySqlConnection)
    return $this->connections[$name];
}

这里重点要理理解的是,在ConnectionFactory 中构造出 \Illuminate\Database\MysqlConnector ,并通过MySqlConnection的构造参数注入MysqlConnector 。结果是,通过DatabaseManager的connection()函数,我们拿到了一个链接器实例MySqlConnection,该connection中还装着一个MySqlConnector,及其相关配置 。

继续看源码,被调用的makeConnection调用了Illuminate\Database\ConnectionFactory的make函数,

    protected function makeConnection($name)
    {
        // 传入的参数$name="mysql", 数组的结果看后面的分析
        $config = $this->configuration($name);

        // 看用户有没有自定义的数据库,有的话就先用用户自定义的数据库
        if (isset($this->extensions[$name])) {
            return call_user_func($this->extensions[$name], $config, $name);
        }

        // 看有没有用户自定义的驱动,有的话先调用自定义的
        if (isset($this->extensions[$driver = $config['driver']])) {
            return call_user_func($this->extensions[$driver], $config, $name);
        }

        // 一般我们是没有自定义的数据库和驱动的,所以只有最后这一句是有效的,
        return $this->factory->make($config, $name);
    }

附说明:上面的函数中,得到$config的数组打印出来看一下,

array:12 [▼
  "driver" => "mysql"
  "host" => "127.0.0.1"
  "port" => "3306"
  "database" => "laraveldb"
  "username" => "user01"
  "password" => "secrete"
  "unix_socket" => ""
  "charset" => "utf8mb4"
  "collation" => "utf8mb4_unicode_ci"
  "prefix" => ""
  "strict" => true
  "engine" => "InnoDB, ROW_FORMAT=DYNAMIC"
]

再来看Illuminate\Database\ConnectionFactory的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);
    }

附说明,上面函数的这个$config只增加了一行, “name” => “mysql”,

array:13 [▼
  "driver" => "mysql"
  "host" => "127.0.0.1"
  "port" => "3306"
  "database" => "laraveldb"
  "username" => "user01"
  "password" => "secrete"
  "unix_socket" => ""
  "charset" => "utf8mb4"
  "collation" => "utf8mb4_unicode_ci"
  "prefix" => ""
  "strict" => true
  "engine" => "InnoDB, ROW_FORMAT=DYNAMIC"
  "name" => "mysql"
]

再来看一下其调用的createSingleConnection函数,

    protected function createSingleConnection(array $config)
    {
        // 这里拿到的$pdo是一个闭包,如下第一步所述
        $pdo = $this->createPdoResolver($config);

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

第一步,先看createPdoResolver(),因为有host,就会运行createPdoResolverWithHosts(),实际上withoutHosts

相当简单,也就是创建一个 connector 对象,再利用这个connector 对象进行数据库的连接。

    protected function createPdoResolver(array $config)
    {
        return array_key_exists('host', $config)
                            ? $this->createPdoResolverWithHosts($config)
                            : $this->createPdoResolverWithoutHosts($config);
    }

我们接着看WithHosts(),

protected function createPdoResolverWithHosts(array $config)
    {
        return function () use ($config) {
            foreach (Arr::shuffle($hosts = $this->parseHosts($config)) as $key => $host) {
                $config['host'] = $host;

                try {
                    // 这里建立数据库的连接类MySqlConnector对象,并进行连接,
                    return $this->createConnector($config)->connect($config);
                } catch (PDOException $e) {
                    if (count($hosts) - 1 === $key && $this->container->
                        bound(ExceptionHandler::class)) {
                            $this->container->make(ExceptionHandler::class)->report($e);
                        }
                }
            }

            throw $e;
        };
    }

这个闭包调用了下面的函数,

    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']}]");
    }

得到一个连接类MySqlConnector,其父类正是前面第一节所讲的Illuminate\Database\Connectors\Connector, 然后进行连接(参考createPdoResolverWithHosts),我们再次把其代码贴出来,

<?php
namespace Illuminate\Database\Connectors;
use PDO;

class MySqlConnector extends Connector implements ConnectorInterface
{
    public function connect(array $config)
    {
        $dsn = $this->getDsn($config);
        $options = $this->getOptions($config); 

        // 关键看下面这句,在这里调用其父类中定义的createConnection创建PDO,并返回连接
        $connection = $this->createConnection($dsn, $config, $options);

        if (! empty($config['database'])) {
            $connection->exec("use `{$config['database']}`;");
        }

        $this->configureEncoding($connection, $config);

        // Next, we will check to see if a timezone has been specified in this config
        // and if it has we will issue a statement to modify the timezone with the
        // database. Setting this DB timezone is an optional configuration item.
        $this->configureTimezone($connection, $config);

        $this->setModes($connection, $config);

        return $connection;
    }
    ...
}

这里最关键的,是看$connection = $this->createConnection($dsn, $config, $options)这句,它调用了其父类Illuminate\Database\Connectors\Connector的createConnection()函数。这个正是在前面第一节里详细描述过的。

这样,

return $this->createConnector($config)->connect($config);

运行完毕,得到一个$pdo 闭包。

第二步,运行createSingleConnection中的createConnection(),

protected function createConnection($driver, $connection, $database, $prefix = '', 
    array $config = [])
{
        // 这个resolver没有去具体分析,实际运行过程中得到的是null.
        if ($resolver = Connection::getResolver($driver)) {
            return $resolver($connection, $database, $prefix, $config);
        }


        switch ($driver) {
            case 'mysql': 
                // 这里创建一个新的数据库连接器MySqlConnection
                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]");
    }
}

这个要注意,Illuminate\Database\MySqlConnection继承的类是Illuminate\Database\Connection。其传入的参数中,$connection正是前面得到的$pdo

附参考,

数据库全局范围内的脉络,注意'db' ,也就是DatabaseManager在容器中的解析步骤。

#1 
[internal function]: Composer\Autoload\ClassLoader->loadClass('Illuminate\\Data...')
#2 
Illuminate\Database\DatabaseServiceProvider.php(62): spl_autoload_call('Illuminate\\Data...')
#3 
Illuminate\Container\Container.php(749): 
Illuminate\Database\DatabaseServiceProvider->Illuminate\Database\{closure}(Object(Illuminate\Foundation\Application), Array)
#4 
Illuminate\Container\Container.php(631): 
Illuminate\Container\Container->build(Object(Closure))
#5 
Illuminate\Container\Container.php(586): 
Illuminate\Container\Container->resolve('db', Array)
#6 
Illuminate\Foundation\Application.php(732): 
Illuminate\Container\Container->make('db', Array)
#7 
Illuminate\Container\Container.php(1195): 
Illuminate\Foundation\Application->make('db')
#8 
Illuminate\Database\DatabaseServiceProvider.php(23): 
Illuminate\Container\Container->offsetGet('db')
#9 
[internal function]: Illuminate\Database\DatabaseServiceProvider->boot()

猜你喜欢

转载自blog.csdn.net/tanmx219/article/details/78886301