在 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
近期评论