在 Yii 2 Starter Kit 中数据库迁移的多租户实现
1、前文:http://www.shuijingwanwq.com/2018/01/18/2328/
2、参考第11步骤,db 组件移至开发环境,以方便于 Gii 的使用,\common\config\base.php,如图1
3、配置为生产环境,以便于测试数据库迁移的多租户实现,不依赖于 db 组件,如图2
4、运行命令:.\yii app/setup,报错:Exception ‘yii\base\InvalidConfigException’ with message ‘Failed to instantiate component or class “db”.’,如图3
5、基于 Trait 实现,将 getTenantDb() 方法放入 Trait 中,新建 \common\traits\TenantDb.php,通过 [[yii\di\ServiceLocator::setComponents()]] 方法注册数据库连接组件、RBAC组件
<?php
/**
* Created by PhpStorm.
* User: WangQiang
* Date: 2018/01/26
* Time: 16:25
*/
namespace common\traits;
use Yii;
use common\logics\http\tenant\Env;
use yii\web\ServerErrorHttpException;
/**
* 获取租户模块环境配置信息,存储至Redis,注册数据库连接组件
*
* @author Qiang Wang <shuijingwanwq@163.com>
* @since 1.0
*/
trait TenantDb
{
/**
* 数据库连接组件ID(基于多租户)
*
* @return string
*/
public static function getTenantDb()
{
$env = new Env();
$tenantEnv = $env->getTenantEnv();
if ($tenantEnv === false) {
if ($env->hasErrors()) {
foreach ($env->getFirstErrors() as $message) {
$firstErrors = $message;
}
throw new ServerErrorHttpException(Yii::t('error', Yii::t('error', Yii::t('error', '20006'), ['firstErrors' => $firstErrors])), 20006);
} elseif (!$env->hasErrors()) {
throw new ServerErrorHttpException('Multi-tenant HTTP requests fail for unknown reasons.');
}
}
$tenantDb = $tenantEnv['data']['tenantid'] . 'Db';
// 检查数据库连接组件、RBAC组件是否被注册
if (!(Yii::$app->has($tenantDb) && Yii::$app->has('authManager'))) {
// 注册数据库连接组件、RBAC组件
Yii::$app->setComponents([
$tenantDb => [
'class' => 'yii\db\Connection',
'dsn' => 'mysql:host=' . $tenantEnv['data']['db_info']['host'] . ';port=3306;dbname=' . $tenantEnv['data']['db_info']['database'] . '',
'username' => $tenantEnv['data']['db_info']['login'],
'password' => $tenantEnv['data']['db_info']['password'],
'tablePrefix' => $tenantEnv['data']['db_info']['prefix'],
'charset' => 'utf8',
'enableSchemaCache' => YII_ENV_PROD,
'schemaCache' => 'redisCache',
],
'authManager' => [
'class' => 'yii\rbac\DbManager',
'db' => $tenantDb,
'itemTable' => '{{%rbac_auth_item}}',
'itemChildTable' => '{{%rbac_auth_item_child}}',
'assignmentTable' => '{{%rbac_auth_assignment}}',
'ruleTable' => '{{%rbac_auth_rule}}'
],
]);
}
return $tenantDb;
}
}
6、编辑 \common\components\db\ActiveRecord.php,导入 common\traits\TenantDb
<?php
/**
* Created by PhpStorm.
* User: Administrator
* Date: 2018/01/16
* Time: 10:31
*/
namespace common\components\db;
use Yii;
use common\traits\TenantDb;
/**
* 导入 TenantDb,注册数据库连接组件
*
* @author Qiang Wang <shuijingwanwq@163.com>
* @since 1.0
*/
class ActiveRecord extends \yii\db\ActiveRecord
{
use TenantDb;
/**
* Returns the database connection used by this AR class.
* By default, the "db" application component is used as the database connection.
* You may override this method if you want to use a different database connection.
* @return Connection the database connection used by this AR class.
*/
public static function getDb()
{
$tenantDb = self::getTenantDb();
return Yii::$app->$tenantDb;
}
}
7、基于 Trait 实现,导入 common\traits\TenantDb,将 init() 方法放入 Trait 中,新建 \common\traits\TenantMigration.php,将 $tenantDb 设置为数据库连接组件
<?php
/**
* Created by PhpStorm.
* User: WangQiang
* Date: 2018/01/26
* Time: 16:25
*/
namespace common\traits;
use Yii;
use common\traits\TenantDb;
/**
* 导入 TenantDb,将 $tenantDb 设置为数据库连接组件
*
* @author Qiang Wang <shuijingwanwq@163.com>
* @since 1.0
*/
trait TenantMigration
{
use TenantDb;
/**
* Initializes the migration.
* This method will set [[db]] to be the 'db' application component, if it is `null`.
*/
public function init()
{
$tenantDb = self::getTenantDb();
$this->db = $tenantDb;
parent::init();
}
}
8、新建 \common\components\db\Migration.php,导入 common\traits\TenantMigration
<?php
/**
* Created by PhpStorm.
* User: Administrator
* Date: 2018/01/26
* Time: 13:48
*/
namespace common\components\db;
use common\traits\TenantMigration;
/**
* 导入 TenantMigration,将 $tenantDb 设置为数据库连接组件
*
* @author Qiang Wang <shuijingwanwq@163.com>
* @since 1.0
*/
class Migration extends \yii\db\Migration
{
use TenantMigration;
}
9、在目录 \common\migrations 中查找:yii\db\Migration,批量替换为:common\components\db\Migration,如图4
10、编辑 \common\migrations\db\m140703_123055_log.php,导入 common\traits\TenantMigration
<?php
require(Yii::getAlias('@yii/log/migrations/m141106_185632_log_init.php'));
use common\traits\TenantMigration;
class m140703_123055_log extends m141106_185632_log_init
{
use TenantMigration;
}
11、编辑 \common\migrations\db\m140703_123813_rbac.php,导入 common\traits\TenantMigration
<?php
require(Yii::getAlias('@yii/rbac/migrations/m140506_102106_rbac_init.php'));
use common\traits\TenantMigration;
class m140703_123813_rbac extends m140506_102106_rbac_init
{
use TenantMigration;
}
12、新建数据库迁移类,\console\controllers\MigrateController.php
<?php
/**
* Created by PhpStorm.
* User: Administrator
* Date: 2018/01/26
* Time: 17:35
*/
namespace console\controllers;
use common\traits\TenantMigration;
/**
* 导入 TenantMigration,将 $tenantDb 设置为数据库连接组件
*
* @author Qiang Wang <shuijingwanwq@163.com>
* @since 1.0
*/
class MigrateController extends \yii\console\controllers\MigrateController
{
use TenantMigration;
}
13、调整数据库迁移配置,编辑 \console\config\console.php,如图5
'migrate' => [
'class' => 'console\controllers\MigrateController',
'migrationPath' => '@common/migrations/db',
'migrationTable' => '{{%system_db_migration}}'
],
14、运行命令:.\yii app/setup,报错:Exception ‘yii\base\UnknownMethodException’ with message ‘Calling unknown method: yii\console\Request::get()’,如图6
15、获取请求参数时,判断当前请求是否通过命令行进行,编辑 \common\logics\http\tenant\Env.php
<?php
/**
* Created by PhpStorm.
* User: Administrator
* Date: 2018/01/17
* Time: 15:20
*/
namespace common\logics\http\tenant;
use Yii;
use yii\base\Model;
use yii\web\BadRequestHttpException;
use yii\web\ServerErrorHttpException;
/**
* 多租户的模块环境配置
*
* @author Qiang Wang <shuijingwanwq@163.com>
* @since 1.0
*/
class Env extends Model
{
public $app_name;
public $secret;
public $tenant_id;
public function attributeLabels()
{
return [
'app_name' => \Yii::t('model/http/tenant/env', 'App Name'),
'secret' => \Yii::t('model/http/tenant/env', 'Secret'),
'tenant_id' => \Yii::t('model/http/tenant/env', 'Tenant ID'),
];
}
/**
* 返回租户模块环境配置信息
*
* @return array|false
*
* 格式如下:
*
* 租户模块环境配置信息
* [
* 'message' => '', //说明
* 'data' => [], //数据
* ]
*
* 失败(将错误保存在 [[yii\base\Model::errors]] 属性中)
* false
*
* @throws ServerErrorHttpException 如果响应状态码不等于20x
*/
public function getTenantEnv()
{
/* 获取请求参数 */
$request = Yii::$app->request;
// 判断当前请求是否通过命令行进行
if ($request->isConsoleRequest) {
$get = $request->getParams();
/* 判断请求参数中租户ID是否存在 */
if (empty($get[1])) {
// throw new BadRequestHttpException(Yii::t('error', '20007'), 20007);
$get[1] = env('TENANT_DEFAULT_ID');
}
$this->tenant_id = $get[1];
} else {
$get = $request->get();
/* 判断请求参数中租户ID是否存在 */
if (empty($get['tenantid'])) {
// throw new BadRequestHttpException(Yii::t('error', '20007'), 20007);
$get['tenantid'] = env('TENANT_DEFAULT_ID');
}
$this->tenant_id = $get['tenantid'];
}
// 设置多租户数据的缓存键
$redisCache = Yii::$app->redisCache;
$tenantKey = 'tenant:' . $this->tenant_id;
// 从缓存中取回多租户数据
$tenantData = $redisCache[$tenantKey];
if ($tenantData === false) {
$this->app_name = env('TENANT_APP_NAME');
$this->secret = env('TENANT_SECRET');
$response = Yii::$app->tenantHttp->createRequest()
->setMethod('get')
->setUrl('getTenantEnv')
->setData([
'appname' => $this->app_name,
'secret' => $this->secret,
'tenantid' => $this->tenant_id,
])
->send();
// 检查响应状态码是否等于20x
if ($response->isOk) {
// 检查业务逻辑是否成功
if ($response->data['returnCode'] === 0) {
$tenantData = ['message' => $response->data['returnDesc'], 'data' => $response->data['returnData']];
// 将多租户数据存放到缓存供下次使用
$redisCache[$tenantKey] = $tenantData;
return $tenantData;
} else {
$this->addError('tenant_id', $response->data['returnDesc']);
return false;
}
} else {
throw new ServerErrorHttpException(Yii::t('error', Yii::t('error', Yii::t('error', '20005'), ['statusCode' => $response->getStatusCode()])), 20005);
}
} else {
return $tenantData;
}
}
}
16、删除数据库中所有表,运行命令:.\yii app/setup,报错:Exception ‘yii\db\Exception’ with message ‘SQLSTATE[42S02]: Base table or view not found: 1146 Table ‘cmcp-api.ca_system
_db_migration’ doesn’t exist’,如图7
se targets before executing this migration.’,如图8
18、删除 E:\wwwroot\cmcp-api\common\migrations\db\m140703_123055_log.php,因为日志组件已经配置为基于文件存储了的。
19、删除数据库中所有表,运行命令:.\yii app/setup,报错:Exception: Undefined class constant ‘STATUS_PUBLISHED’ (E:\wwwroot\cmcp-api\common\migrations\db\m150725_192740_seed_dat
a.php:63),如图9
20、编辑 \common\migrations\db\m150725_192740_seed_data.php,\common\models\Page 替换为 \common\logics\Page,如图10
$this->insert('{{%page}}', [
'slug' => 'about',
'title' => 'About',
'body' => 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.',
'status' => \common\logics\Page::STATUS_PUBLISHED,
'created_at' => time(),
'updated_at' => time(),
]);
21、删除数据库中所有表,运行命令:.\yii app/setup,报错:Exception ‘yii\base\UnknownPropertyException’ with message ‘Getting unknown property: yii\console\Application::tenantHtt
p’,如图11
22、通过应用组件配置客户端,编辑 \common\config\web.php,将 tenantHttp 组件移至 \common\config\base.php,如图12
'tenantHttp' => [
'class' => 'yii\httpclient\Client',
'baseUrl' => Yii::getAlias('@tenantUrl'),
'transport' => 'yii\httpclient\CurlTransport'
],
23、删除数据库中所有表,运行命令:.\yii app/setup default,Migrated up successfully.如图13
24、删除数据库中所有表,运行命令:.\yii migrate default,Migrated up successfully.
25、继续运行命令:.\yii rbac-migrate default,报错:Exception ‘yii\base\InvalidConfigException’ with message ‘Failed to instantiate component or class “db”.’,如图14
26、编辑 RBAC 数据库迁移类,\console\controllers\RbacMigrateController.php,
<?php
namespace console\controllers;
use yii\console\controllers\MigrateController;
use common\traits\TenantMigration;
/**
* @author Eugene Terentev <eugene@terentev.net>
*/
class RbacMigrateController extends MigrateController
{
use TenantMigration;
/**
* Creates a new migration instance.
* @param string $class the migration class name
* @return \common\rbac\Migration the migration instance
*/
protected function createMigration($class)
{
$file = $this->migrationPath . DIRECTORY_SEPARATOR . $class . '.php';
require_once($file);
return new $class();
}
}
27、继续运行命令:.\yii rbac-migrate default,Migrated up successfully.如图15
28、打开网址:http://backend.cmcp-api.localhost/ ,报错:SQLSTATE[42S02]: Base table or view not found: 1146 Table ‘cmcp-api.ca_system_log’ doesn’t exist
The SQL being executed was: SELECT COUNT(*) FROM `ca_system_log`,如图16
30、运行命令:.\yii migrate/create create_news_table,报错:Exception ‘yii\web\ServerErrorHttpException’ with message ‘多租户HTTP请求失败:模块信息未配置’,如图17
31、决定将租户ID从参数形式转换为选项,如–tenantid=default,编辑 \common\logics\http\tenant\Env.php
// 判断当前请求是否通过命令行进行
if ($request->isConsoleRequest) {
$get = $request->getParams();
foreach ($get as $value) {
$option = explode('=', $value);
if ($option[0] == '--tenantid') {
$optionValue = $option[1];
break;
}
}
/* 判断请求参数中租户ID是否存在 */
if (empty($optionValue)) {
// throw new BadRequestHttpException(Yii::t('error', '20007'), 20007);
$optionValue = env('TENANT_DEFAULT_ID');
}
$this->tenant_id = $optionValue;
} else {
$get = $request->get();
/* 判断请求参数中租户ID是否存在 */
if (empty($get['tenantid'])) {
// throw new BadRequestHttpException(Yii::t('error', '20007'), 20007);
$get['tenantid'] = env('TENANT_DEFAULT_ID');
}
$this->tenant_id = $get['tenantid'];
}
32、通过覆盖在 [[yii\console\Controller::options()]] 中的方法, 以指定可用于控制台命令(controller/actionID)选项。编辑 \console\controllers\AppController.php
<?php
namespace console\controllers;
use Yii;
use yii\console\Controller;
use yii\helpers\Console;
/**
* @author Eugene Terentev <eugene@terentev.net>
*/
class AppController extends Controller
{
public $writablePaths = [
'@common/runtime',
'@frontend/runtime',
'@frontend/web/assets',
'@backend/runtime',
'@backend/web/assets',
'@api/runtime',
'@api/web/assets',
'@storage/cache',
'@storage/web/source'
];
public $executablePaths = [
'@backend/yii',
'@api/yii',
'@frontend/yii',
'@console/yii',
];
public $generateKeysPaths = [
'@base/.env'
];
public $tenantid;
public function options($actionID)
{
return ['color', 'interactive', 'help', 'tenantid'];
}
public function actionSetup()
{
$this->runAction('set-writable', ['interactive' => $this->interactive]);
$this->runAction('set-executable', ['interactive' => $this->interactive]);
$this->runAction('set-keys', ['interactive' => $this->interactive]);
\Yii::$app->runAction('migrate/up', ['interactive' => $this->interactive]);
\Yii::$app->runAction('rbac-migrate/up', ['interactive' => $this->interactive]);
}
public function actionSetWritable()
{
$this->setWritable($this->writablePaths);
}
public function actionSetExecutable()
{
$this->setExecutable($this->executablePaths);
}
public function actionSetKeys()
{
$this->setKeys($this->generateKeysPaths);
}
public function setWritable($paths)
{
foreach ($paths as $writable) {
$writable = Yii::getAlias($writable);
Console::output("Setting writable: {$writable}");
@chmod($writable, 0777);
}
}
public function setExecutable($paths)
{
foreach ($paths as $executable) {
$executable = Yii::getAlias($executable);
Console::output("Setting executable: {$executable}");
@chmod($executable, 0755);
}
}
public function setKeys($paths)
{
foreach ($paths as $file) {
$file = Yii::getAlias($file);
Console::output("Generating keys in {$file}");
$content = file_get_contents($file);
$content = preg_replace_callback('/<generated_key>/', function () {
$length = 32;
$bytes = openssl_random_pseudo_bytes(32, $cryptoStrong);
return strtr(substr(base64_encode($bytes), 0, $length), '+/', '_-');
}, $content);
file_put_contents($file, $content);
}
}
}
33、通过覆盖在 [[yii\console\Controller::options()]] 中的方法, 以指定可用于控制台命令(controller/actionID)选项。编辑 \console\controllers\MigrateController.php、\console\controllers\RbacMigrateController.php
\console\controllers\MigrateController.php
<?php
/**
* Created by PhpStorm.
* User: Administrator
* Date: 2018/01/26
* Time: 17:35
*/
namespace console\controllers;
use common\traits\TenantMigration;
/**
* 导入 TenantMigration,将 $tenantDb 设置为数据库连接组件
*
* @author Qiang Wang <shuijingwanwq@163.com>
* @since 1.0
*/
class MigrateController extends \yii\console\controllers\MigrateController
{
use TenantMigration;
public $tenantid;
public function options($actionID)
{
return ['color', 'interactive', 'help', 'tenantid'];
}
}
\console\controllers\RbacMigrateController.php
<?php
namespace console\controllers;
use yii\console\controllers\MigrateController;
use common\traits\TenantMigration;
/**
* @author Eugene Terentev <eugene@terentev.net>
*/
class RbacMigrateController extends MigrateController
{
use TenantMigration;
public $tenantid;
public function options($actionID)
{
return ['color', 'interactive', 'help', 'tenantid'];
}
/**
* Creates a new migration instance.
* @param string $class the migration class name
* @return \common\rbac\Migration the migration instance
*/
protected function createMigration($class)
{
$file = $this->migrationPath . DIRECTORY_SEPARATOR . $class . '.php';
require_once($file);
return new $class();
}
}
34、删除数据库中所有表,调整第24、25、26步骤的命令,.\yii app/setup –tenantid=default、.\yii migrate –tenantid=default、.\yii rbac-migrate –tenantid=default,成功运行
35、运行命令:.\yii migrate/create create_news_table、.\yii migrate/create create_news_table –tenantid=default,成功运行,如图18
36、由于已经在控制器中定义了数据库 application component 的 ID,将第8(删除 \common\components\db\Migration.php)、9、11步骤还原,在目录 \common\migrations 中查找:common\components\db\Migration,批量替换为:yii\db\Migration,如图19





![删除数据库中所有表,运行命令:.\yii app/setup,报错:Exception 'yii\db\Exception' with message 'SQLSTATE[42S02]: Base table or view not found: 1146 Table 'cmcp-api.ca_system _db_migration' doesn't exist'](http://www.shuijingwanwq.com/wp-content/uploads/2018/01/7-3.png)








![打开网址:http://backend.cmcp-api.localhost/ ,报错:SQLSTATE[42S02]: Base table or view not found: 1146 Table 'cmcp-api.ca_system_log' doesn't exist The SQL being executed was: SELECT COUNT(*) FROM `ca_system_log`](http://www.shuijingwanwq.com/wp-content/uploads/2018/01/16-2.png)



近期评论