通常情况下,我们的系统在重构之后,表名、字段名都会发生不同程度的变化,如果说重构之后数据库结构变更很小,那么恭喜你,你省去了很多麻烦。但是往往实际情况事与愿违,所以此处仅记录本人迁移生产数据的一些思路和步骤,以及会遇到的一些问题
方案一:Server Link(推荐)
Mysql提供一种引擎叫FEDERATED,这种引擎允许我们链接其他数据库,然后通过sql命令复制其他库表到任意数据库。简单来说就类似于Navicat复制一个数据库的表到另一个数据库中去,这样相当于两个库的表都在一个库里边,导入数据就变得简单多了
1、创建远程server
在新数据库中执行
CREATE SERVER crmlink
FOREIGN DATA WRAPPER mysql
OPTIONS (USER 'ygsw', PASSWORD 'hugo2021!',HOST '192.168.30.15', PORT 3306, DATABASE 'crm');
上面的命令意思是创建一个server链接,使用的是mysql数据库,并配置了数据库链接参数
这样的话我们就变相的连接到了生产数据库
2、复制原表
对需要迁移的表先复制到新库中,以mls_base_corp(部门表)为例
CREATE TABLE `link_mls_base_corp` (
`ORIGREC` decimal(22,0) DEFAULT NULL,
`ORIGSTS` char(1) DEFAULT 'N',
`CORPID` varchar(50) NOT NULL,
`PARENTID` varchar(50) DEFAULT '0',
`CORPCODE` varchar(30) DEFAULT NULL,
`CORPDESC` varchar(120) DEFAULT NULL,
`RECORDERCODE` varchar(30) DEFAULT NULL,
`RECORDERDESC` varchar(60) DEFAULT NULL,
`RECORDTIME` datetime DEFAULT CURRENT_TIMESTAMP,
`FREEZEFLAG` varchar(3) DEFAULT '0',
`FREEZERCODE` varchar(30) DEFAULT NULL,
`FREEZERDESC` varchar(60) DEFAULT NULL,
`FREEZETIME` datetime DEFAULT NULL,
`ORDERCODE` varchar(100) DEFAULT NULL,
`CODE` varchar(400) DEFAULT NULL,
`CODEID` varchar(50) DEFAULT NULL,
`ABBREVIATION` varchar(50) DEFAULT NULL,
`ADDRESS` varchar(50) DEFAULT NULL,
`PHONE` varchar(50) DEFAULT NULL,
`FAX` varchar(50) DEFAULT NULL,
`POSTCODE` varchar(50) DEFAULT NULL,
`PARENTNAME` varchar(120) DEFAULT NULL,
`UNITTYPE` varchar(100) DEFAULT NULL COMMENT '单位类型',
`LIMSCODE` varchar(200) DEFAULT NULL,
`WEBSERVICEURL` varchar(500) DEFAULT NULL,
`PARENTDIV` varchar(50) DEFAULT NULL COMMENT '成检标记实验室所属事业部字段',
`CORPLEVEL` varchar(10) DEFAULT NULL COMMENT '客户级别',
`HIERARCHYCODE` varchar(400) DEFAULT NULL COMMENT '层级编码',
`DEFAULTCURRENCY` varchar(20) DEFAULT NULL COMMENT '默认币种',
`CREDITPOOLFROZENQUOTA` varchar(50) DEFAULT NULL COMMENT '信用池冻结金额',
`CREDITPOOLQUOTA` varchar(50) DEFAULT NULL COMMENT '信用池额度',
`USEMONY` varchar(40) DEFAULT NULL COMMENT '信用池已用额度',
`RESIDUALAMOUNT` varchar(40) DEFAULT NULL COMMENT '信用池剩余额度',
PRIMARY KEY (`CORPID`),
KEY `CORPCODE_INDEX` (`CORPCODE`)
) ENGINE = FEDERATED CONNECTION='crmlink/mls_base_corp';
注意create table的表名不要和原表冲突,前面加上link_前缀方便识别
中间至最后一行之前可以直接复制原表的建表语句
最后一行的ENGINE必须是FEDERATED,CONNECTION值引号中 / 前边是你在第一个创建的server链接名,/ 之后是原表名
执行sql命令之后就可以看到多了一张链接表
注意:
FEDERATED链接的表的DML不会对原表产生影响,但是TRUNCATE(截断)命令会对原表产生效果
3、新表的表结构
初始化新表
CREATE TABLE `sys_dept` (
`dept_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'ID',
`pid` bigint(20) DEFAULT NULL COMMENT '上级部门',
`sub_count` int(5) DEFAULT '0' COMMENT '子部门数目',
`name` varchar(255) NOT NULL COMMENT '名称',
`dept_sort` int(5) DEFAULT '999' COMMENT '排序',
`enabled` bit(1) NOT NULL COMMENT '状态',
`create_by` varchar(255) DEFAULT NULL COMMENT '创建者',
`update_by` varchar(255) DEFAULT NULL COMMENT '更新者',
`create_time` datetime DEFAULT NULL COMMENT '创建日期',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`dept_code` varchar(30) DEFAULT NULL COMMENT '部门编码',
PRIMARY KEY (`dept_id`) USING BTREE,
KEY `inx_pid` (`pid`) USING BTREE,
KEY `inx_enabled` (`enabled`) USING BTREE,
KEY `dept_code_index` (`dept_code`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=128 DEFAULT CHARSET=utf8 COMMENT='部门';
4、导入原数据
对比原表和新表的表结构,我们需要的字段变得少了很多。分析对应字段
新表字段 | 对应关系说明 | 原表字段 |
---|---|---|
dept_id | 主键自增,不需要导入 | |
pid | 上级id,由于原表主键是uuid生成,所以此字段不能一步到位,我们只需要在新表中存入原表中的唯一字段,比如dept_code,部门编码(CORPCODE)在原表中是唯一字段,为了正确关联关系,我们在新表中也必须存上,然后在下一步中进行处理 | PARENTID |
sub_count | 原表中无此字段,在下面步骤中处理 | |
name | 菜单名 | CORPDESC |
dept_sort | 排序,只不过原表此字段是varchar类型 | ORDERCODE |
enabled | 启用标识,和原表恰好相反对应 | FREEZEFLAG |
create_by | 创建者 | RECORDERCODE |
update_by | 更新人,原表没存,用创建者的值就行 | |
create_time | 创建时间 | RECORDTIME |
update_time | 修改时间,原表没存,用创建时间的值就行 | |
dept_code | 部门编码 | CORPCODE |
梳理完对应关系之后,我们就可以通过sql语句导入数据了
/*导入数据到sys_dept*/
INSERT INTO `sys_dept`
(`pid`, `sub_count`, `name`, `dept_sort`, `enabled`, `create_by`, `update_by`, `create_time`, `update_time`, `dept_code`)
select (case PARENTID when '01' then null else 0 end),0,CORPDESC,(ORDERCODE + 0),(case FREEZEFLAG when '0' then 1 when '1' then 0 end),RECORDERCODE,RECORDERCODE,RECORDTIME,RECORDTIME,CORPCODE from link_mls_base_corp
这里其实就是从我们创建的链接表link_mls_base_corp中查询数据,再经过部分处理之后插入到新表之中
5、补充处理
这一步不一定会有,在当前表中我们还需要更新pid(上级id)、sub_count(子部门数目),这些处理都需要用到存储过程+遍历进行处理
①:更新pid
-- ****************************更新pid --开始**************************
/*拿dept_code联查原corp信息,corp信息中的PARENTID并非数字,但对应sys_dept中的dept_code,所以查询dept_code对应的主键id就是需要pid*/
-- 创建存储过程之前需判断该存储过程是否已存在,若存在则删除
DROP PROCEDURE IF EXISTS sync_dept_pid;
-- 创建存储过程
CREATE PROCEDURE sync_dept_pid()
BEGIN
-- 定义变量
DECLARE s int DEFAULT 0;
DECLARE deptCode varchar(30);
DECLARE parentId bigint(20);
-- 定义游标,并将sql结果集赋值到游标中
DECLARE dept CURSOR FOR select d.dept_code,p.dept_id from sys_dept d join link_mls_base_corp c on d.dept_code = c.CORPID join sys_dept p on c.PARENTID = p.dept_code where d.pid is not null;
-- 声明当游标遍历完后将标志变量置成某个值
DECLARE CONTINUE HANDLER FOR NOT FOUND SET s=1;
-- 打开游标
open dept;
-- 将游标中的值赋值给变量,注意:变量名不要和返回的列名同名,变量顺序要和sql结果列的顺序一致
fetch dept into deptCode,parentId;
-- 当s不等于1,也就是未遍历完时,会一直循环
while s<>1 do
-- 执行业务逻辑
update sys_dept set pid = parentId where dept_code=deptCode;
-- 将游标中的值再赋值给变量,供下次循环使用
fetch dept into deptCode,parentId;
-- 当s等于1时表明遍历以完成,退出循环
end while;
-- 关闭游标
close dept;
END;
-- 执行存储过程
call sync_dept_pid();
-- 最后删除存储过程
DROP PROCEDURE IF EXISTS sync_dept_pid;
-- ****************************更新pid --结束**************************
大概逻辑是:
查询新表的部门,得到部门编码dept_code,对应原表CORPCODE,从而得到原表上级PARENTID,根据PARENTID得到上级id对应的CORPCODE,再拿上级id的CORPCODE在新表中查询对应的主键id即是需要的pid,有点绕,参考定义游标出的sql
select d.dept_code,p.dept_id from sys_dept d join link_mls_base_corp c on d.dept_code = c.CORPID join sys_dept p on c.PARENTID = p.dept_code where d.pid is not null
对结果集遍历然后批量跟新即可修改pid的值了
②:更新sub_count
同样我们使用存储过程+遍历的方式批量修改
-- ****************************更新sub_count --开始**************************
-- 创建存储过程之前需判断该存储过程是否已存在,若存在则删除
DROP PROCEDURE IF EXISTS sync_dept_sub_count;
-- 创建存储过程
CREATE PROCEDURE sync_dept_sub_count()
BEGIN
-- 定义变量
DECLARE s int DEFAULT 0;
DECLARE deptId bigint(20);
-- 定义游标,并将sql结果集赋值到游标中
DECLARE deptIdList CURSOR FOR select dept_id from sys_dept;
-- 声明当游标遍历完后将标志变量置成某个值
DECLARE CONTINUE HANDLER FOR NOT FOUND SET s=1;
-- 打开游标
open deptIdList;
-- 将游标中的值赋值给变量,注意:变量名不要和返回的列名同名,变量顺序要和sql结果列的顺序一致
fetch deptIdList into deptId;
-- 当s不等于1,也就是未遍历完时,会一直循环
while s<>1 do
-- 执行业务逻辑
update sys_dept set sub_count = (select a.count from (select count(*) as count from sys_dept where pid = deptId) a) where dept_id=deptId;
-- 将游标中的值再赋值给变量,供下次循环使用
fetch deptIdList into deptId;
-- 当s等于1时表明遍历以完成,退出循环
end while;
-- 关闭游标
close deptIdList;
END;
-- 执行存储过程
call sync_dept_sub_count();
-- 最后删除存储过程
DROP PROCEDURE IF EXISTS sync_dept_sub_count;
-- ****************************更新sub_count --结束**************************
看下结果把:
原表数据
新表数据
6、其他注意事项
使用此种方式有一个好处就是,我们一张一张的表处理完之后,最后我们汇聚成一个sql文件,到时候就可以一步到位进行数据迁移,不需要写代码去迁移数据
另外实际上先迁移什么表,后迁移什么表,应该是有逻辑顺序的,这个根据自己的业务去判断,比如我的用户表中必须存dept_id(部门id),所以应先迁移部门表数据然后迁移用户数据
如果新库的菜单表和原先菜单数据不一致,也就是说重构过程中改变了菜单结构,那么实际上我们希望是以新的菜单为准,那么用新的菜单数据,角色-菜单的绑定就需要放在迁移完所有数据之后通过管理页面或者sql手动绑定了
方案二:使用ETL工具
现在市面上也有很多ETL工具,全程数据提取转换加载工具(Extract-Transform-Load),虽然多用在大数据处理中,但根据业务需要其实也是符合我们重构数据迁移的,比如Kettle工具
关于Kettle网上有一大堆的介绍和使用,这里就不作介绍了,只提供一种思路
像这种可视化工具的好处就是带界面,字段可以手动选择对应关系,比如下图
对于像还要做进一步处理的(比如上面的更新pid和sub_count),kettle应该也是提供单独的执行sql的处理的
所以这也算是一种界面+脚本的方式吧
其他方案
现在的云服务也有提供数据传输的,比如阿里云的数据传输服务 DTS,这种适合服务器本身就是阿里托管的,并且数据量庞大的情况。当然这是收费的,很多云服务商都有提供此功能,建议根据需要选择。
写在最后:
本篇文章只是提供一些系统重构之后数据迁移的一些思路,方案有很多,不见得我的方法对于你就是可行的,也有我可能不知道的更优的方案,这里仅作记录,分享给有需要的同学。