使用Migrations为整个数据库表创建迁移 [ 2.0 版本 ]
本教程为整个数据库表进行创建迁移,弥补以前未做的工作,且仅适合于Migrations(2.0.8)版本用户及以上。
大家都知道Migrations是一个在开发和维护数据库驱动的应用过程中,数据库的结构与源代码的开发同步更新。例如,在应用开发的过程中,新建了一张表,在应用部署到生产环境后,发现需要为这张表创建一个索引以提升查询性能,等等。因为数据库结构改变后需要源代码随之而改变,Yii支持此类数据库迁移特征,这样你就可以用数据库迁移的形式追踪数据库的变化,也就是与源代码同步的版本控制。
那么我现在数据表有接近300多张,所以不可能每张表进行命令创建迁移,这样太浪费时间且项目也不止一个,所以我想到一个思路,就是使用命令让程序批量将每张表创建迁移文件,那么原生的Migrations据我了解是没办法实现将表里每个字段都输出到迁移代码里面,所以我们需要稍微改动一下。
我们先找到一个核心文件:/vendor/yiisoft/yii2/console/controllers/BaseMigrateController.php
创建迁移的视图文件:/vendor/yiisoft/yii2/views/createTableMigration.php
我们先打开核心文件(BaseMigrateController.php)方法:actionCreate
行数大概在:493行。
使用Migrations命令创建迁移的时候,命令会询问我们是否需要创建,填写y 或 n,那么我们既然要批量创建,肯定是不能允许这种阻止程序的事情发生,在502行,有个if判断$this->confirm("Create new migration '$file'?")
,这句代码就是在我们操作Migrations无论创建或其他操作的时候都会询问,那么我们在if判断里面添加一个或者条件preg_match('/^create_(.+)$/', $name, $matches)
意思就是如果我是创建我就不需要经过询问(当然后期如果有类似需求,可以直接将这个if判断询问干掉)。
在if判断里面有做了六件事,我们这次仅针对于创建的时候修改,找到else if的preg_match('/^create_(.+)$/', $name, $matches)
这个条件里面,以下是我的代码:
$this->addDefaultPrimaryKey();
$primaryKeyArray = $createIndexArray = array();
$tableInfo = Yii::$app->getDb()->getSchema()->getTableSchema($matches[1]);
foreach($tableInfo->columns as $key => $value):
if($value->isPrimaryKey == 1 && !$value->autoIncrement):
$primaryKeyArray[] = $value->name;
endif;
endforeach;
$fieldsIndex = Yii::$app->db->createCommand("SHOW index FROM {$matches[1]} WHERE Key_name<>'PRIMARY'")->queryAll();
foreach($fieldsIndex as $key => $value):
$createIndexArray[$key]['Key_name'] = $value['Key_name'];
$createIndexArray[$key]['Column_name'] = $value['Column_name'];
$createIndexArray[$key]['Index_cat'] = $value['Non_unique'] < 1 ? 'Unique' : 'Normal';
$createIndexArray[$key]['Index_type'] = $value['Index_type'];
endforeach;
$content = $this->renderFile(Yii::getAlias($this->generatorTemplateFiles['create_table']), [
'className' => $className,
'table' => mb_strtolower($matches[1], Yii::$app->charset),
'fields' => $this->fields,
'tableInfo' => $tableInfo,
'primaryKeyArray' => $primaryKeyArray,
'createIndexArray' => $createIndexArray
]);
思路是,先用Yii::$app->getDb()->getSchema()->getTableSchema(表名)
方法获取到表字段数据,然后我们循环字段,判断isPrimaryKey是否为1 且 autoIncrement是否不存在(因为有的表可能不需要自增而需要主键,这个循环判断就是为了干这件事),然后我们会发现Yii::$app->getDb()->getSchema()->getTableSchema(表名)
方法并不能获取到我的索引字段,那么我们就不要局限于Schema,我们改用mysql语句来查询:Yii::$app->db->createCommand("SHOW index FROM {$matches[1]} WHERE Key_name<>'PRIMARY'")->queryAll()
。
这里为什么要新增条件 WHERE Key_name<>'PRIMARY'
,因为当你有个自增主键的时候,他也会输出出来,但这个自增主键并不是我们想要的索引字段,所以我们使用条件将他干掉。
下面foreach循环就是为了等下输出的时候方便(Non_unique在作者这里原以为用Migrations新增索引的时候能该类型,所以就写上去了,谁知道后面发现索引类型,已经写死了,必须为unique类型,createIndex方法代码在:/vendor/yiisoft/yii2/db/Migration.php 468行)
数据表有用到外键的朋友,代码你们可能要自己手写一小段了,作者项目中未遇到外键所以代码没写,在Yii::$app->getDb()->getSchema()->getTableSchema(表名)
方法中,已经查出了表的外键,你们可以利用。
接着往下代码就是渲染视图模板,模板路径在上面刚刚已经说了,这个时候,我们把刚刚查出来的三个数组传进去。
现在开始到视图模板(/vendor/yiisoft/yii2/views/createTableMigration.php):我们修改up
方法里面的代码,这里能看到只有一个自增ID。
$this->createTable('<?= $table ?>', [
<?php foreach ($tableInfo->columns as $key => $value): ?>
'<?= $key ?>' => $this-><?php if($value->isPrimaryKey == 1 && $value->autoIncrement == 1): ?>primaryKey()<?php else: ?><?php if($value->type == 'smallint'): ?>smallinteger<?php elseif($value->type == 'bigint'): ?>biginteger<?php else: ?><?= $value->type ?><?php endif; ?>(<?php if(isset($value->scale)): ?><?= $value->size ?>, <?= $value->scale ?><?php else: ?><?= $value->size ?><?php endif; ?>)<?php if(!$value->allowNull): ?>->notNull()<?php endif; ?><?php if(isset($value->defaultValue) && $value->defaultValue != 'CURRENT_TIMESTAMP' && $value->defaultValue != '' && $value->defaultValue != '\'\''): ?>->defaultValue('<?= $value->defaultValue ?>')<?php endif; ?><?php if(isset($value->comment) && $value->comment != ''): ?>->comment('<?= $value->comment ?>')<?php endif; ?><?php endif; ?><?= ",\n" ?>
<?php endforeach; ?>
]);
<?php if(count($primaryKeyArray) > 0 && !empty($primaryKeyArray)): ?>
$this->addPrimaryKey('<?= $table ?>', '<?= $table ?>', '<?= implode(",", $primaryKeyArray) ?>');
<?php endif; ?>
<?php if(count($createIndexArray) > 0 && !empty($createIndexArray)): ?>
<?php foreach($createIndexArray as $key => $value): ?>
$this->createIndex('<?= $value['Key_name'] ?>', '<?= $table ?>', '<?= $value['Column_name'] ?>', true);
<?php endforeach; ?>
<?php endif; ?>
以上代码就是将刚刚查到的数据字段进行循环,然后拼接成字段名
=> 字段自增->字段类型(字段大小)->是否为空->字段默认值->字段注释
(Migrations2.0.8版本才支持注释2.0.8版本以下不支持字段注释)。
好,上面的代码我能满足百分之80以上的字段,除了一些个别特殊的字段,什么是特殊的字段呢?例如,在mysql类型中是:smallint
但我在Migrations中必须是 smallinteger
包括 bigint
也要改为 biginteger
,目前我就发现这两个不一样,其他的暂时还没遇到。
然后我们开始输出主键字段(并不是自增的哦~自增的如果存在就已经在上面输出了,这里的代码只处理主键字段)我们先判断数组是否存在且数组个数大于0,这里不能使用foreach
来循环主键数组,因为$this->addPrimaryKey('name', 'tableName', 'columns')
方法只能存在一个,所以我们使用PHP的 implode()
方法进行拆分数组。
主键的解决了,还差一个新增索引的,新增索引方法为 $this->createIndex('name', 'tableName', 'Column_name')
,这个方法允许存在多个,那么我们就先判断数组是否存在且个数是否大于0,然后再使用 foreach
方法,Key_name
是新增索引时的名字,table
就是你新增索引到哪个表,Column_name
就是字段名。
以上步骤都完成以后,我们就开始新建console命令啦~
作者创建的控制器是:TimerController.php,如果你们有控制器可以直接使用,再新建一个Model文件,并且将引入Model关键词
代码:
<?php
namespace console\controllers;
use Yii;
use yii\console\Controller;
use console\models\MigrationDb;
/**
* 定时任务
* @author mo
*
*/
class TimerController extends Controller
{
public function actionMigrationdb()
{
$Migrate = new MigrationDb();
// 获取迁移目录路径 console/migrations/
$dirName = Yii::getAlias('@console').'/migrations';
// 先删除该路径下已生成的所有文件
$Migrate->deleteFile($dirName);
// 获取所有表名 开始循环获取表字段信息,创建迁移
$db = Yii::$app->getDb();
$tablesName = $db->getSchema()->getTableNames();
foreach($tablesName as $key => $value)
{
$tablesInfo = $db->getSchema()->getTableSchema($value);
exec("yii migrate/create create_".$value, $info);
}
}
}
我们先实例化模型文件,然后获取到存放迁移文件的路径,先将迁移路径下的所有迁移文件删除掉(避免重复),然后我们就使用:Yii::$app->getDb()->getSchema()->getTableNames()
获取所有的表名,接着就 foreach
循环所有的表,key
为键值 value
为表名,然后我们使用php的 exec
函数执行命令,这命令的意思是,创建迁移文件,文件名是以:create_表名
形式拼接好的,$info
可以输出打印调试结果,执行成功将会返回 New migration created successfully.
。
好了我们最后开始写Model文件了
代码:
<?php
namespace console\models;
use yii\base\Model;
class MigrationDb extends Model
{
/**
* 删除该目录下的所有文件及文件夹
* @dirName 路径名
*/
public static function deleteFile($dirName)
{
if($handle = opendir($dirName))
{
while(false != ($item = readdir($handle)))
{
if($item != '.' && $item != '..')
{
if(is_dir($dirName."/".$item))
{
self::deleteFile($dirName."/".$item);
}else
{
unlink($dirName.'/'.$item);
}
}
}
closedir($handle);
}
}
}
这里就是找到指定目录将其目录下的所有文件及文件夹删除掉(如果不满足你们需求可以进行更改)。
到了最后紧张又刺激的时刻了,我们的工作已经完成,就差运行命令调试。
我们先将所有表备份一份并导出到本地(以防万一,我不舍得你们跑路啊),确保所有表都在的时候,我们就是用命令执行console任务。
(先进入到你的程序根目录,有yii.bat的那里)
windows的DOC命令:/你的文件夹路径/yii timer(控制器名)/migrationdb(方法名)。
Linux命令:老子不会。
这个时候:console/migrations/ 目录下会创建迁移文件,成功创建完迁移文件之后,我们将所有表删除掉(删除之前记得备份!备份!!备份!!!),然后我们打开命令执行:yii migrate,这个时候有多少个迁移文件会告诉你,还会问你是否执行,我们输入y
确定执行,这个时候就开始往数据库导入表了,如有报错可发截图并询问我或者百度。
如果报表已存在的错误的话,那么就是你没有将表删完,Migrations创建迁移 跟 其他操作的时候,会自动新增一张为 migrtions的表,这张表是记录的。
JasonMiki 常州
最后登录:2019-12-26
在线时长:11小时0分
- 粉丝0
- 金钱290
- 威望10
- 积分500
共 5 条评论
很好很强大
写的很详细, 谢谢分享
抱歉抱歉各位,排版已改的稍微好看点了。
建议单独出来 直接 做成一个新命令
嗯?我这个不就是新建了一个命令做完全部事情嘛,前面的操作流程只是修改Migrations的一些文件。
yii migrate/backup all