在 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组件
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 61 62 63 64 65 66 67 68 69 70 71 72 73 74 | <?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
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 | <?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 设置为数据库连接组件
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 | <?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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | <?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
1 2 3 4 5 6 7 8 9 | <?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
1 2 3 4 5 6 7 8 9 | <?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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | <?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
1 2 3 4 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
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 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 | <?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
1 2 3 4 5 6 7 8 | $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
1 2 3 4 5 | '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,
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 | <?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
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 | // 判断当前请求是否通过命令行进行 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
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 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 | <?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
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 | <?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
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 | <?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
近期评论