vitepress-blog/src/laravel/Eloquent查询构造器_1.md
2024-07-08 18:15:43 +09:00

530 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
layout: wiki
wiki: laravel #项目名
date: 2024-02-05 16:56
categories: [Laravel]
title: Eloquent构造器_1
---
{%quot el:h2 数据库 %}
`Laravel`的`ORM`是`Eloquent``Eloquent`是基于`DB`的查询构建器。
> `DB`, `DB`是与PHP底层的`PDO`直接进行交互的,通过查询构建器提供了一个方便的接口来创建及运行数据库查询语句。
> `Eloquent Model`, `Eloquent`是建立在`DB`的查询构建器基础之上,对数据库进行了抽象的`ORM`功能十分丰富让我们可以避免写复杂的SQL语句并用优雅的方式解决了数据表之间的关联关系。
> 上面说的这两个部分都包括在了`Illuminate/Database`包里面除了作为Laravel的数据库层`Illuminate/Database`还是一个PHP数据库工具集 在任何项目里你都可以通过`composer install illuminate/databse`安装并使用它。
## 服务注册
`Database`作为服务注册到服务容器中,服务提供者是`Illuminate\Database\DatabaseServiceProvider`
```php
namespace Illuminate\Database;
use Faker\Factory as FakerFactory;
use Faker\Generator as FakerGenerator;
use Illuminate\Contracts\Queue\EntityResolver;
use Illuminate\Database\Connectors\ConnectionFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\QueueEntityResolver;
use Illuminate\Support\ServiceProvider;
class DatabaseServiceProvider extends ServiceProvider
{
/**
* The array of resolved Faker instances.
*
* @var array
*/
protected static $fakers = [];
/**
* 启动服务
*
* @return void
*/
public function boot()
{
// 设置数据库连接解析器
Model::setConnectionResolver($this->app['db']);
// 注册模型监听,监听数据库事件
Model::setEventDispatcher($this->app['events']);
}
/**
* 在这里注册服务
*
* @return void
*/
public function register()
{
// 确保在注册数据库服务时清理掉这些已经启动的模型,以确保注册过程的干净
Model::clearBootedModels();
// 第二步注册ConnectionServices
$this->registerConnectionServices();
// 创建数据库模型实例,方便地生成模拟数据,用于测试或种子数据的填充。
$this->registerEloquentFactory();
// 注册队列实体解析器
$this->registerQueueableEntityResolver();
}
/**
* Register the primary database bindings.
*
* @return void
*/
protected function registerConnectionServices()
{
// 创建数据库连接实例
$this->app->singleton('db.factory', function ($app) {
return new ConnectionFactory($app);
});
// DatabaseManger 作为Database面向外部的接口DB这个Facade就是DatabaseManager的静态代理。
// 应用中所有与Database有关的操作都是通过与这个接口交互来完成的。
$this->app->singleton('db', function ($app) {
return new DatabaseManager($app, $app['db.factory']);
});
// 数据库连接实例是与底层PDO接口进行交互的底层类可用于数据库的查询、更新、创建等操作。
$this->app->bind('db.connection', function ($app) {
return $app['db']->connection();
});
// 提供数据库事务管理相关的功能
$this->app->singleton('db.transactions', function ($app) {
return new DatabaseTransactionsManager;
});
}
/**
* Register the Eloquent factory instance in the container.
*
* @return void
*/
protected function registerEloquentFactory()
{
$this->app->singleton(FakerGenerator::class, function ($app, $parameters) {
$locale = $parameters['locale'] ?? $app['config']->get('app.faker_locale', 'en_US');
if (! isset(static::$fakers[$locale])) {
static::$fakers[$locale] = FakerFactory::create($locale);
}
static::$fakers[$locale]->unique(true);
return static::$fakers[$locale];
});
}
/**
* Register the queueable entity resolver implementation.
*
* @return void
*/
protected function registerQueueableEntityResolver()
{
$this->app->singleton(EntityResolver::class, function () {
return new QueueEntityResolver;
});
}
}
```
`DatabaseManager`作为接口与外部交互,在应用需要时通过`ConnectionFactory`创建了数据库连接实例,最后执行数据库的增删改查是由数据库连接实例来完成。
`DB Facade`代理的是`DatabaseManager`,所以使用`DB`实际操作的是`DatabaseManager`,而数据库管理类实例化时,通过数据库链接对象工厂`(ConnectionFactory)`获取数据库连接对象`(Connection)`,然后进行具体的`CRUD`操作。
## DatabaseManager
数据库管理对象的构造函数
```php
// 绑定时传递进来的参数
$this->app->singleton('db', function ($app) {
// $app['db.factory'] 是传递进来的Connection工厂类
return new DatabaseManager($app, $app['db.factory']);
});
// ----------------------------------------------------------
public function __construct($app, ConnectionFactory $factory)
{
// 容器实例
$this->app = $app;
// 数据库连接工厂
$this->factory = $factory;
// 用于重连数据库连接
// 接收一个数据库连接实例作为参数
// 调用reconnect方法重新连接该数据库连接
// 保持数据库连接的稳定性和可靠性
$this->reconnector = function ($connection) {
$this->reconnect($connection->getNameWithReadWriteType());
};
}
```
当使用`DB`访问数据库时,`DB`通过`DatabaseManager`获取数据库连接实例,`DatabaseManager`没有对应的方法,会触发魔术方法`__callStatic`
```php
public function __call($method, $parameters)
{
return $this->connection()->$method(...$parameters);
}
//....
// $name指定获取的数据库连接名称
public function connection($name = null)
{
// 解析数据库连接名称
[$database, $type] = $this->parseConnectionName($name);
$name = $name ?: $database;
// 检查是否已经创建了指定名称的连接
if (! isset($this->connections[$name])) {
// 根据配置文件中的配置信息创建连接
// 创建连接后会调用configure方法对连接进行配置这可能包括设置连接的查询返回类型等
$this->connections[$name] = $this->configure(
// 创建连接
$this->makeConnection($database), $type
);
}
return $this->connections[$name];
}
```
解析数据库连接名称
```php
protected function parseConnectionName($name)
{
$name = $name ?: $this->getDefaultConnection();
// 返回指定的连接类型如果没有则默认为null
return Str::endsWith($name, ['::read', '::write'])
? explode('::', $name, 2) : [$name, null];
}
// -------------------------------------
// 获取配置文件下面的默认数据库连接名称
public function getDefaultConnection()
{
return $this->app['config']['database.default'];
}
```
### makeConnection创建配置信息
```php
protected function makeConnection($name)
{
// 获取配置文件中,配置的所有连接信息
$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 = $config['driver']])) {
return call_user_func($this->extensions[$driver], $config, $name);
}
// 创建连接实例
return $this->factory->make($config, $name);
}
protected function configuration($name)
{
// 获取连接名称
$name = $name ?: $this->getDefaultConnection();
// 获取配置文件中,配置的所有连接信息
$connections = $this->app['config']['database.connections'];
// 如果连接名称不存在,则抛出异常
if (is_null($config = Arr::get($connections, $name))) {
throw new InvalidArgumentException("Database connection [{$name}] not configured.");
}
// 解析配置信息
return (new ConfigurationUrlParser)
->parseConfiguration($config);
}
```
创建连接实例
#### ConnectionFactory创建数据库连接实例
```php
public function make(array $config, $name = null)
{
// 解析配置信息
$config = $this->parseConfig($config, $name);
// 如果配置了read键则创建一个读写分离的连接
if (isset($config['read'])) {
return $this->createReadWriteConnection($config);
}
// 创建单连接
return $this->createSingleConnection($config);
}
protected function createSingleConnection(array $config)
{
// 创建PDO连接
$pdo = $this->createPdoResolver($config);
// 创建连接实例
return $this->createConnection(
$config['driver'], $pdo, $config['database'], $config['prefix'], $config
);
}
// 创建连接实例
protected function createConnection($driver, $connection, $database, $prefix = '', array $config = [])
{
// 尝试获取给定驱动类型的连接解析器。如果解析器存在,则调用解析器来创建连接,并返回连接实例。
if ($resolver = Connection::getResolver($driver)) {
return $resolver($connection, $database, $prefix, $config);
}
// 根据给定的驱动类型,创建连接实例
switch ($driver) {
case 'mysql':
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}].");
}
protected function createPdoResolver(array $config)
{
// 创建不同类型的 PDO 连接解析器
return array_key_exists('host', $config)
? $this->createPdoResolverWithHosts($config)
: $this->createPdoResolverWithoutHosts($config);
}
protected function createPdoResolverWithHosts(array $config)
{
return function () use ($config) {
foreach (Arr::shuffle($hosts = $this->parseHosts($config)) as $key => $host) {
$config['host'] = $host;
try {
return $this->createConnector($config)->connect($config);
} catch (PDOException $e) {
continue;
}
}
throw $e;
};
}
// 创建 PDO 连接解析器
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']}].");
}
```
### 连接器
在`illuminate/database`中连接器Connector是专门负责与PDO交互连接数据库的以`mysql`驱动为例,其连接器为`Illuminate\Database\Connectors\MySqlConnector`
```php
namespace Illuminate\Database\Connectors;
use PDO;
class MySqlConnector extends Connector implements ConnectorInterface
{
/**
* Establish a database connection.
*
* @param array $config
* @return \PDO
*/
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']}`;");
}
// 设置事务隔离级别
$this->configureIsolationLevel($connection, $config);
// 设置编码
$this->configureEncoding($connection, $config);
// 设置时区
$this->configureTimezone($connection, $config);
// 设置模式
$this->setModes($connection, $config);
// 返回连接对象
return $connection;
}
}
public function createConnection($dsn, array $config, array $options)
{
[$username, $password] = [
$config['username'] ?? null, $config['password'] ?? null,
];
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);
}
```
- `$dsn`参数是数据库连接字符串,包含了连接到数据库所需的信息,如主机、端口、数据库名等。
- `$config` 参数是一个包含了数据库连接配置信息的数组。
- `$options` 参数是一个包含了数据库连接选项的数组,如连接超时时间、字符集等。
- 方法首先尝试调用 `createPdoConnection`方法来创建`PDO`连接实例,传递了连接字符串 `$dsn`、用户名和密码 `$username`、`$password`,以及连接选项 `$options`
- 如果在创建 `PDO`连接时抛出了异常,方法会尝试调用 `tryAgainIfCausedByLostConnection` 方法来处理异常情况,可能是由于连接丢失导致的异常。在这个方法中,会尝试重新连接数据库,以应对连接丢失的情况。
总的来说,`createConnection` 方法用于根据给定的连接信息和配置信息来创建数据库连接。在创建连接时,会尝试处理连接可能出现的异常情况,以确保连接的稳定性和可靠性。
### createPdoResolver方法返回pdo连接实例
看`createConnection`方法,
```php
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 = [])
{
if ($resolver = Connection::getResolver($driver)) {
return $resolver($connection, $database, $prefix, $config);
}
switch ($driver) {
case 'mysql':
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}].");
}
```
以`MySqlConnection`为例分析,所有的数据库`Connection`类都继承了`Illuminate\Database\Connection`类,其构造方法为
```php
public function __construct($pdo, $database = '', $tablePrefix = '', array $config = [])
{
$this->pdo = $pdo;
// First we will setup the default properties. We keep track of the DB
// name we are connected to since it is needed when some reflective
// type commands are run such as checking whether a table exists.
$this->database = $database;
$this->tablePrefix = $tablePrefix;
$this->config = $config;
// We need to initialize a query grammar and the query post processors
// which are both very important parts of the database abstractions
// so we initialize these to their default values while starting.
$this->useDefaultQueryGrammar();
$this->useDefaultPostProcessor();
}
public function useDefaultQueryGrammar()
{
$this->queryGrammar = $this->getDefaultQueryGrammar();
}
public function useDefaultPostProcessor()
{
$this->postProcessor = $this->getDefaultPostProcessor();
}
```
然后依次返回,最终发现`DatabaseManager`中,`make`方法返回的是`MySqlConnection`实例。
## 回到configure方法
`$this->makeConnection($database)`实际是数据库中配置的对应的驱动连接,如`MySqlConnection`实例。
```php
$this->connections[$name] = $this->configure(
$this->makeConnection($database), $type
);
protected function configure(Connection $connection, $type)
{
// 设置连接的读写类型。
$connection = $this->setPdoForType($connection, $type)->setReadWriteType($type);
// 设置连接的事件分发器
if ($this->app->bound('events')) {
$connection->setEventDispatcher($this->app['events']);
}
// 设置连接的事务管理器。
if ($this->app->bound('db.transactions')) {
$connection->setTransactionManager($this->app['db.transactions']);
}
// 设置连接的重新连接器。
$connection->setReconnector($this->reconnector);
// 注册配置的 `Doctrine` 数据库类型。
$this->registerConfiguredDoctrineTypes($connection);
return $connection;
}
```
- `$connection` 参数是要配置的数据库连接实例,它是`Illuminate\Database\Connection`类的一个实例。
- `$type` 参数是连接类型,通常是读取连接或写入连接,默认为读取连接。
- 根据应用程序是否已绑定了事件管理器和事务管理器,分别设置连接的事件分发器和事务管理器。
- `$connection->setReconnector($this->reconnector)`:这行代码设置了连接的重新连接器。重新连接器是一个可回调的闭包,用于重新连接失效的数据库连接。
- `$this->reconnector`是一个闭包,用于重新连接数据库连接,其参数是数据库连接实例,在`DatabaseManager`类的构造函数中赋值。
- 最后调用了`registerConfiguredDoctrineTypes`方法,用于注册配置过的 `Doctrine` 数据库类型。
## 总结
1. 数据库的服务提供者是`Illuminate\Database\DatabaseServiceProvider`类,其`register`方法中绑定了`Illuminate\Database\DatabaseManager`类、数据库连接工厂`ConnectionFactory`类以及数据库事物管理器`DatabaseTransactionsManager`
2. `DatabaseManager`类是数据库连接管理器,代理类是`DB`,实例化该`DatabaseManager`类时,传入容器实例以及数据库连接工厂`ConnectionFactory`类实例
3. 当使用`DB`代理类时,会调用`DatabaseManager`类的魔术方法`__callStatic`方法,该方法中通过`$this->connection()`获取数据库的连接实例对象,其中该类通过`makeConnection`方法创建对应的数据库连接实例对象
调用`DB`的方法,实际调用`DatabaseManager`类的`connection`方法,返回数据库连接实例对象,该实例对象通过`configure`方法进行配置,配置后返回配置后的数据库连接实例对象。当配置的数据库为`MySql`时,实际返回的是`MySqlConnection`实例对象,
该实例对象继承了`Illuminate\Database\MySqlConnection`类,该类中定义了`MySql`数据库连接的常用方法。最终调用`MySqlConnection`实例中定义的数据库常用方法。