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

在咱們學習和使用一個開發框架時,不管使用什麼框架,如何鏈接數據庫、對數據庫進行增刪改查都是學習的重點,在Laravel中咱們能夠經過兩種方式與數據庫進行交互:php

  • DB, DB是與PHP底層的PDO直接進行交互的,經過查詢構建器提供了一個方便的接口來建立及運行數據庫查詢語句。
  • Eloquent Model, Eloquent是創建在DB的查詢構建器基礎之上,對數據庫進行了抽象的ORM,功能十分豐富讓咱們能夠避免寫複雜的SQL語句,並用優雅的方式解決了數據表之間的關聯關係。

上面說的這兩個部分都包括在了Illuminate/Database包裏面,除了做爲Laravel的數據庫層Illuminate/Database仍是一個PHP數據庫工具集, 在任何項目裏你均可以經過composer install illuminate/databse安裝並使用它。mysql

Database服務註冊和初始化

Database也是做爲一種服務註冊到服務容器裏提供給Laravel應用使用的,它的服務提供器是Illuminate\Database\DatabaseServiceProviderlaravel

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

    $this->registerConnectionServices();

    $this->registerEloquentFactory();

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

第一步:Model::clearBootedModels()。在 Eloquent 服務啓動以前爲了保險起見須要清理掉已經booted的Model和全局查詢做用域git

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

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

第二步:註冊ConnectionServicesgithub

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建立了數據庫鏈接實例,最後執行數據庫的增刪改查是由數據庫鏈接實例來完成的。sql

第三步:註冊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')
        );
    });
}
複製代碼

啓動數據庫服務bash

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的構造函數:app

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核心代碼學習裏,歡迎訪問閱讀。

相關文章
相關標籤/搜索