Laravel5.5原始碼詳解 -- 資料庫的啟動與連線過程
Laravel5.5原始碼詳解 – 資料庫的啟動與連線過程
整個laravel的操作,一般情況下,資料庫的處理會佔掉很大一部分。所以對資料 庫處理的理解,顯得尤為重要。關於其原始碼解析,網上有非常多的文獻,但流程一般都含糊其辭,讀完來龍去脈甚為不解。所以,我自己做了一次流程分析,並記錄下全過程。
Laravel對不同資料庫連線的例項封裝了對應連線的PDO類,為上層使用資料庫連線例項提供了統一的介面。我這裡原始碼分析,都以以mySql為例項進行講解,過程大致如下,
DB::table('users') --> 拿到PDO --> 呼叫核心類Illuminate\Database\Connection
這裡先區域性後全域性,分三部分講解。
- 資料庫的連線與PHP-PDO的關係:解釋laravel與資料庫的接洽點;
- 資料庫查詢構造流程:理解是如何最後實現呼叫核心類Connection::table()函式的;
- 資料庫啟動大脈絡分析:理解全流程,然後重點是如何拿到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()