1. 程式人生 > >Laravel5.5原始碼詳解 -- 資料庫的啟動與連線過程

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,可以參考以下官網

言歸正傳,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版以後開始提供。你可以參考官方網站,

因此不難明白,所謂的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。

不過,這些具體內容在網上已經寫得比較詳細,我這裡不再重複,可以參考

第三節 資料庫啟動大脈絡分析

我這裡類(物件)的呼叫關係用===>表示,==>表示物件內部的函式呼叫,::表示屬於該類的子函式,整個大脈絡如下:

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()