Yaf集成Eloquent——使用事务以及DB Facade

TL;DR

集成方法参见 Yaf集成Eloquent

集成基类的多个 Model 如果要正确的运行事务,需要保证各个 Model 的实例使用的是同一个数据库连接,在代码上可以通过共用同一个 Illuminate\Database\Capsule\Manager 对象实现。

使用 DB Facade 需要为 Facade 提供已经关联了 db 作为键,以 Illuminate\Database\Capsule\Manager 的实例为值的容器。

实验环境

  • MySQL 5.6
  • PHP 5.6.31
  • Yaf 2.3.3
  • Eloquent 4.2.17(5.0亦可)

事务

Laravel 中的数据库事务

Laravel 中的数据库事务可以通过 DB Facade 开启:

1
2
3
4
5
6
DB::transaction(function()
{
DB::table('users')->update(['votes' => 1]);

DB::table('posts')->delete();
});

即通过通过 DB Facade 访问了服务容器内的各个 Model 发起了事务。

MySQL 事务

MySQL 中执行事务有两大前提条件:

  • 同一个数据库
  • 同一个数据库连接

如果使用 PDO 连接 MySQL 发起事务,那么 PDO 对象应该只有一个。

原有样例存在的问题

部分单独使用 Eloquent 的样例,包括自己早期的尝试在内,都存在一个类似的情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<?php

use Illuminate\Database\Capsule\Manager as IlluminateCapsule;
use Illuminate\Database\Eloquent\Model as IlluminateModel;
use Yaf\Registry as YRegistry;


class BaseModel extends IlluminateModel
{

protected $config = null;
protected $capsule = null;

public function __construct(array $attributes = array())
{

parent::__construct($attributes);
$dbConfigKey = DATABASE_CONFIG_KEY;
$this->config = YRegistry::get('config');

if (!$this->config->$dbConfigKey) {
throw new Exception("Must configure database in .ini first");
}

$this->config = $this->config->$dbConfigKey->toArray();
$this->capsule = new IlluminateCapsule();
$this->capsule->addConnection($this->config);
$this->capsule->bootEloquent();
}
}

这里在构造函数中会构建一个 Illuminate\Database\Capsule\Manager $capsue 对象,这一对象是 Eloquent 的关键部分之一,实现了连接管理等功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php namespace Illuminate\Database\Capsule;

use PDO;
use Illuminate\Events\Dispatcher;
use Illuminate\Cache\CacheManager;
use Illuminate\Container\Container;
use Illuminate\Database\DatabaseManager;
use Illuminate\Database\Eloquent\Model as Eloquent;
use Illuminate\Database\Connectors\ConnectionFactory;
use Illuminate\Support\Traits\CapsuleManagerTrait;

class Manager {

use CapsuleManagerTrait;

/**
* The database manager instance.
*
* @var \Illuminate\Database\DatabaseManager
*/

protected $manager;

通过内置的 protected \Illuminate\Database\DatabaseManager $manager 对象,建立真正的连接。

Manager 会根据声明的连接配置名称创建对应的连接,通过连接配置名称可以直接获取对应的连接对象,注入到实际执行查询操作的 Illuminate\Database\Query\Builder 中完成查询。

连接是按序连接,在需要进行查询时,\Illuminate\Database\DatabaseManager 会调用 \Illuminate\Database\Connectors\ConnectionFactory 成员的 make() 方法创建连接,在这一方法中完成连接对象的创建和连接操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
<?php namespace Illuminate\Database\Connectors;

use PDO;
use Illuminate\Container\Container;
use Illuminate\Database\MySqlConnection;
use Illuminate\Database\SQLiteConnection;
use Illuminate\Database\PostgresConnection;
use Illuminate\Database\SqlServerConnection;

class ConnectionFactory {

/**
* The IoC container instance.
*
* @var \Illuminate\Container\Container
*/
protected $container;

/**
* Create a new connection factory instance.
*
* @param \Illuminate\Container\Container $container
* @return void
*/
public function __construct(Container $container)
{
$this->container = $container;
}

/**
* Establish a PDO connection based on the configuration.
*
* @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);
}

/**
* Create a single database connection instance.
*
* @param array $config
* @return \Illuminate\Database\Connection
*/
protected function createSingleConnection(array $config)
{
$pdo = $this->createConnector($config)->connect($config);

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

那么问题来了,在多个 Model 实现类进行事务操作时,对于每一个子类来说,基类 BaseModel 中都会建立一个全新的 DatabaseManager 对象,实际上对于每个 Model 来说都建立了不同的数据库连接,不同连接下执行事务是不可能保证 ACID 的。

解决

Laravel 中的处理

来看原生支持 Eloquent 的 Laravel 是如何处理的。

阅读 Illuminate\Database\DatabaseServiceProvider 的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
* Register the primary database bindings.
*
* @return void
*/
protected function registerConnectionServices()
{
// The connection factory is used to create the actual connection instances on
// the database. We will inject the factory into the manager so that it may
// make the connections while they are actually needed and not of before.
$this->app->singleton('db.factory', function ($app) {
return new ConnectionFactory($app);
});

// The database manager is used to resolve various connections, since multiple
// connections might be managed. It also implements the connection resolver
// interface which may be used by other components requiring connections.
$this->app->singleton('db', function ($app) {
return new DatabaseManager($app, $app['db.factory']);
});

$this->app->bind('db.connection', function ($app) {
return $app['db']->connection();
});
}

Laravel的处理很简单,将 \Illuminate\Database\DatabaseManager 处理成单例。

本文解决方案

样例 中使用了同样的方式,即将此对象实现为单例模式。

DB Facade

通过 Facade 可以方便的访问数据库相关功能。

Eloquent 中 Illuminate\Support\Facades\DB 即是入口。

Laravel 中的 DB Facade

Laravel 中的 Facade 实际上是通过访问已关联的应用程序对象中已关联的对象,通过对象实现的 call 以及 callStatic 魔术方法,实现功能。

摘录部分 DB Facade 代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
protected static function getFacadeAccessor()
{
return 'db';
}

/**
* Resolve the facade root instance from the container.
*
* @param string|object $name
* @return mixed
*/
protected static function resolveFacadeInstance($name)
{
if (is_object($name)) {
return $name;
}

if (isset(static::$resolvedInstance[$name])) {
return static::$resolvedInstance[$name];
}

return static::$resolvedInstance[$name] = static::$app[$name];
}

/**
* Get the root object behind the facade.
*
* @return mixed
*/
public static function getFacadeRoot()
{
return static::resolveFacadeInstance(static::getFacadeAccessor());
}

/**
* Handle dynamic, static calls to the object.
*
* @param string $method
* @param array $args
* @return mixed
*
* @throws \RuntimeException
*/
public static function __callStatic($method, $args)
{
$instance = static::getFacadeRoot();

if (! $instance) {
throw new RuntimeException('A facade root has not been set.');
}

return $instance->$method(...$args);
}

可以看到,DB Facade 访问的是 $app['db'] 对应的对象。

回到之前 Illuminate\Database\DatabaseServiceProviderregisterConnectionServices 方法:

1
2
3
$this->app->singleton('db', function ($app) {
return new DatabaseManager($app, $app['db.factory']);
});

实际上 $app['db'] 关联的是一个 DatabaseManager 对象。

文中的 DB Facade

按照这一思路,将 Illuminate\Support\Facades\DB 的 $app 对象增加一个 key db,并关联上当前存在的 DatabaseManager 对象即可:

1
2
3
4
5
self::$capsule = new IlluminateCapsule();
self::$capsule->bootEloquent();
Illuminate\Support\Facades\DB::setFacadeApplication([
'db' => self::$capsule->getDatabaseManager(),
]);