若依这套脚手架在国内中小型管理系统里出镜率极高,默认那套 MySQL 跑得好好的,一旦甲方机房摆的是一台 SQL Server,事情就来了。我前后经手过三个把若依从 MySQL 迁到 SQL Server 的项目,第一个项目踩了整整两周的坑,后面两个就顺多了。这篇就把这套改造的完整链路摊开讲清楚:从依赖替换、驱动配置、建表脚本改写,到分页方言、主键策略、批量插入、日期函数这些真正会卡住你的地方,一个不落。适合手上正拿着若依源码、面对 SQL Server 数据库不知道从哪下手的朋友,也适合想提前摸清两种数据库差异、避免上线前夜返工的同学。全文按我实际改造的顺序走,每一步都说明为什么这么改,能直接照着抄。
1. 先想清楚:为什么若依换库不是改个连接串那么简单
很多人第一反应是:把application.yml里的driver-class-name和url一换,重启,完事。真这么干,大概率启动能过,一登录就报错。原因在于若依虽然是基于 MyBatis-Plus 的通用脚手架,但它在很多地方默认绑定了 MySQL 的行为习惯,这些习惯散落在依赖、SQL 脚本、分页插件、代码生成器模板里,不是一处配置能覆盖的。
1.1 若依对 MySQL 的隐性依赖藏在哪几个层面
我把这些年遇到的依赖点归了归类,大致是四层:
- 驱动与连接层:
pom.xml里默认引的是 MySQL 的 JDBC 驱动,连接串参数也是 MySQL 方言(比如useUnicode、serverTimezone)。 - 建表脚本层:若依自带的
sql目录下是 MySQL 语法,AUTO_INCREMENT、ENGINE=InnoDB、COMMENT内联注释、datetime默认值写法,SQL Server 全都不认。 - 框架配置层:MyBatis-Plus 的分页插件如果不显式指定数据库类型,会按连接推断,某些版本推断不准就分页错乱;主键生成策略默认可能是自增,而 SQL Server 的自增列叫
IDENTITY。 - 业务代码层:代码里散落的
DATE_FORMAT、IFNULL、LIMIT、GROUP_CONCAT这类 MySQL 函数,以及大小写敏感、字符串拼接concat的行为差异。
这四层里,前两层是"必须改",第三层是"不改会出诡异 bug",第四层是"跑起来才知道"。所以正确的改造顺序应该是:先动依赖和脚本把库建起来,再调框架配置让基础 CRUD 正常,最后靠实际点页面把业务 SQL 里的方言问题一个个揪出来。反过来做,你会在一堆报错里迷失。
1.2 改造前必须确认的三件事
动手之前,有三件事先确认清楚,能省掉后面大量返工:
- SQL Server 版本。2012 和 2019 差别不小,尤其是分页
OFFSET FETCH需要 2012 及以上,STRING_AGG需要 2017 及以上。如果甲方给的是 2008,那分页得换ROW_NUMBER()写法,字符串聚合得用FOR XML PATH。版本决定了你后面能用哪些语法。 - 排序规则(Collation)。SQL Server 默认排序规则常见是
Chinese_PRC_CI_AS,CI表示大小写不敏感,这点和 MySQL 在 Linux 下默认大小写敏感的表名行为不同。如果你的表名、字段名在代码里大小写混用,迁过去可能反而不报错,但一旦换到大小写敏感的排序规则就会炸。建议建库时就明确排序规则,别依赖默认。 - 是否保留若依的多数据源能力。若依有
@DataSource多数据源注解,如果项目里用了,改造时每个数据源都要单独配 SQL Server 驱动,别只改主库。
提示:改造前先把原始 MySQL 版本的代码打一个 tag 或拉一个分支,SQL Server 改造过程中会大量改脚本和配置,留好回退点,出问题能快速对比。
2. 依赖与连接配置:把 MySQL 的痕迹彻底换掉
这一层是最机械但也最容易漏的。漏一个,启动就报ClassNotFoundException或者驱动版本不匹配。
2.1 pom.xml 里的驱动替换
若依父工程和ruoyi-framework、ruoyi-system等子模块里,MySQL 驱动通常声明在父pom.xml的dependencyManagement或直接在各模块。找到类似这样的依赖:
<dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency>替换为 SQL Server 驱动。这里有个选型问题:用mssql-jdbc还是老的sqljdbc4。我的建议是一律用微软官方的mssql-jdbc,版本跟 JDK 对齐——JDK 8 用 8.x 或 9.x 的mssql-jdbc,JDK 11 及以上用 10.x 以上。老的sqljdbc4是 2012 年前后的产物,对 TLS、新版本 SQL Server 支持差,别用。
<dependency> <groupId>com.microsoft.sqlserver</groupId> <artifactId>mssql-jdbc</artifactId> <version>9.4.1.jre8</version> </dependency>版本号里的jre8后缀别漏,微软的驱动按 JDK 版本分了jre8和jre11两个包,选错了运行时报UnsupportedClassVersionError。
2.2 连接串参数的门道
MySQL 的连接串参数在 SQL Server 上大部分不适用,得换成 SQL Server 的写法。一个能用的基础连接串长这样:
spring: datasource: druid: driver-class-name: com.microsoft.sqlserver.jdbc.SQLServerDriver url: jdbc:sqlserver://127.0.0.1:1433;databaseName=ry_sqlserver;encrypt=false;trustServerCertificate=true username: sa password: your_password几个参数值得单独说:
databaseName是 SQL Server 指定库的方式,不是像 MySQL 那样写在路径里。encrypt=false和trustServerCertificate=true:新版驱动默认encrypt=true,连本地或内网没配证书的实例会直接握手失败。开发环境关掉加密最省事,生产环境如果要求加密,就得正经配证书,别图省事一直关着。- 如果实例不是默认端口,
1433换成实际端口;命名实例则用instanceName=XXX而不是端口。
Druid 的校验 SQL 也要改。若依默认配的是SELECT 1,这个在 SQL Server 上其实也能跑,但更稳妥的是SELECT 1保持不动即可,不用改成SELECT 1 FROM dual(那是 Oracle 的写法,SQL Server 没有dual表,改了反而错)。这一点很多人搞混,记住 SQL Server 支持无 FROM 的SELECT 1。
2.3 别忘了代码生成器和多数据源
若依的代码生成器模块ruoyi-generator里,读取表结构的 SQL 是写死的 MySQL 查询information_schema的方式。SQL Server 也有information_schema,但字段和函数不完全一样,比如取表注释、列注释的方式不同。如果项目要用代码生成器,这块得单独改,否则生成器页面一打开就报错。多数据源场景下,DataSourceType枚举和DruidConfig里每个数据源都要补 SQL Server 的驱动类名,别只改主库。
3. 建表脚本改写:SQL Server 不认的那些 MySQL 语法
这是改造里工作量最大、也最考验耐心的一块。若依自带的ry_20xxx.sql和quartz.sql加起来几十张表,逐条手改不现实,得先摸清差异规律,再批量处理加人工核对。
3.1 高频语法差异对照
我把改造中反复遇到的差异整理成表,照着替换能覆盖八成以上:
| MySQL 写法 | SQL Server 对应写法 | 说明 |
|---|---|---|
AUTO_INCREMENT | IDENTITY(1,1) | 自增列,位置在类型后 |
ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 | 删除 | SQL Server 无此概念 |
COMMENT 'xxx'(列内联) | EXEC sp_addextendedproperty ... | 注释要单独语句加 |
datetime DEFAULT CURRENT_TIMESTAMP | datetime DEFAULT GETDATE() | 默认时间函数不同 |
tinyint(1) | tinyint或bit | 布尔语义建议用bit |
text/longtext | nvarchar(max) | 大文本 |
varchar(n) | nvarchar(n) | 建议统一 nvarchar 支持中文 |
反引号` | [ ]或不用 | SQL Server 用方括号 |
LIMIT 0,10 | OFFSET 0 ROWS FETCH NEXT 10 ROWS ONLY | 分页语法 |
IFNULL(a,b) | ISNULL(a,b) | 空值处理 |
DATE_FORMAT(d,'%Y-%m-%d') | CONVERT(varchar(10), d, 23) | 日期格式化 |
GROUP_CONCAT(x) | STRING_AGG(x, ',') | 需 2017+ |
NOW() | GETDATE() | 当前时间 |
ON DUPLICATE KEY UPDATE | MERGE语句 | upsert 逻辑 |
这张表建议打印出来贴屏幕边上,改脚本时随时对照。
3.2 注释和默认值的处理技巧
MySQL 的列注释是内联的,SQL Server 得用扩展属性。一张表几十个字段,一个个写sp_addextendedproperty会疯。我的做法是:先不管注释,把表结构建对,注释最后用脚本批量补。批量补的思路是从原 MySQL 脚本里把COMMENT 'xxx'抽出来,用正则生成对应的sp_addextendedproperty语句。举个单条的例子:
EXEC sp_addextendedproperty @name = N'MS_Description', @value = N'用户ID', @level0type = N'SCHEMA', @level0name = N'dbo', @level1type = N'TABLE', @level1name = N'sys_user', @level2type = N'COLUMN', @level2name = N'user_id';字段多的时候,写个小脚本(Python 或 Shell 都行)读原 SQL、正则匹配COMMENT、拼出这批语句,比手写快一个数量级。这是改造里少数值得花时间写工具的地方。
默认值方面,CURRENT_TIMESTAMP换成GETDATE()是基本操作,但要注意 SQL Server 里datetime和datetime2精度不同,若依里大量用datetime存创建时间,保持datetime即可,别自作主张换datetime2,否则和 Java 侧Date映射时可能出现精度截断的困惑。
3.3 索引、主键与自增的坑
若依的表主键基本都是bigint自增。SQL Server 里bigint IDENTITY(1,1)没问题,但有个坑:自增列不能显式插入值,除非开启IDENTITY_INSERT。若依的初始化数据脚本里如果有显式指定主键值的INSERT(比如预置管理员、菜单数据),直接跑会报错。解决办法是在这批 INSERT 前后包一层:
SET IDENTITY_INSERT sys_user ON; INSERT INTO sys_user (user_id, ...) VALUES (1, ...); SET IDENTITY_INSERT sys_user OFF;每张有预置数据的表都要这么处理,且IDENTITY_INSERT同一时刻只能对一张表开启,不能并行。初始化脚本里表多的话,得一张张来,别想着一次性全开。
另外,MySQL 的KEY idx_xxx (col)建索引语法,SQL Server 要写成CREATE INDEX idx_xxx ON table(col),或者建表时用INDEX子句但语法不同。若依脚本里索引不少,这块要逐条转。
4. 框架层适配:分页、主键、批量操作这些"隐形雷"
库建好了、连接通了、能登录了,别急着庆祝。真正的坑往往在业务跑起来之后才暴露。这一层的问题最隐蔽,因为不报错,只是结果不对。
4.1 MyBatis-Plus 分页方言必须显式指定
若依用的是 MyBatis-Plus 的分页插件。默认情况下,分页插件会尝试根据 JDBC URL 自动识别数据库类型,但某些版本识别 SQL Server 时不够准,或者项目里配了DbType.MYSQL写死,导致分页 SQL 拼出来还是LIMIT,SQL Server 直接语法错误。正确做法是在分页插件配置里显式指定:
@Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); PaginationInnerInterceptor pagination = new PaginationInnerInterceptor(DbType.SQL_SERVER); pagination.setMaxLimit(500L); interceptor.addInnerInterceptor(pagination); return interceptor; }DbType.SQL_SERVER这个枚举值别写错,写SQLSERVER或别的拼法编译不过。指定之后,分页会自动生成OFFSET ... FETCH NEXT ...的写法。这里顺带说setMaxLimit,设个上限防止有人传个巨大的pageSize把库拖垮,500 是个经验值,按业务调。
4.2 主键生成策略的调整
若依实体类上常见@TableId(type = IdType.AUTO),依赖数据库自增。SQL Server 的IDENTITY列在 JDBC 里能正常回填自增主键,所以IdType.AUTO理论上是兼容的。但如果你项目里用了IdType.ASSIGN_ID(雪花算法)或ASSIGN_UUID,那和数据库无关,不用改。真正要注意的是代码生成器生成的实体,它可能按 MySQL 习惯生成AUTO,迁到 SQL Server 后如果某张表主键不是IDENTITY而是应用层赋值,就会插入失败。改造时把生成器模板里的主键策略检查一遍,确保和实际表结构一致。
4.3 批量插入的写法差异
若依里批量插入常见两种:MyBatis 的<foreach>拼多值INSERT,或者saveBatch。前者在 SQL Server 上有个硬限制:一条 INSERT 语句的 VALUES 行数不能无限多,虽然不像 Oracle 有 1000 行限制那么死,但行数太多会触发语句过大。更稳的做法是分批,比如每 500 条一批。saveBatch底层也是拼多值 INSERT,同样建议控制批次大小。这个参数在MybatisPlus的批量配置或业务代码里调,别用默认的"一次全塞"。
还有一个隐蔽点:SQL Server 对INSERT INTO ... VALUES (...),(...)的多值语法支持是从 2008 开始的,如果甲方是更老的版本(虽然少见),就得分单条插。确认版本后决定。
5. 业务 SQL 里的方言清理:靠点页面一个个揪
前面四层是"改配置和脚本",这一层是"改代码里的 SQL"。若依自带的业务 SQL 大部分是标准 SQL,但 XML mapper 里散落着一些 MySQL 函数,得靠实际跑功能来发现。
5.1 日期函数是重灾区
若依的日志、监控、定时任务模块里,日期格式化用得很多。DATE_FORMAT(create_time, '%Y-%m-%d')这种在 SQL Server 里没有直接对应,得用CONVERT或FORMAT。FORMAT函数(2012+)写法更接近直觉:
-- MySQL SELECT DATE_FORMAT(create_time, '%Y-%m-%d %H:%i:%s') FROM sys_logininfor; -- SQL Server SELECT FORMAT(create_time, 'yyyy-MM-dd HH:mm:ss') FROM sys_logininfor;FORMAT的缺点是性能不如CONVERT,数据量大时慎用。追求性能就用CONVERT(varchar(19), create_time, 120),120这个 style 值对应yyyy-mm-dd hh:mi:ss,记不住就查一下 style 对照表。我的经验是:列表展示用CONVERT,格式特殊、量小的场景用FORMAT。
5.2 字符串聚合与空值处理
若依的部门树、角色权限相关查询里可能用到GROUP_CONCAT把多个值拼成一个字符串。SQL Server 2017 之前没有STRING_AGG,得用FOR XML PATH的土办法:
-- 2017+ SELECT STRING_AGG(role_name, ',') FROM sys_role; -- 2017 以前 SELECT STUFF(( SELECT ',' + role_name FROM sys_role FOR XML PATH('') ), 1, 1, '');STUFF那个写法第一次看很别扭,本质是把结果拼成一串 XML 文本再去掉第一个逗号。改造时如果版本够,优先STRING_AGG,可读性好太多。
空值处理IFNULL换ISNULL是机械替换,但要注意ISNULL的返回类型由第一个参数决定,如果第一个参数是varchar而第二个是nvarchar,可能有隐式转换问题,必要时显式CAST。
5.3 大小写与排序规则的连带影响
MySQL 在 Linux 下表名默认大小写敏感,SQL Server 默认排序规则通常大小写不敏感。这意味着原来在 MySQL 上因为大小写写错而报错的 SQL,迁到 SQL Server 上可能"意外能跑",但这是假象——一旦哪天换了排序规则或者上了大小写敏感的库,立刻炸。改造时不要因为"能跑"就放过,把 mapper 里的表名、字段名大小写统一成和建表脚本一致,这是给自己留后路。
6. 实测排查链路:一个分页错乱的完整定位过程
光讲理论不够,讲一个我实际遇到的、比较典型的排查过程,你能看到思路是怎么走的。
6.1 现象:列表页数据重复且总数不对
改造到一半,用户管理列表页出现怪现象:第一页显示 10 条,但其中几条重复,翻到第二页又出现第一页的数据,总数显示也不对。这种"数据重复 + 总数错"的组合,八成是分页 SQL 有问题。
6.2 定位:从日志里把真实 SQL 捞出来
第一步不是猜,是开 MyBatis 的 SQL 日志,把实际执行的 SQL 打出来。若依里配logging.level.com.ruoyi: debug或者配 MyBatis-Plus 的log-impl为StdOutImpl。捞出来的 SQL 一看,末尾是LIMIT 0,10——问题定位了,分页插件还在按 MySQL 方言拼 SQL。
6.3 根因与修复
根因就是 4.1 说的:分页插件没显式指定DbType.SQL_SERVER,或者项目里某处配置覆盖成了 MySQL。检查MybatisPlusConfig,发现分页拦截器构造时没传DbType,依赖自动推断,而当前驱动版本推断失败,回退到了默认的 MySQL 方言。改成显式new PaginationInnerInterceptor(DbType.SQL_SERVER),重启,分页正常。
这个案例的价值在于:遇到分页问题,先看真实 SQL,别在 Java 代码里瞎找。SQL 日志是最直接的证据。
6.4 举一反三:还有哪些"不报错但结果错"的坑
顺着这个思路,我整理了几个同类隐患,改造时主动去验证:
- 排序不稳定:MySQL 分页不带
ORDER BY时结果顺序不保证,SQL Server 也一样,但两者"不保证"的具体表现不同。分页查询务必带稳定排序(比如按主键),否则翻页数据错乱。 COUNT与分页总数:某些复杂查询里COUNT子查询和主查询条件不一致,MySQL 上碰巧对,SQL Server 上暴露。改造后重点核对带JOIN的列表总数。TOP与LIMIT混用:如果代码里手写了LIMIT,SQL Server 要换TOP n或OFFSET FETCH,别漏。
7. 收尾阶段:数据迁移与回归验证清单
代码改完、功能能点,最后一步是把存量数据从 MySQL 搬到 SQL Server,并做一轮系统验证。这一步做扎实,上线才稳。
7.1 数据迁移的两种路子
- 工具迁移:用支持异构库的数据迁移工具,配置源和目标,做字段映射。优点是快,缺点是类型映射和自增列处理要人工核对,尤其是
datetime精度、text到nvarchar(max)的转换。 - 脚本导出导入:从 MySQL 导出 CSV 或 SQL,清洗后导入 SQL Server。适合数据量不大、需要精细控制的场景。导入前记得处理
IDENTITY_INSERT,导入后重置自增种子(DBCC CHECKIDENT('table', RESEED, 0)),否则新插入的数据主键会从旧的最大值继续,可能冲突。
迁移后一定要抽查:中文有没有乱码(排序规则和nvarchar是否到位)、时间字段有没有偏移(时区处理)、大文本有没有截断。
7.2 回归验证的重点清单
功能点太多不可能全测,按风险排序,优先验证这些:
| 验证项 | 关注点 |
|---|---|
| 登录与权限 | 用户、角色、菜单关联查询是否正常 |
| 列表分页 | 每页条数、总数、翻页数据一致性 |
| 新增/编辑 | 自增主键回填、默认值、非空约束 |
| 批量操作 | 批量删除、批量导入的批次大小 |
| 定时任务 | Quartz 表在 SQL Server 下的兼容性 |
| 日志与监控 | 日期格式化、统计查询 |
| 代码生成器 | 若使用,表结构读取是否正常 |
Quartz 这块单独提一句:若依集成了 Quartz,quartz.sql建表脚本也是 MySQL 语法,得一起改。Quartz 官方其实提供了各数据库的建表脚本,直接找 SQL Server 版本替换,比自己改省事。
7.3 几个我踩过、值得你避开的细节
nvarchar长度别照抄varchar:MySQL 的varchar(255)迁到 SQL Server 如果写成nvarchar(255),因为nvarchar每字符占 2 字节,索引键长度限制更容易触顶。长字段评估一下,该用nvarchar(max)就用。- 默认 schema:SQL Server 默认 schema 是
dbo,若依脚本建表没指定 schema 时落在dbo下,连接用户如果有自定义默认 schema,可能找不到表。连接串或用户配置里确认默认 schema 是dbo。 - 事务与锁:SQL Server 默认隔离级别是
READ COMMITTED,和 MySQL 的REPEATABLE READ不同,某些依赖可重复读的业务逻辑迁过来行为会变。若依自带业务一般不受影响,但如果有自定义的复杂事务,得复核。
改造这件事,说到底不是"把 MySQL 换成 SQL Server"这么一句,而是把若依里所有默认绑定 MySQL 行为的地方,一个个换成 SQL Server 的等价物,再用实际功能验证。我个人的体会是:配置和脚本的改动是体力活,框架层和业务 SQL 的适配才是脑力活,前者靠对照表批量处理,后者靠日志和点页面逐个揪。把第 4 章和第 5 章的内容当成检查清单,改造完对着过一遍,能挡掉绝大多数上线后才会暴露的问题。