Laravel核心程式碼學習 -- Database 基礎介紹

kevinyan發表於2018-05-29

在我們學習和使用一個開發框架時,無論使用什麼框架,如何連線資料庫、對資料庫進行增刪改查都是學習的重點,在Laravel中我們可以通過兩種方式與資料庫進行互動:

  • DB, DB是與PHP底層的PDO直接進行互動的,通過查詢構建器提供了一個方便的介面來建立及執行資料庫查詢語句。
  • Eloquent Model, Eloquent是建立在DB的查詢構建器基礎之上,對資料庫進行了抽象的ORM,功能十分豐富讓我們可以避免寫複雜的SQL語句,並用優雅的方式解決了資料表之間的關聯關係。

上面說的這兩個部分都包括在了Illuminate/Database包裡面,除了作為Laravel的資料庫層Illuminate/Database還是一個PHP資料庫工具集, 在任何專案裡你都可以通過composer install illuminate/databse安裝並使用它。

Database服務註冊和初始化

Database也是作為一種服務註冊到服務容器裡提供給Laravel應用使用的,它的服務提供器是Illuminate\Database\DatabaseServiceProvider

public function register()
{
    Model::clearBootedModels();

    $this->registerConnectionServices();

    $this->registerEloquentFactory();

    $this->registerQueueableEntityResolver();
}
複製程式碼

第一步:Model::clearBootedModels()。在 Eloquent 服務啟動之前為了保險起見需要清理掉已經booted的Model和全域性查詢作用域

/**
 * Clear the list of booted models so they will be re-booted.
 *
 * @return void
 */
public static function clearBootedModels()
{
    static::$booted = [];

    static::$globalScopes = [];
}
複製程式碼

第二步:註冊ConnectionServices

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();
    });
}
複製程式碼
  • db.factory用來建立資料庫連線例項,它將被注入到DatabaseManager中,在講服務容器繫結時就說過了依賴注入的其中一個作用是延遲初始化物件,所以只要在用到資料庫連線例項時它們才會被建立。
  • db DatabaseManger 作為Database面向外部的介面,DB這個Facade就是DatabaseManager的靜態代理。應用中所有與Database有關的操作都是通過與這個介面互動來完成的。
  • db.connection 資料庫連線例項,是與底層PDO介面進行互動的底層類,可用於資料庫的查詢、更新、建立等操作。

所以DatabaseManager作為介面與外部互動,在應用需要時通過ConnectionFactory建立了資料庫連線例項,最後執行資料庫的增刪改查是由資料庫連線例項來完成的。

第三步:註冊Eloquent工廠

protected function registerEloquentFactory()
{
    $this->app->singleton(FakerGenerator::class, function ($app) {
        return FakerFactory::create($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')
        );
    });
}
複製程式碼

啟動資料庫服務

public function boot()
{
    Model::setConnectionResolver($this->app['db']);

    Model::setEventDispatcher($this->app['events']);
}
複製程式碼

資料庫服務的啟動主要設定 Eloquent Model 的連線分析器(connection resolver),讓model能夠用db服務連線資料庫。還有就是設定資料庫事件的分發器 dispatcher,用於監聽資料庫的事件。

DatabaseManager

上面說了DatabaseManager是整個資料庫服務的介面,我們通過DB門面進行操作的時候實際上呼叫的就是DatabaseManager,它會通過資料庫連線物件工廠(ConnectionFacotry)獲得資料庫連線物件(Connection),然後資料庫連線物件會進行具體的CRUD操作。我們先看一下DatabaseManager的建構函式:

public function __construct($app, ConnectionFactory $factory)
{
    $this->app = $app;
    $this->factory = $factory;
}
複製程式碼

ConnectionFactory是在上面介紹的繫結db服務的時候傳遞給DatabaseManager的。比如我們現在程式裡執行了DB::table('users')->get(), 在DatabaseManager裡並沒有table方法然後就會觸發魔術方法__call

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

    public function __call($method, $parameters)
    {
        return $this->connection()->$method(...$parameters);
    }
    
    public function connection($name = null)
    {
        list($database, $type) = $this->parseConnectionName($name);

        $name = $name ?: $database;

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

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

}
複製程式碼

connection方法會返回資料庫連線物件,這個過程首先是解析連線名稱parseConnectionName

protected function parseConnectionName($name)
{
    $name = $name ?: $this->getDefaultConnection();
    // 檢查connection name 是否以::read, ::write結尾  比如'ucenter::read'
    return Str::endsWith($name, ['::read', '::write'])
                        ? explode('::', $name, 2) : [$name, null];
}

public function getDefaultConnection()
{
    // laravel預設是mysql,這裡假定是常用的mysql連線
    return $this->app['config']['database.default'];
}
複製程式碼

如果沒有指定連線名稱,Laravel會使用database配置裡指定的預設連線名稱, 接下來makeConnection方法會根據連線名稱來建立連線例項:

protected function makeConnection($name)
{
    //假定$name是'mysql', 從config/database.php中獲取'connections.mysql'的配置
	 $config = $this->configuration($name);

	//首先去檢查在應用啟動時是否通過連線名註冊了extension(閉包), 如果有則通過extension獲得連線例項
	//比如在AppServiceProvider裡通過DatabaseManager::extend('mysql', function () {...})
    if (isset($this->extensions[$name])) {
        return call_user_func($this->extensions[$name], $config, $name);
    }
	
	//檢查是否為連線配置指定的driver註冊了extension, 如果有則通過extension獲得連線例項
    if (isset($this->extensions[$driver])) {
        return call_user_func($this->extensions[$driver], $config, $name);
    }
    
    // 通過ConnectionFactory資料庫連線物件工廠獲取Mysql的連線類    
    return $this->factory->make($config, $name);
}    
複製程式碼

ConnectionFactory

上面makeConnection方法使用了資料庫連線物件工程來獲取資料庫連線物件,我們來看一下工廠的make方法:

/**
 * 根據配置建立一個PDO連線
 *
 * @param  array   $config
 * @param  string  $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);
}

protected function parseConfig(array $config, $name)
{
    return Arr::add(Arr::add($config, 'prefix', ''), 'name', $name);
}
複製程式碼

在建立連線之前, 先通過parseConfig向配置引數中新增預設的 prefix 屬性與 name 屬性。

接下來根據配置檔案中是否設定了讀寫分離。如果設定了讀寫分離,那麼就會呼叫 createReadWriteConnection 函式,生成具有讀、寫兩個功能的 connection;否則的話,就會呼叫 createSingleConnection 函式,生成普通的連線物件。

protected function createSingleConnection(array $config)
{
    $pdo = $this->createPdoResolver($config);

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

protected function createConnection($driver, $connection, $database, $prefix = '', array $config = [])
{
	......
    switch ($driver) {
        case 'mysql':
            return new MySqlConnection($connection, $database, $prefix, $config);
        case 'pgsql':
            return new PostgresConnection($connection, $database, $prefix, $config);
        ......                
    }

    throw new InvalidArgumentException("Unsupported driver [$driver]");
}
複製程式碼

建立資料庫連線的方法createConnection裡引數$pdo是一個閉包:

function () use ($config) {
    return $this->createConnector($config)->connect($config);
};
複製程式碼

這就引出了Database服務中另一部份聯結器Connector, Connection物件是依賴聯結器連線上資料庫的,所以在探究Connection之前我們先來看看聯結器Connector。

Connector

illuminate/database中聯結器Connector是專門負責與PDO互動連線資料庫的,我們接著上面講到的閉包引數$pdo往下看

createConnector方法會建立聯結器:

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']}]");
}
複製程式碼

這裡我們還是以mysql舉例看一下Mysql的聯結器。

class MySqlConnector extends Connector implements ConnectorInterface 
{
    public function connect(array $config)
    {
        //生成PDO連線資料庫時用的DSN連線字串
        $dsn = $this->getDsn($config);
		//獲取要傳給PDO的選項引數
        $options = $this->getOptions($config);
		//建立一個PDO連線物件
        $connection = $this->createConnection($dsn, $config, $options);

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

		//為連線設定字符集和collation
        $this->configureEncoding($connection, $config);
		//設定time zone
        $this->configureTimezone($connection, $config);
		//為資料庫會話設定sql mode
        $this->setModes($connection, $config);

      return $connection;
    }
}
複製程式碼

這樣就通過聯結器與PHP底層的PDO互動連線上資料庫了。

Connection

所有型別資料庫的Connection類都是繼承了Connection父類:

class MySqlConnection extends Connection
{
	 ......
}

class Connection implements ConnectionInterface
{
    public function __construct($pdo, $database = '', $tablePrefix = '', array $config = [])
    {
        $this->pdo = $pdo;

        $this->database = $database;

        $this->tablePrefix = $tablePrefix;

        $this->config = $config;

        $this->useDefaultQueryGrammar();

        $this->useDefaultPostProcessor();
    }
    ......   
    public function table($table)
    {
        return $this->query()->from($table);
    }
    ......
    public function query()
    {
        return new QueryBuilder(
            $this, $this->getQueryGrammar(), $this->getPostProcessor()
        );
    }
    ......
}
複製程式碼

Connection就是DatabaseManager代理的資料庫連線物件了, 所以最開始執行的程式碼DB::table('users')->get()經過我們上面講的歷程,最終是由Connection來完成執行的,table方法返回了一個QueryBuilder物件,這個物件裡定義裡那些我們經常用到的where, get, first等方法, 它會根據呼叫的方法生成對應的SQL語句,最後通過Connection物件執行來獲得最終的結果。 詳細內容我們等到以後講查詢構建器的時候再看。

總結

說的東西有點多,我們來總結下文章裡講到的Database的這幾個元件的角色

名稱 作用
DB DatabaseManager的靜態代理
DatabaseManager Database面向外部的介面,應用中所有與Database有關的操作都是通過與這個介面互動來完成的。
ConnectionFactory 建立資料庫連線物件的類工廠
Connection 資料庫連線物件,執行資料庫操作最後都是通過它與PHP底層的PDO互動來完成的
Connector 作為Connection的成員專門負責通過PDO連線資料庫

我們需要先理解了這幾個元件的作用,在這些基礎之上再去順著看查詢構建器的程式碼。

本文已經收錄在系列文章Laravel核心程式碼學習裡,歡迎訪問閱讀。

相關文章