JasonMiki 2017-10-21 10:36:10 402次浏览 4条评论 1 1 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的表,这张表是记录的。

您需要登录后才可以评论。登录 | 立即注册