数据库 - 记一次 MySQL 到 SQL Server 的项目迁移
记一次 MySQL 到 SQL Server 的项目迁移
在最近的工作中,遇到了一个数据库迁移的需求,即把某个系统的数据库从 MySQL 迁移到 SQL Server 。特此记录。
准备#
工具下载#
打开下面的网址,下载 SQL Server 官方的迁移工具:
https://www.microsoft.com/en-us/download/details.aspx?id=54257
第一个是64位版的,x86是32位版的。我用到的是第一个。
若缺失Microsoft.NetFramework 4.5.2可在该地址下载:
https://www.microsoft.com/en-us/download/confirmation.aspx?id=42642
安装#
双击刚下载好的安装包进行安装,如果没有安装 MySql ODBC driver,就点击页面上的 download 下载 ODBC安装包。
退出SSMA安装程序,先安装 My SQL ODBC driver。安装成功之后再打开 SSMA 安装程序进行安装。
使用 SSMA 迁移数据库#
开始迁移#
打开 SSMA
新建工程
选择你要迁移的目标数据库服务器
连接 My SQL ,选择需要同步的数据库
选择要迁移的数据库,右键单击数据库名称,选择 Create Report :
在我待迁移的 My SQL 生成的 Report 中,报了一个 Error 和两个 Warning:
这也直接导致了迁移预计时间高达154小时,这就有点难以接受了,需要把这三个问题解决掉。
错误处理#
其实第一个 Error 和 Warning 中的第二个 Primary key size exceeded 是一个问题,就是 Quartz 框架在 My SQL 数据库中生成的组合主键/索引,在 SQL Server 中过大了:
M2SS0020: The size of the index key is more than 900 bytes
M2SS0019: The size of the primary key is more than 900 bytes
我这边根据项目实际情况考虑是, Quartz 相关表就不迁移了,原因下面会讲到。
而第二个 Warning 则是 My SQL 版本引起的:
M2SS0183: The following SQL clause was ignored during conversion: COLLATE utf8mb4_0900_ai_ci.
utf8mb4_0900_ai_ci 在 My SQL 8以下是不被支持的,导出的数据库需要修改 utf8mb4_0900_ai_ci 为 utf8mb4_unicode_ci 或者 utf8mb4_general_ci。这里理论上可以通过修改DDL文件,把collate 的属性值从 utf8mb4_0900_ai_ci 修改为utf8_general_ci,DEFAULT CHARSET 的属性值从 utf8mb4 修改为 utf8。但我改了几次没好用,就直接用 My SQL Workbench 手动修改了。很难受。
继续迁移#
连接到 SQL Server
转换模式(即表) 在 My SQL 的元数据资源管理器中 ,右键单击要迁移的数据库,然后选择 " Convert Schema ":
将转换的表同步到SQL Server:
从 My SQL 迁移数据:
备注:
直接迁移会出现下面的崩溃
解决办法:
设置之后,迁移数据就可以操作成功了。
迁移中遇到的方案变更#
在初步设想中,本想把一整套 nacos 注册的微服务迁移到 SQL Server 中去。但在数据库迁移的过程中,发现了两个问题。
Nacos 只支持 My SQL#
此段摘自:https://www.cnblogs.com/xixisix/p/15821706.html
Nacos 的数据源获取都是通过 com.alibaba.nacos.config.server.service.datasource.DynamicDataSource 来获取的,在获取数据源时,根据配置判断你到底是使用内置的本地数据库还是外部的数据库(My SQL)
public synchronized DataSourceService getDataSource() { try { // Embedded storage is used by default in stand-alone mode // In cluster mode, external databases are used by default // 根据System.getProperty("nacos.standalone")来判断你到底是不是standalone模式 // standalone模式,使用内置数据库 if (PropertyUtil.isEmbeddedStorage()) { if (localDataSourceService == null) { localDataSourceService = new LocalDataSourceServiceImpl(); localDataSourceService.init(); } return localDataSourceService; } else { // 如果不是standalone,直接创建外部的数据源 if (basicDataSourceService == null) { basicDataSourceService = new ExternalDataSourceServiceImpl(); basicDataSourceService.init(); } return basicDataSourceService; } } catch (Exception e) { throw new RuntimeException(e); } }
外部数据源com.alibaba.nacos.config.server.service.datasource.ExternalDataSourceServiceImpl.init()
@Override public void init() { queryTimeout = ConvertUtils.toInt(System.getProperty("QUERYTIMEOUT"), 3); jt = new JdbcTemplate(); // Set the maximum number of records to prevent memory expansion jt.setMaxRows(50000); jt.setQueryTimeout(queryTimeout); testMasterJT = new JdbcTemplate(); testMasterJT.setQueryTimeout(queryTimeout); testMasterWritableJT = new JdbcTemplate(); // Prevent the login interface from being too long because the main library is not available testMasterWritableJT.setQueryTimeout(1); // Database health check testJtList = new ArrayList<JdbcTemplate>(); isHealthList = new ArrayList<Boolean>(); tm = new DataSourceTransactionManager(); tjt = new TransactionTemplate(tm); // Transaction timeout needs to be distinguished from ordinary operations. tjt.setTimeout(TRANSACTION_QUERY_TIMEOUT); // 判断到底是是不是用外部数据库 // 这个可以在com.alibaba.nacos.config.server.utils.PropertyUtil#loadSetting中看到 // setUseExternalDB("mysql".equalsIgnoreCase(getString("spring.datasource.platform", ""))); // 好家伙,直接判断配置的是不是mysql,是mysql那就是外部数据库,进行reload,不是,那就不管了 if (PropertyUtil.isUseExternalDB()) { try { reload(); } catch (IOException e) { e.printStackTrace(); throw new RuntimeException(DB_LOAD_ERROR_MSG); } if (this.dataSourceList.size() > DB_MASTER_SELECT_THRESHOLD) { ConfigExecutor.scheduleConfigTask(new SelectMasterTask(), 10, 10, TimeUnit.SECONDS); } ConfigExecutor.scheduleConfigTask(new CheckDbHealthTask(), 10, 10, TimeUnit.SECONDS); } }
在com.alibaba.nacos.config.server.service.datasource.ExternalDataSourceServiceImpl#reload中,我们可以看到
@Override public synchronized void reload() throws IOException { try { // 根据配置文件,构建数据源集合 dataSourceList = new ExternalDataSourceProperties() .build(EnvUtil.getEnvironment(), (dataSource) -> { JdbcTemplate jdbcTemplate = new JdbcTemplate(); jdbcTemplate.setQueryTimeout(queryTimeout); jdbcTemplate.setDataSource(dataSource); testJtList.add(jdbcTemplate); isHealthList.add(Boolean.TRUE); }); new SelectMasterTask().run(); new CheckDbHealthTask().run(); } catch (RuntimeException e) { FATAL_LOG.error(DB_LOAD_ERROR_MSG, e); throw new IOException(e); } }
在com.alibaba.nacos.config.server.service.datasource.ExternalDataSourceProperties#build中
List<HikariDataSource> build(Environment environment, Callback<HikariDataSource> callback) { List<HikariDataSource> dataSources = new ArrayList<>(); // 把胚子信息绑定到当前的ExternalDataSourceProperties对象,赋值操作 // 因为外面是直接new出来的,需要对属性根据文件进行赋值 Binder.get(environment).bind("db", Bindable.ofInstance(this)); Preconditions.checkArgument(Objects.nonNull(num), "db.num is null"); Preconditions.checkArgument(CollectionUtils.isNotEmpty(user), "db.user or db.user.[index] is null"); Preconditions.checkArgument(CollectionUtils.isNotEmpty(password), "db.password or db.password.[index] is null"); // 可以配置多个数据库 for (int index = 0; index < num; index++) { int currentSize = index + 1; Preconditions.checkArgument(url.size() >= currentSize, "db.url.%s is null", index); // 拿到spring.datasource.xxx一堆,这个针对所有的数据源都适用 DataSourcePoolProperties poolProperties = DataSourcePoolProperties.build(environment); // 为每一个数据源进行单独的url,user,password进行替换 poolProperties.setDriverClassName(JDBC_DRIVER_NAME); poolProperties.setJdbcUrl(url.get(index).trim()); poolProperties.setUsername(getOrDefault(user, index, user.get(0)).trim()); poolProperties.setPassword(getOrDefault(password, index, password.get(0)).trim()); HikariDataSource ds = poolProperties.getDataSource(); ds.setConnectionTestQuery(TEST_QUERY); dataSources.add(ds); callback.accept(ds); } Preconditions.checkArgument(CollectionUtils.isNotEmpty(dataSources), "no datasource available"); return dataSources; }
这个整体还行,但是为啥JDBC_DRIVER_NAME是硬编码呢,代码中清晰看到
private static final String JDBC_DRIVER_NAME = "com.mysql.cj.jdbc.Driver";
到这已经一目了然,代码中硬编码了mysql,driver也没法改,所以根本没法更换数据库驱动,有点骚,而且com.mysql.cj.jdbc.Driver是mysql8的驱动,对mysql版本是有要求的
再看其他部分,也可以发现大量的硬编码,例如 com.alibaba.nacos.config.server.auth.ExternalUserPersistServiceImpl 中,几乎所有的sql都是硬编码。所以要改造成其他数据库工作量还是非常大的
Quartz 框架表结构迁移#
- 由于使用了nacos作为注册中心,所以服务器中必定存在My SQL
- Quartz 框架与业务弱相关,可以看成另一个独立于整体业务框架的第三方服务
- 理论上 Quartz 可以和 SQL Server 使用,但涉及到表迁移的话,可能存在不兼容风险,且解决起来需要时间,可能会延长项目周期
决定不迁移 Quartz。
Spring Boot 连接 SQL Server#
POM文件中引入 mssql 驱动依赖#
<dependency> <groupId>com.microsoft.sqlserver</groupId> <artifactId>mssql-jdbc</artifactId> <version>7.4.1.jre8</version> </dependency>
配置yml文件#
spring: datasource: druid: stat-view-servlet: enabled: true loginUsername: admin loginPassword: 123456 dynamic: druid: initial-size: 5 min-idle: 5 maxActive: 20 maxWait: 60000 timeBetweenEvictionRunsMillis: 60000 minEvictableIdleTimeMillis: 300000 # mysql 用这个 # validationQuery: SELECT 1 FROM DUAL
# sql server 用 mysql 版本的参数的话,会报错:com.microsoft.sqlserver.jdbc.SQLServerException: 对象名 ‘DUAL‘ 无效。 # sql server 用这个 validationQuery: SELECT 1 testWhileIdle: true testOnBorrow: false testOnReturn: false poolPreparedStatements: true maxPoolPreparedStatementPerConnectionSize: 20 filters: stat,slf4j connectionProperties: druid.stat.mergeSql\=true;druid.stat.slowSqlMillis\=5000 datasource: master: # sql server driver-class-name: com.microsoft.sqlserver.jdbc.SQLServerDriver url: jdbc:sqlserver://localhost:1433;SelectMethod=Cursor;DatabaseName=数据库名; username: 用户名 password: 密码
SQL语句后续兼容问题#
mybatis 在saveBatch的时候 报错 The statement must be executed before any results can be obtained#
或
mybatis plus 报错com.microsoft.sqlserver.jdbc.SQLServerException: 必须执行该语句才能获得结果#
原因:
从 MyBatis3.3.1版本开始,MyBatis开始支持批量新增回写主键值的功能,这个功能首先要求数据库主键值为自增类型,同时还要求该数据库提供的 JDBC 驱动可以支持返回批量插入的主键值(JDBC提供了接口,但并不是所有数据库都完美实现了该接口),因此到目前为止,可以完美支持该功能的仅有MySQL数据库。由于SQL Server数据库官方提供的 JDBC 只能返回最后一个插入数据的主键值,所以不能支持该功能。
解决:
1.数据表主键使用idworker:idworker自动生成主键,能够会填主键,而自增不行。
2.自定义批量新增语句
// 设置useGeneratedKeys为true,返回数据库自动生成的记录主键id <insert id="batchInsert" useGeneratedKeys="true" keyProperty="id"> insert into xxx (x,y) VALUES <foreach collection="list" item="item" index="index" separator=","> ( #{item.x}, #{item.y} ) </foreach> </insert>
在SQL Server中使用 FOR UPDATE 报错:Failure occured during job recovery. [See nested exception: org.quartz.impl.jdbcjobstore.LockException: Failure obtaining db row lock: 第 1 行: 只有 DECLARE CURSOR 才允许使用 FOR UPDATE 子句。 [See nested exception: com.microsoft.sqlserver.jdbc.SQLServerException: 第 1 行: 只有 DECLARE CURSOR 才允许使用 FOR UPDATE 子句。]]#
mssql 的链接参数
url: jdbc:jtds:sqlserver://ip:1433;useCursors=true;DatabaseName=db
把 "SelectMethod=Cursor" 改成 "useCursors=true" 即可
SQL Server 报错:操作数类型冲突: varbinary 与 float 不兼容#
当 insert into table (id, xx……) values(……),如果插入值有 null 值并且 table 表中字段类型是 float,则报 操作数类型冲突: varbinary 与 float 不兼容。
解决方案: 不要在 float 类型的字段插入 null 就好了。(但这个在 MySQL 数据库没有影响。)
SQL Server 没有 double 类型#
My SQL 有 double 类型,但是 SQL Server 没有。
- 如果需要用双精度数据,且不固定小数位,用 float 就可以了
- 如果是固定小数位,可以用 numric
- 如果整数和小数都出现,可以用 real
SQL Server 中 FIND_IN_SET 函数不可用#
用
charindex(cast(匹配列 as varchar(50)), 被匹配列(多个用,分开的值)) <![CDATA[ > ]]> 0
替换 MySQL 中的 FIND_IN_SET
date_format() 函数在 SQL Server 中不可用#
用
FORMAT(time,'yyyy-MM-dd')
代替
date_format(time, '%y%m%d')
%y%m%d 在 SQL Server 里解析出来不是 220101 且月份不对,m得换成大写或者 yyyy-MM-dd
SQL Server 报错:选择列表中的列 'table.column' 无效,因为该列没有包含在聚合函数或 GROUP BY 子句中#
SQL Server 的 group by 必须包含返回的所有字段。
ifnull 函数在 SQL Server 中不可用#
用 SQL Server 中的
ISNULL(字段, '')
代替
limit 关键字在 SQL Server 中不可用#
limit x 关键字在 SQL Server 中不可用,要在 select 关键字后面加 top x
sysdate() 函数在 SQL Server 中不可用#
My SQL 中的 sysdate() 在 SQL Server 中要换成 Sysdatetime()
SQL Server 不支持 replace into 关键字#
目前没有找到替代方案,所以在项目中把相关逻辑迁移到了 service 层
group_concat 函数在 sql server 中不可用#
目前没找到替代方案,所以在项目中把 group_concat 的逻辑迁移到了 service 层
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!