ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Ruoyi MySQL迁移PostgreSQL:配置、SQL与数据全流程避坑

Ruoyi MySQL迁移PostgreSQL:配置、SQL与数据全流程避坑 Ruoyi 这套权限管理框架从发布第一天起就是抱着 MySQL 长大的。官方sql目录里的建表脚本、代码生成器的模板、Quartz 定时任务的建表 SQL一股脑全是 MySQL 语法。等到项目因为信创要求、运维成本或者单纯想用 PostgreSQL 的 JSONB、全文检索、向量扩展这些能力要把底层库换掉很多人才发现这根本不是改个连接串就完事的事。我自己在两个项目里完整做过 Ruoyi 的 MySQL 到 PostgreSQL 迁移第一个项目花了三天踩坑第二个项目半天收工中间差的就是对坑点的预判。下面把这些坑按层次拆开讲从依赖配置、SQL 语法、建表脚本、数据迁移到回归验证全程给出可直接抄的改法和参数适合正在做或准备做这次库切换的后端同学。1. 先把 Ruoyi 的数据库耦合点摸清楚1.1 Ruoyi 默认 MySQL 不是随便选的Ruoyi 选择 MySQL 作为默认数据库背后是很实际的考量。它的定位是开箱即用的后台管理脚手架目标用户大量是国内中小团队而 MySQL 在部署门槛、周边工具、教程资料上都是最厚的。框架里很多设计是顺着 MySQL 的特性走的比如用AUTO_INCREMENT做主键、用反引号包表名字段名、用date_format做日志统计、用group_concat拼部门树。这些写法在 MySQL 里非常自然甚至是被官方文档推荐的最佳实践但它们全都带着 MySQL 的方言烙印。问题就在这里Ruoyi 不是一张白纸它是一套已经跑通、已经在生产环境验证过的系统。你切换数据库等于要把这套系统里所有“跟 MySQL 绑定的假设”逐一翻出来重新审视。假设越隐式坑就越深。所以第一步不是急着改配置而是先把耦合点列成清单做到心里有数。1.2 切换前必须盘点的四层依赖我习惯把 Ruoyi 的数据库耦合分成四层从下往上依次是驱动与连接池层pom.xml里的 JDBC 驱动、Druid 的方言检测、连接 URL 参数。框架配置层application.yml里的分页插件方言、MyBatis 的驼峰映射、多数据源配置。SQL 语法层Mapper XML 和注解里的函数、分页写法、自增主键回填、标识符引用。资产脚本层建表 SQL、Quartz 的QRTZ_*表脚本、代码生成器读取的元数据表。这四层的改动成本是递增的。第一层半小时能改完第四层往往要花上一整天。很多人栽跟头是因为只改了第一层就以为完事了结果启动时连不上、查询时报函数不存在、定时任务静默不执行最后各种疑难杂症混在一起排查效率极低。提示动手前先把当前 MySQL 版的完整 SQL 备份一份迁移过程中你会反复拿它做对照。这份 SQL 是唯一的“标准答案”别嫌它占地方。1.3 为什么建议在分支上做而不是直接改 master这看起来像废话但真的有人直接在主干上改。切换数据库是个牵一发动全身的动作中途一定会有“改了一半、跑不起来、又得回滚”的阶段。放在独立分支上你可以随时对比改动前后的差异也能在出问题时秒回退。另外如果你的项目还带着其他团队共用的模块分支能避免把别人拖下水。我第二个项目就是在一个feature/pg-migration分支上做的整个过程提交了十几个 commit每个 commit 对应一类改动驱动、配置、Mapper、建表脚本、迁移工具。这样最后 review 的时候改动是可追溯、可分块验证的。等一切稳定了再合并干净利落。2. 依赖与配置层改起来最快也最容易留下暗坑2.1 驱动、连接池与 pom.xml 的替换Ruoyi 的 JDBC 驱动依赖通常在ruoyi-framework模块的pom.xml里名字叫mysql-connector-java。替换成 PostgreSQL 驱动dependency groupIdorg.postgresql/groupId artifactIdpostgresql/artifactId version42.7.3/version /dependency版本别乱选42.x 系列对 JDK 8 到 JDK 17 都有良好支持42.7.x 是目前比较稳的一档。如果你的项目还在用 JDK 8确认一下驱动的字节码版本太新的驱动可能要求 JDK 11。实测下来 42.7.3 在 JDK 8 上跑 Ruoyi 没有问题。连接池这块Ruoyi 老版本默认用 Druid新版本部分模块切到了 HikariCP。Druid 有个需要注意的点它会根据 URL 自动识别数据库类型然后选择对应的 ValidConnectionChecker。正常情况下jdbc:postgresql://会被正确识别为 PostgreSQL不需要手动配。但如果你在配置里显式写了db-type: mysql那就得改成postgresql否则校验逻辑会走错分支。注意如果你启用过 Druid 的wall过滤器务必检查它的 SQL 白名单。wall默认按 MySQL 方言做语法校验PostgreSQL 的某些写法会被它误判拦截表现为“SQL 没毛病但就是执行不了”。2.2 application.yml 里那些 MySQL 专属参数改连接串是第一步。MySQL 的 URL 里常带serverTimezone、characterEncoding、useSSL、allowPublicKeyRetrieval这些参数它们全都是 MySQL 驱动专属的PostgreSQL 驱动不认带上虽然不一定报错但属于无效配置容易误导后来人。PostgreSQL 的连接串干净得多url: jdbc:postgresql://127.0.0.1:5432/ruoyi?currentSchemapublicstringtypeunspecified这里有两个参数值得说明。currentSchemapublic显式指定模式避免因为用户默认search_path不同导致找不到表。stringtypeunspecified是我强烈建议加上的它能让 PostgreSQL 驱动在传字符串参数时不做强制类型转换绕过大量“类型不匹配”的报错。Ruoyi 里有很多字段在 MySQL 是char或varchar到了 PG 侧比较时经常撞上类型问题加上这个参数能省掉一大半麻烦。用户名密码就换成 PG 的账号。Ruoyi 默认的root/123456是 MySQL 的习惯PG 侧通常是postgres加上你安装时设的密码。2.3 分页插件与多数据源的方言配置这是最经典的一个坑。Ruoyi 用的分页插件 PageHelper 需要在application.yml里声明方言pagehelper: helperDialect: postgresql supportMethodsArguments: true params: countcountSql默认值是mysql。如果这里不改PageHelper 会生成 MySQL 风格的LIMIT语句在 PG 上直接报语法错误。新版 PageHelper 支持自动检测方言但在多数据源场景下自动检测会失灵所以显式配置永远是最稳的选择。如果你的 Ruoyi 用了自带的多数据源DataSource注解那套配置在application-druid.yml里的master和slave那么每一个数据源的 URL 都要改一个都不能漏。我见过有人只改了master主库正常了但某个从库查询的功能一直报错查了半天才发现是slave的 URL 还是 MySQL 的。提示改完配置后先用命令行客户端连一下 PG跑一条select now();确认网络和账号没问题再启动应用。把网络层的问题和框架层的问题分开排查能省不少时间。3. SQL 语法层Mapper 里的每个函数都得过一遍3.1 标识符反引号、大小写与关键字冲突MySQL 允许用反引号包标识符比如sys_user、user_name。PostgreSQL 不认反引号它用双引号包标识符。所以你在自定义 Mapper XML 里写的每一处反引号都会变成语法错误ERROR: syntax error at or near 解决办法很简单要么把反引号去掉推荐因为 Ruoyi 的表名和字段名都是小写下划线不加引号也不会冲突要么改成双引号。但要注意PG 加双引号后标识符是大小写敏感的sys_user和sys_user在 PG 里不是一回事别自己给自己挖坑。大小写的问题还有另一面。PostgreSQL 会把未加引号的标识符统一转成小写MySQL 在 Linux 上默认表名区分大小写、Windows 上不区分。这个差异在 Ruoyi 里通常不明显因为框架的表名字段名都是小写。但如果你之前手写过驼峰表名或者用代码生成器生成了带大写的字段迁移时就会翻车。最稳妥的做法是建表脚本里全部用小写加下划线跟 Ruoyi 官方风格保持一致。关键字冲突则是另一个高频问题。PostgreSQL 保留字比 MySQL 多像user、order、group、limit、offset、end、comment这些词直接当列名或表名会报错。Ruoyi 官方表里踩雷的不多但你的业务表就说不准了。遇到关键字加双引号包起来即可。3.2 常用函数映射对照表与批量改造Ruoyi 的 Mapper 里散落着不少 MySQL 专属函数主要集中在登录日志、操作日志的统计查询上。我整理了一份对照表改造时照着换就行MySQL 写法PostgreSQL 写法说明IFNULL(a, b)COALESCE(a, b)空值兜底DATE_FORMAT(t, %Y-%m-%d)to_char(t, YYYY-MM-DD)日期格式化GROUP_CONCAT(col)string_agg(col, ,)分组拼接FIND_IN_SET(x, ids)x ANY(string_to_array(ids, ,))集合包含判断SUBSTRING_INDEX(s, ,, 1)split_part(s, ,, 1)按分隔符截取DATE_SUB(now(), INTERVAL 1 DAY)now() - interval 1 day日期减法DATE_ADD(now(), INTERVAL 1 DAY)now() interval 1 day日期加法CURDATE()CURRENT_DATE当前日期SYSDATE()now()当前时间LIMIT 0, 10LIMIT 10 OFFSET 0分页这张表是浓缩版实际操作时你会发现ifnull和date_format出现频率最高。Ruoyi 的操作日志统计里有一段按月分组的 SQL用的就是date_format改成to_char时注意格式化串的差异MySQL 用%Y-%mPG 用YYYY-MM百分号要去掉。3.3 分页、自增主键与批量插入的写法差异分页除了 PageHelper 的方言自己手写的分页 SQL 也要改。MySQL 的LIMIT a, b写法在 PG 里不支持必须写成LIMIT b OFFSET a。虽然 PageHelper 会帮你处理大部分场景但如果你在 Mapper 里手写了带limit的 SQL记得一并改掉。自增主键是另一个重灾区。MySQL 用AUTO_INCREMENTPG 用SERIAL或IDENTITY。更麻烦的是 MyBatis 的主键回填Ruoyi 的 Mapper 里大量使用useGeneratedKeystrue keyPropertyxxxId。在 MySQL 上这会把刚插入的自增 ID 塞回实体到了 PG驱动的getGeneratedKeys行为不一样PG JDBC 在开启RETURN_GENERATED_KEYS时会返回整行数据MyBatis 如果没有正确配置keyColumn可能取到错误的列。我的解决办法是给这类 insert 显式加上keyColumninsert idinsertUser useGeneratedKeystrue keyPropertyuserId keyColumnuser_id ... /insert或者在 PG 上用selectKey配合RETURNING user_id不过这样要改 XML 结构工作量更大。加keyColumn是改动最小、收益最直接的方式实测下来很稳。批量插入也要注意。MySQL 的insert into t values (...),(...),(...)写法 PG 支持但批量更新ON DUPLICATE KEY UPDATE是 MySQL 专属的PG 侧要用ON CONFLICT ... DO UPDATE。Ruoyi 核心代码里用得不多但业务扩展里常见。注意PG 的char(n)是定长类型比较时会忽略尾部空格0 0 为真MySQL 的char也有类似行为但细节不同。如果 Ruoyi 的status、del_flag用的是char(1)一般没事但千万别用char存长度不一致的编码。4. 建表脚本与 Quartz最容易被忽略的两个重灾区4.1 表结构脚本的重写要点Ruoyi 官方提供的sql/ry_xxxx.sql是纯 MySQL 语法直接拿到 PG 上跑一定失败。需要改写的地方主要有去掉ENGINEInnoDB DEFAULT CHARSETutf8mb4这类表选项。AUTO_INCREMENT改成SERIAL或GENERATED BY DEFAULT AS IDENTITY。datetime改成timestamptinyint改成smallintint改成integerdouble改成double precisiondecimal(m,n)改成numeric(m,n)。text/longtext都改成text。内联的COMMENT xxx要去掉改成独立的COMMENT ON COLUMN 表.字段 IS xxx;语句。手工改几十张表很痛苦所以我更推荐用工具生成在 Navicat 或 DataGrip 里连接 MySQL 源库用“数据传输”或“导出为 PG 脚本”功能让工具帮你做类型映射。生成之后人工抽查一遍重点看主键、索引、注释是否完整。我个人的偏好是先用工具生成 PG 建表脚本再和官方 MySQL 脚本逐表对照确保字段、索引、默认值一个不漏。这样比纯手写靠谱也比纯靠工具放心。4.2 Quartz 定时任务表与代码生成器模板Quartz 是 Ruoyi 定时任务模块的核心它需要一组QRTZ_开头的表。MySQL 和 PG 的建表脚本完全不同Quartz 官方发行包里就提供了tables_postgres.sql直接拿来跑就行。很多人迁移时忘了这块结果应用能启动、页面能打开但定时任务就是不执行也不报错查日志一片安静。这个现象在搜索里出现频率很高根因往往就在这里。除了 Quartz 表本身还要注意sys_job表的数据要跟着迁过来否则你配好的定时任务全丢了。代码生成器这边的问题更隐蔽。Ruoyi 的代码生成器读取数据库元数据来生成代码GenUtils里有一段数据库类型到 Java 类型的映射逻辑判断的是varchar、bigint、datetime这些 MySQL 类型名。到了 PG类型名变成了character varying、int8、timestamp映射逻辑匹配不上生成的实体类字段类型就会错乱。这个需要改GenUtils.getJavaType这类方法的判断分支把 PG 的类型名补进去。// 常见需要补充的 PG 类型映射 if (int8.equalsIgnoreCase(dataType) || bigint.equalsIgnoreCase(dataType)) { return Long; } if (timestamp.equalsIgnoreCase(dataType) || timestamptz.equalsIgnoreCase(dataType)) { return Date; } if (dataType ! null dataType.startsWith(numeric)) { return BigDecimal; }改完这块代码生成器才能正常工作否则你连新增业务表都成问题。提示如果你用上了搜索里常见的 KkFile、Sa-Token 签名、WebSocket 这些扩展模块先确认它们各自的建表脚本有没有 PG 版本。扩展模块往往是坑的集中地。5. 存量数据迁移与业务回归验证5.1 三条迁移路线与序列重置数据迁移有三条常见路线各有适用场景工具直连迁移Navicat、DataGrip 的数据传输功能配置好源和目标的连接就能搬。优点是快适合中小数据量缺点是字段类型映射需要人工干预。中间文件迁移MySQL 导出 CSV 或 SQL转换成 PG 能吃的格式再导入。优点是可控性强能逐表核对缺点是表多的时候繁琐。专业迁移工具pgloader 这类工具支持从 MySQL 直接读到 PG能自动处理不少类型转换。适合表结构复杂、数据量大的场景但学习成本略高。不管走哪条路序列重置这一步绝对不能省。PG 的SERIAL列底层是一个独立序列数据迁移时如果你直接把 ID 值插进去序列的当前值不会自动跟着涨。结果就是新增数据时主键冲突报duplicate key value violates unique constraint。重置语句很简单SELECT setval(sys_user_user_id_seq, (SELECT MAX(user_id) FROM sys_user));每张有自增主键的表都要执行一遍。表多了可以写个脚本批量生成这些语句别手敲。5.2 上线前必须跑的回归清单数据迁完不代表完事功能验证才是决定能不能上线的关键。下面这份清单是我两个项目里踩过坑之后总结的建议逐项过验证项重点看什么常见异常登录账号密码校验、验证码、登录日志大小写敏感导致登录失败用户/角色/菜单增删改查、关联查询类型不匹配报错字典管理字典类型与数据group_concat相关定时任务任务执行、日志输出Quartz 表未迁移代码生成读取表结构、生成代码类型映射错误操作日志/登录日志列表查询、日期筛选、清空date_format未改数据权限部门树、权限过滤 SQL递归查询语法差异文件上传附件表写入、主键回填自增回填失败其中“登录失败”和“定时任务不执行”是两个最具迷惑性的问题。前者经常被误判成密码加密问题其实是 PG 的字符串比较默认大小写敏感而 MySQL 的utf8mb4_general_ci不敏感导致模糊查询用户名的结果不一样。后者的根因前面说了多半是 Quartz 表没迁。6. 常见报错速查与实操避坑心得6.1 报错速查表迁移过程中你会遇到大量报错我把最典型的整理成表照着查能省不少搜索时间报错信息根因处理方式syntax error at or near \Mapper 里用了反引号去掉反引号或改双引号function ifnull(...) does not existMySQL 专属函数换COALESCEfunction date_format(...) does not exist同上换to_charfunction group_concat(...) does not exist同上换string_aggsyntax error at or near ,limit 处用了LIMIT a,b改LIMIT b OFFSET arelation xxx does not exist表未建或模式不对检查建表和search_pathcolumn xxx does not exist大小写或引号问题统一小写去掉多余引号duplicate key value violates unique constraint序列未重置执行setvaloperator does not exist: bigint character varying类型不匹配加stringtypeunspecified或显式转换null value in column xxx violates not-null默认值未设置补默认值或调整插入逻辑定时任务静默不执行Quartz 表未迁移跑tables_postgres.sql分页结果异常PageHelper 方言未改改helperDialect: postgresql6.2 几个我踩得最深的坑第一个坑是stringtypeunspecified。第一个项目做迁移时登录接口一直报operator does not exist排查了两个小时最后发现是字符串参数和bigint列比较时的类型推导问题。加上这个连接参数问题瞬间消失。这个坑的隐蔽之处在于它只在特定查询上出现不是所有接口都报很容易让人误判成业务代码问题。第二个坑是字符集排序。MySQL 的utf8mb4_general_ci让所有比较都忽略大小写PG 默认区分。用户的模糊搜索、用户名查重、字典 key 比对都可能因为这一个差异出现“时好时坏”的现象。解决办法是在需要忽略大小写的查询里用lower()或ILIKE或者在建库时指定citext扩展。这个坑不解决测试会一直不稳定。第三个坑是时间类型。MySQL 的datetime没有时区概念PG 的timestamp也没有但timestamptz有。Ruoyi 里默认字段用的是datetime迁到 PG 用timestamp一般没问题。但如果你的服务器时区和数据库时区不一致时间字段会比较着看不对。我建议统一用timestamp并在应用侧处理时区别混用两种类型。最后分享一个提效的小方法把改造点按文件分类用grep -rn批量搜索关键字。比如搜ifnull(、date_format(、limit、反引号一次性列出所有待改位置逐个处理。这比一个个翻 Mapper 快得多也能避免遗漏。等所有关键字都搜不出结果了基本就说明语法层改干净了。
返回列表