尧图精选

RuoYi 从 MySQL 迁移 PostgreSQL:SQL 适配与避坑实战

🕒 发布时间:2026/9/18 12:07:12 📁 来源:尧图网络
1. 迁移前先想清楚为什么要动数据库把 Ruoyi 从 MySQL 迁到 PostgreSQL这件事本身不难难的是迁完之后系统还能原样跑起来。我前前后后在三套 Ruoyi 项目上做过这种切换有 RuoYi-Vue 单体版的也有 RuoYi-Cloud 拆成多模块的还碰过 RuoYi-Vue-Plus 那种默认就带 MyBatis-Plus 的版本。每一次都会在不同的地方被绊一下而且每次绊倒的位置还不一样——第一次死在自增主键第二次死在find_in_set第三次死在他妈代码生成器。RuoYi、MySQL、PostgreSQL 这三个词组合在一起本质上是一个「由约定俗成的 MySQL 方言惯用法撞上更严格标准的 PostgreSQL 语义」的适配问题。MySQL 对语法很宽容date_format、find_in_set、反引号、insert ignore、limit 0,10这些它都认PostgreSQL 对标准的坚持更强很多 MySQL 里随手就写的东西它直接给你报错。而 Ruoyi 恰恰是一个在 MySQL 上长了七八年、沉淀了大量 MySQL 味道 SQL 的脚手架所以换库等于把所有历史包袱翻出来重新过一遍。这篇东西适合谁看如果你正在用 Ruoyi 做政企项目、信创项目或者客户明确要求 PostgreSQL那基本就是你要面对的活。也适合那些用 RuoYi-Vue-Plus 想换库但不知道从哪下手的人。我会把每一处坑的原理、改法、验证方式都摆出来尽量让你在真机上少走我走过的弯路。下面按「环境打通 → 建表脚本 → 代码适配 → 排查」的顺序铺开顺序可以按你的项目现状跳着看。1.1 换库的真实动因先说动因因为这直接决定你迁移时的取舍策略。常见的三种情况一是项目进了信创环境数据库只能选 PostgreSQL 或某些兼容 PG 的国产库二是数据规模上来了需要 PG 更强的窗口函数、JSONB、并行查询能力三是团队本来就以 PG 为技术栈不想为了一个脚手架再养一套 MySQL。三种动因对应三种完全不同的改法。如果只是「跑起来就行」那很多地方可以用兼容层糊过去比如保留char(1)字段、不重构 SQL只做最小化替换。如果是为了长期在 PG 上维护那就要趁这次把类型、函数、索引都改干净别留半吊子。我踩过的最大的坑就是一开始想着「先跑通再说」结果糊了一堆兼容写法半年后想加新功能时发现到处是地雷最后返工比一次改到位还累。还有一点不同版本的 Ruoyi 结构差别不小。RuoYi 单体版和 Cloud 版的核心表基本一致但 RuoYi-Vue-Plus 用的是 MyBatis-Plus很多查询不写在 XML 里而是靠条件构造器拼出来的这就意味着换库时你要改的位置完全不同——XML 里那些手写 SQL 变成构造器里的字段名和类型映射。所以动工之前先花半小时把项目里所有*.xml、application-*.yml、sql/目录、pom.xml里的数据库依赖列个清单后面按清单推进比拍脑袋改靠谱得多。1.2 换库前必须盘点的四类东西第一类是建表脚本。RuoYi 自带的ry_20xxx.sql是纯 MySQL 语法里面有AUTO_INCREMENT、ENGINEInnoDB、COMMENT、反引号、tinyint(1)、datetime这些东西全部要改写。第二类是框架里写死的 SQL。最典型的是权限相关的树形查询、日志统计、定时任务以及代码生成器的元数据查询。这些散落在各模块的 Mapper XML 里不翻一遍根本不知道埋了多少。第三类是配置。数据源 URL、驱动类名、Druid 校验语句、分页插件方言、MyBatis-Plus 的dbType、Quartz 的驱动器代理类一个都不能漏。第四类是应用层代码。有些地方 Java 里直接拼 SQL 或者依赖自增返回还有的地方用到了 MySQL 特有的时间格式。这部分藏得最深往往是运行到某个功能才暴露。盘完这四类心里就有底了。我一般会做一个表格标出「必须改」「可以不改」「改不改都行」三档避免过度修改引入新问题。2. 环境与依赖第一步先把连接打通连接打通是前提但很多人卡在这里就开始怀疑人生了。PG 的连接串、驱动、校验语句跟 MySQL 都不一样尤其 RuoYi 的application-druid.yml里有些默认值就是 MySQL 专属的。这一章先把这层打通后面的表结构和 SQL 才有意义。2.1 驱动和连接串先改pom.xml。MySQL 的依赖是mysql-connector-java新版本叫mysql-connector-jPG 要换成dependency groupIdorg.postgresql/groupId artifactIdpostgresql/artifactId version42.7.3/version /dependency版本别选太老的。42.2.x 在某些 JDK 版本上会有getGeneratedKeys行为和bytea单元格读写的兼容问题42.7.x 稳定得多。如果你用的是 RuoYi-Cloud 这种多模块项目别忘了ruoyi-common或者各业务模块里可能也单独声明了驱动搜一遍mysql关键字确认清干净。连接串改法# MySQL url: jdbc:mysql://localhost:3306/ry?useUnicodetruecharacterEncodingutf8zeroDateTimeBehaviorconvertToNulluseSSLtrueserverTimezoneGMT%2B8 driverClassName: com.mysql.cj.jdbc.Driver # PostgreSQL url: jdbc:postgresql://localhost:5432/ry?currentSchemapublicstringtypeunspecifiedreWriteBatchedInsertstrue driverClassName: org.postgresql.Driver这里有个参数值得单独说stringtypeunspecified。它让 PG 驱动在传字符串参数时不做强制类型推断避免column xxx is of type integer but expression is of type character varying这类报错。RuoYi 里大量 Mapper 传的参数是字符串但数据库字段是数值或时间PG 的类型检查比 MySQL 严不加这个参数会经常翻车。reWriteBatchedInsertstrue是批量插入的优化配合 RuoYi 的批量新增能快不少建议一起加上。2.2 数据源配置里几个必改项RuoYi 默认用 Druidapplication-druid.yml里有几个字段是 MySQL 专属的。validationQuery在 RuoYi 里默认写的是validationQuery: SELECT 1 FROM DUALPostgreSQL 没有DUAL这个虚拟表直接会报relation dual does not exist。改成SELECT 1就行。这就是个特别经典、特别容易漏、一启动就报错的坑。另一个是 Druid 的filters。如果配置里带了wall那它会对 SQL 做防火墙检查而 Druid 的 wall 过滤器主要是为 MySQL 设计的PG 的很多语法它识别不了会误拦。RuoYi 默认一般只有stat,slf4j但有些二次开发的版本加了wall这时候要么去掉wall要么换别的方案。还有连接池的健康检查、initialSize、minIdle这些不用动。dbType如果配置了记得从mysql改成postgresql。2.3 初始化脚本的取舍RuoYi 的sql/目录里有好几个脚本主业务表、Quartz 表、初始化数据可能还有流程引擎的表。MySQL 版和 PG 版不完全对应。Quartz 这块基本都是坑。RuoYi 默认给的是 MySQL 的quartz.sql直接扔到 PG 里执行会报一堆语法错。标准做法是用 Quartz 官方发行包里docs/dbTables/tables_postgres.sql同时把配置里的驱动器代理类改成org.quartz.jobStore.driverDelegateClassorg.quartz.impl.jdbcjobstore.PostgreSQLDelegateRuoYi 里这个配置通常在application.yml的spring.quartz.properties下或者单独一个quartz.properties。这个不改定时任务要么不执行要么执行报Couldnt retrieve job because a required class was not found。热词里有人问「ruoyi 任务不执行什么原因」如果你换了 PG 又没改这个代理类八成就是这个原因。初始化数据脚本里那些INSERT语句也要注意字符串里的单引号转义、日期字面量格式MySQL 和 PG 都能认大部分但\这种转义在 PG 的标准模式里可能出问题。稳妥一点是把\换成也就是用两个单引号表示一个单引号。3. 建表脚本类型映射踩坑最密集的地方建表脚本这一关是最费时的也是最值得认真做的。RuoYi 的表结构里藏着很多 MySQL 惯用法一次性改干净后面能省无数事。这一章把最常出问题的几类逐个说清楚。3.1 自增主键与序列MySQL 里bigint(20) NOT NULL AUTO_INCREMENT就搞定了自增。PG 现在推荐用标准 SQL 的GENERATED BY DEFAULT AS IDENTITY或者老式的bigserial。两种写法-- 方式一identity推荐PG 10 user_id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY -- 方式二bigserial user_id bigserial PRIMARY KEY建议用 identity因为它更接近标准后续维护清晰。但这里有个必须注意的细节RuoYi 很多表在初始化数据时是显式指定主键 ID插入的比如insert into sys_user(user_id, ...) values(1, ...)。用 identity 加BY DEFAULT时显式插入不会自动推进序列等你后面用框架自增插入新数据时序列还停在 1直接报主键冲突duplicate key value violates unique constraint。解决办法有两步。第一步建表时用GENERATED BY DEFAULT AS IDENTITY而不是ALWAYSALWAYS会禁止显式插入初始化脚本直接失败。第二步初始化数据跑完之后给每个有自增的表重置序列SELECT setval(pg_get_serial_sequence(sys_user, user_id), (SELECT COALESCE(MAX(user_id), 1) FROM sys_user));pg_get_serial_sequence对 identity 和 serial 都有效。这个操作一定要做我见过太多人迁完之后「查询都正常一新增就炸」排查半天才发现是序列没同步。而且这个问题特别隐蔽因为开发阶段你可能只点查询上线之后用户一注册就爆。另外RuoYi 的gen_table、gen_table_column、sys_oper_log、sys_logininfor这些表都有自增主键全部要处理。表多的话可以写个DO块批量处理但初期手动列个清单挨个setval更保险避免漏。3.2 tinyint、char(1) 与布尔RuoYi 里最典型的字段就是status char(1)、del_flag char(1)、visible char(1)值就是0和1。迁到 PG 之后char(1)本身语法是合法的但 PG 的char是定长类型存储时会用空格补齐到指定长度。char(1)存0没问题但如果某些表里是char(2)存了1实际存进去是1 比较时会出幺蛾子。我的建议是统一改成varchar(1)语义一样还避免补齐问题索引比较也不会被空格坑。RuoYi 的查询里status 0这种比较非常多定长类型的隐式转换虽然大多数时候能对上但涉及表达式索引、联合索引的时候容易失效。如果表里有tinyint(1)PG 里根本没有tinyint这个类型最近的是smallint。但tinyint(1)在 MySQL 里通常被当成布尔用PG 有原生boolean类型。这时候要看代码怎么处理的如果 Java 里映射的是Boolean那 PG 用boolean最自然如果 Java 里映射的仍然是Integer或String那就用smallint或varchar(1)更省事。别想当然地改成booleanMyBatis 的类型处理器没配好会报Cannot convert value of type ... to boolean。注意PG 的boolean字面量是true/false跟 MySQL 的1/0不通用。如果你的 SQL 里写了where enabled 1字段是 boolean 就会报类型错。这一点在写条件查询时最容易忽略。3.3 datetime、text、blob时间类型的对应关系不复杂但有几个细节。MySQL 的datetime对应 PG 的timestamp不带时区或者timestamptz带时区。RuoYi 里create_time、update_time这种字段用timestamp就够了行为跟datetime最接近。如果你想统一时区处理用timestamptz也行但那样 Java 端读取时要保证时区配置一致否则容易出现差 8 小时的问题。date对datetime对time这些没争议。text类型两边都有直接对应。但要注意MySQL 里varchar(2000)和text的区别经常被忽略PG 里也类似超过一定长度就得用text。RuoYi 的sys_oper_log.json_result用的是varchar(2000)sys_user.remark是varchar(500)这些长度只要够用就不用动。blob在 PG 里要改成bytea。RuoYi 本身用 blob 的地方不多但如果是自己扩展的表比如存附件二进制就要注意。MyBatis 映射bytea到byte[]没问题但 Druid 或某些连接池在读取大字段时可能有性能问题大文件建议还是走对象存储别塞数据库。还有个细节MySQL 的double、float、decimal在 PG 里分别对应double precision、real、numeric。RuoYi 里金额字段一般用decimal(10,2)PG 用numeric(10,2)语义完全一致。3.4 关键字、大小写与引号MySQL 用反引号包裹标识符PG 用双引号。建表脚本里的反引号全部要去掉或者换成双引号。但更重要的一个问题是大小写。PG 有个默认行为不加引号的标识符全部转小写。userName会被理解成username。而 MySQL 在 Linux 下默认表名区分大小写Windows 下不区分。如果 RuoYi 的表名和字段名都是小写它基本都是就没什么问题。但如果你的自定义表和字段用了驼峰迁到 PG 后就会找不到列。所以原则是不要用双引号去强行保留大小写那样以后每一条 SQL 都必须带引号极其痛苦。统一改成小写下划线命名是最省心的。还有一个高频坑是保留字。PG 里user、order、group、desc、asc、limit、offset、check、default、table这些都是保留字或者半保留字。表名sys_user带前缀所以安全但如果你的业务表里有个字段叫order或者user查询就会报语法错。解决办法只有一个要么改名加前缀要么每次用双引号包起来。我一般直接改名比如order改成order_no一劳永逸。3.5 注释与索引MySQL 的建表语句里注释是内嵌的user_name varchar(30) NOT NULL COMMENT 用户账号PG 不支持这种行内注释要改成独立的语句COMMENT ON COLUMN sys_user.user_name IS 用户账号;表注释同理COMMENT ON TABLE sys_user IS 用户信息表;。RuoYi 的表注释挺全的如果在意文档完整性就都补上如果是内部项目不追求这个注释可以先略过但代码生成器依赖表注释来生成字段描述所以你要是还用代码生成功能注释就最好补上。索引方面MySQL 的KEY idx_x (col)、UNIQUE KEY、PRIMARY KEY写法要改成 PG 的CREATE INDEX idx_user_name ON sys_user(user_name); ALTER TABLE sys_user ADD CONSTRAINT uk_xxx UNIQUE(user_name);外键约束 RuoYi 默认都不加所以这里不用太操心。但如果你自己加了外键注意 PG 的外键检查比 MySQL 严删除数据时的顺序会影响执行结果。还有个容易忽略的点MySQL 的ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_general_ci这些尾部子句在 PG 里必须删掉。PG 的字符集和排序规则在建库时指定不在建表时。建库推荐CREATE DATABASE ry WITH ENCODING UTF8 LC_COLLATE zh_CN.UTF-8 LC_CTYPE zh_CN.UTF-8 TEMPLATE template0;如果服务器上没有中文排序规则用C或者en_US.UTF-8也行但中文排序可能不理想。4. 代码与 SQL 层适配表结构改完、数据导完接下来是真正考验耐心的地方——框架里那些 MySQL 专属 SQL。这一章按类别列都是我在实际项目里一条条改过的。4.1 分页插件与 MyBatis 配置RuoYi 老版本用 PageHelper 做分页。它的方言配置在application.yml或者pagehelper相关配置里pagehelper: helperDialect: postgresql reasonable: true supportMethodsArguments: true params: countcountSqlhelperDialect从mysql改成postgresql这个不改分页 SQL 会生成limit 0, 10这种 MySQL 语法PG 直接报错。实际上 PageHelper 的 PG 方言生成的是limit 10 offset 0是对的。如果你用的是 RuoYi-Vue-Plus 或者是新版的 MyBatis-Plus 分页配置在MybatisPlusConfig里Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.POSTGRE_SQL)); return interceptor; }DbType从MYSQL改成POSTGRE_SQL。不改的话分页查询一样会生成 MySQL 版的limit。还有application.yml里的mybatis-plus.global-config.db-config.db-type有的话改成postgresql。4.2 日期时间函数改写这是重灾区。RuoYi 的 Mapper XML 里到处都是 MySQL 的日期函数最典型的是date_format。比如按时间段查询用户很多版本里写的是if testparams.beginTime ! null and params.beginTime ! and date_format(u.create_time,%y%m%d) gt; date_format(#{params.beginTime},%y%m%d) /ifPG 里没有date_format要改成to_charand to_char(u.create_time, YYYYMMDD) to_char(#{params.beginTime}::timestamp, YYYYMMDD)注意#{params.beginTime}传进来是字符串PG 需要显式转成时间类型再做to_char否则会报类型不匹配。或者更简单的写法是直接比较时间范围避免字符串转换and u.create_time #{params.beginTime}::timestamp我一般倾向于后者因为to_char会把索引用不上大表上性能差很多。原来 MySQL 那种写法本身就是个性能隐患正好借这次迁移改掉。其他常见函数对应关系MySQLPostgreSQL说明date_format(d, %Y%m%d)to_char(d, YYYYMMDD)格式串语法不同sysdate()/now()now()PG 的now()返回事务开始时间date_sub(now(), interval 1 day)now() - interval 1 day写法差异date_add(d, interval 7 day)d interval 7 day直接加减unix_timestamp()extract(epoch from now())::bigint返回秒级时间戳curdate()current_date当前日期from_unixtime(x)to_timestamp(x)时间戳转时间RuoYi 的登录日志、操作日志里时间格式化用 Java 的DateUtils比较多这部分不用动。纯 SQL 里遇到的按上面表换就行。4.3 字符串与聚合函数字符串函数这块PG 大部分都有对应但行为细节有差异。concat(a, b)两边都有但 PG 的concat会忽略 NULLMySQL 的concat遇到 NULL 返回 NULL。这个差异会导致结果不一致。比如concat(remark, -, user_name)如果remark是 NULLMySQL 返回 NULLPG 返回-张三。如果业务逻辑依赖这个行为就要小心。建议用||拼接||在两边遇到 NULL 都是返回 NULL行为一致。ifnull(a, b)在 PG 里没有要用coalesce(a, b)。这个替换很直接全局搜索替换即可。group_concat在 PG 里是string_agg而且必须指定分隔符-- MySQL select group_concat(user_name) from sys_user where dept_id 100 -- PostgreSQL select string_agg(user_name, ,) from sys_user where dept_id 100substring_index(a, ,, 1)在 PG 里要用split_part(a, ,, 1)。RuoYi 里用到这个的地方主要在部门树和角色权限的字符串处理上。注意split_part的索引是从 1 开始的跟substring_index一致但split_part只支持取单段不像substring_index可以取前 n 段。如果是取前 n 段得用array_to_string((string_to_array(a, ,))[1:n], ,)。locate(a, b)在 PG 里用position(a in b)或者strpos(b, a)。注意参数顺序这两个函数跟locate不太一样容易写反。replace、upper、lower、trim、length这些两边都有语义基本一致。但length在 PG 里对text返回字符数对bytea返回字节数MySQL 的length默认返回字节数char_length才是字符数用的时候留意一下。4.4 find_in_set 与树形结构查询这是个特别经典的坑。RuoYi 的部门表sys_dept有个ancestors字段存的是祖先路径比如0,100,101。查某个部门及其所有下级的时候SQL 里会用find_in_set(#{deptId}, ancestors)PG 完全没有find_in_set。最直接的改法是用数组#{deptId} any(string_to_array(ancestors, ,))这样写语义等价性能也还行。如果数据量大可以给ancestors加 GIN 索引但前提是把字段类型改成text[]存数组而不是逗号分隔字符串。那就涉及数据迁移了不是简单替换能解决的。还有一个替代写法是用likeancestors like concat(%,, #{deptId}, ,%) or ancestors like concat(#{deptId}, ,%)这个写法不好看而且容易漏边界我不推荐。string_to_array加any干净得多。RuoYi 里还有别的地方用了find_in_set比如角色查部门数据权限的时候。全局搜一遍find_in_set见一个改一个。提示PG 的any配合数组很好用但要注意数组元素的类型。string_to_array返回text[]deptId如果是 bigint比较时会自动转换但为了稳妥可以显式#{deptId}::bigint any(...)。4.5 插入语句与主键回填RuoYi 的 Mapper 里有一些批量插入、insert ignore、on duplicate key update之类的写法。insert ignore into是 MySQL 特有的用来忽略重复主键错误。PG 里用insert into sys_config(...) values(...) on conflict do nothing或者指定冲突列on conflict (config_key) do nothingon duplicate key update在 PG 里是insert into sys_config(config_key, config_value) values(#{configKey}, #{configValue}) on conflict (config_key) do update set config_value excluded.config_value注意excluded是 PG 里的关键字代表要插入的那一行。这个语法差异必须记住。主键回填这块RuoYi 的useGeneratedKeystrue keyPropertyuserId写法在 PG 下是能用的PG 的 JDBC 驱动在Statement.RETURN_GENERATED_KEYS时会自动追RETURNING *。但有个前提表必须有 identity 或 serial 主键。如果你的表没设自增比如用雪花 ID那就不能靠回填得在 Java 层手动赋值。RuoYi-Vue-Plus 用的是 MyBatis-Plus 的IdType.ASSIGN_ID走的是雪花算法本来就不依赖数据库自增这块反而更省事。4.6 代码生成器的隐藏工作量这是我觉得最坑的一块因为很多人根本没想到代码生成器也要改。RuoYi 的代码生成器靠查询information_schema来获取表结构。它默认的 SQL 是 MySQL 方言select table_name, table_comment, create_time, update_time from information_schema.tables where table_schema (select database())这里有两个问题。第一PG 里没有database()函数要用current_schema()或者直接写public。第二PG 的information_schema.tables没有table_comment这一列表注释存在pg_catalog里得用obj_description取。改写成 PG 版本大概是这样select c.relname as table_name, obj_description(c.oid) as table_comment from pg_class c join pg_namespace n on n.oid c.relnamespace where c.relkind r and n.nspname public order by c.relname列信息的查询同理PG 里查字段注释用col_description(attrelid, attnum)判断是否可空要处理 PG 返回的 boolean。RuoYi 原来的 SQL 里写了is_nullable NOPG 的is_nullable返回的是boolean类型NO这个字符串根本比不了得改成not is_nullable或者把查询结果显式转成字符串。这段要彻底重写的话工作量不小。如果你项目里根本不用代码生成功能很多生产项目上手就把它删了那可以直接跳过。但如果要用就要把GenTableMapper.xml和GenTableColumnMapper.xml里的查询全部换成 PG 版本字段类型映射那块也要改——MySQL 的varchar(30)和 PG 的character varying(30)字符串不一致RuoYi 生成实体类时是按类型字符串做映射的PG 返回的format_type结果是character varying、bigint、timestamp without time zone这种得加一层映射逻辑。5. 常见报错与排查速查迁移过程中报错五花八门但归纳起来就那么几十种。这一章把最高频的整理成表再补几个我踩过之后印象特别深的。5.1 报错对照表报错信息根因解决relation dual does not existDruid 校验语句用了 MySQL 的 DUAL改成SELECT 1duplicate key value violates unique constraint序列没同步setval重置序列column ... is of type integer but expression is of type character varying参数类型不匹配连接串加stringtypeunspecified或 SQL 显式转型function date_format(...) does not exist用了 MySQL 日期函数改成to_charfunction find_in_set(...) does not exist部门树查询用了 MySQL 函数改成any(string_to_array(...))function group_concat(...) does not exist聚合拼接用了 MySQL 函数改成string_aggsyntax error at or near limit分页方言没改改helperDialect或DbTypecolumn xxx does not exist大小写问题或字段名错误统一小写检查字段名syntax error at or near 建表或 SQL 里残留反引号去掉反引号null value in column ... violates not null constraint初始化数据缺字段或序列/默认值问题补默认值或修正数据could not determine data type of parameterMyBatis 传 null 参数时 PG 无法推断加jdbcType或在连接串加stringtypeunspecifiedoperator does not exist: boolean integerboolean 和数值混用SQL 显式转型或改字段类型relation QRTZ_... does not existQuartz 表没建或大小写不对用 PG 版脚本注意表名大小写关于could not determine data type of parameter这个特别值得说。MySQL 里传 NULL 参数它经常能蒙对PG 不行它会要求你明确类型。最常见的场景是where create_time #{startTime}里startTime是 nullMyBatis 把 null 传过去PG 不知道这个 null 是什么类型直接报错。解决办法是在 Mapper 里写#{startTime, jdbcTypeTIMESTAMP}或者用if teststartTime ! null包起来避免传空。RuoYi 里大部分动态 SQL 都有if判断但总有漏网的。5.2 上线前自查清单迁移完别急着上线按这个清单过一遍所有pom.xml里的 MySQL 驱动是否清干净有没有依赖传递带进来数据源 URL、驱动类、校验语句、大小写是否正确所有自增序列是否setval重置过特别是初始化数据里显式插了 ID 的表全局搜索date_format、find_in_set、group_concat、ifnull、sysdate、insert ignore、on duplicate key、反引号确认无残留PageHelper 或 MyBatis-Plus 的分页方言是否切换Quartz 的 PG 脚本是否执行、代理类是否配置代码生成器的元数据查询是否适配如果还要用所有char(1)字段在 PG 里确认比较行为正常特别是联合索引场景时间字段的时区行为在测试环境验证一下插入和查询不要有 8 小时偏差用真实数据量跑一遍慢查询PG 的执行计划逻辑跟 MySQL 不一样原来靠索引的地方可能需要重建索引5.3 我的几条实操心得第一条迁移顺序要反过来。很多人习惯先把数据导进去再改代码结果一跑全是报错分不清是表结构问题还是 SQL 问题。我的做法是先用 PG 建一套空表把框架跑起来让所有查询 SQL 都能在没有数据的表上执行通过返回空结果也行确认语法层面没毛病再导数据。这样问题定位简单得多。第二条stringtypeunspecified不是万能的。它解决了大部分参数类型问题但也可能掩盖真正的类型错误。如果某个查询在加了它之后结果不对不是报错是结果不对那就要警惕了。我遇到过varchar和char比较时因为类型推断导致索引失效加了这个参数之后不报错了但查询变成全表扫描。这种情况要单独分析执行计划。第三条数据导出导入用COPY而不是INSERT。PG 的COPY命令处理大批量数据快得多而且对转义的处理跟 MySQL 不一样直接用INSERT导几百万行日志表会很痛苦。用pg_dump从 MySQL 导出的 SQL 要经过转换才能用推荐的做法是用工具比如用 Java 写个临时导入程序或者用COPY ... FROM STDIN配合 CSV把数据转成 PG 能认的格式。第四条布尔值和char(1)的抉择要一次定死。项目里如果status字段有的用char(1)、有的用boolean后期维护会非常痛苦。我倾向于全部保持varchar(1)跟原来的代码习惯兼容改动最小风险最低。等系统稳定运行一段时间再考虑是否做面向 PG 的深度优化。第五条别忘了 Docker 环境。热词里有ruoyi radius docker这种关键词说明不少人是容器化部署。容器里换库要注意JDBC 连接串里的主机名从mysql改成postgresql取决于 compose 里的服务名数据卷要重新挂载docker-compose.yml里的健康检查命令mysqladmin ping要改成pg_isready。这些看起来是小事但第一次做不熟的话光调试「容器起来了但应用连不上」就能耗掉一下午。还有一个坑我单独拎出来说PG 的默认事务隔离级别和 MySQL 不一样。MySQL 默认REPEATABLE READPG 默认READ COMMITTED。RuoYi 的很多业务代码是依赖 MySQL 的可重复读语义跑起来的比如先查再改再查在 PG 的读已提交下可能读到不一样的结果。虽然大部分场景没问题但如果你的业务里有并发扣库存、并发更新状态这类逻辑迁移后要重点测一下。真遇到问题可以通过spring.datasource.hikari.transaction-isolation或者连接参数调整但更根本的做法是让业务代码不依赖数据库的隔离级别特性。这一路迁下来我的整体感受是RuoYi 换 PG 不是「改配置」级别的活而是「把脚手架里所有 MySQL 暗语翻译一遍」的活。翻译工作量跟项目复杂度成正比最省事的路径是先用最小可用集登录、菜单、用户管理跑通再逐个模块推进。真把这一遍走完你对 RuoYi 的 SQL 分布会有全新的认识以后再遇到任何数据库适配问题心态都会稳很多。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →