社区所有版块导航
Python
python开源   Django   Python   DjangoApp   pycharm  
DATA
docker   Elasticsearch  
aigc
aigc   chatgpt  
WEB开发
linux   MongoDB   Redis   DATABASE   NGINX   其他Web框架   web工具   zookeeper   tornado   NoSql   Bootstrap   js   peewee   Git   bottle   IE   MQ   Jquery  
机器学习
机器学习算法  
Python88.com
反馈   公告   社区推广  
产品
短视频  
印度
印度  
Py学习  »  DATABASE

MySQL 切 PostgreSQL 改个驱动就行?这 3 个大坑能让你改一周代码

Java基基 • 3 月前 • 272 次点击  

👉 这是一个或许对你有用的社群

🐱 一对一交流/面试小册/简历优化/求职解惑,欢迎加入芋道快速开发平台知识星球。下面是星球提供的部分资料: 

👉这是一个或许对你有用的开源项目

国产Star破10w的开源项目,前端包括管理后台、微信小程序,后端支持单体、微服务架构

RBAC权限、数据权限、SaaS多租户、商城、支付、工作流、大屏报表、 ERPCRMAI大模型、IoT物联网等功能:

  • 多模块:https://gitee.com/zhijiantianya/ruoyi-vue-pro
  • 微服务:https://gitee.com/zhijiantianya/yudao-cloud
  • 视频教程:https://doc.iocoder.cn
【国内首批】支持 JDK17/21+SpringBoot3、JDK8/11+Spring Boot2双版本 

前言:换数据库不是改个驱动那么简单

原项目栈:Spring Boot + MyBatis-Plus + MySQL 。换 PostgreSQL,最初以为只是改个驱动包、改个 JDBC URL——结果踩了一堆坑。

核心结论先甩在这里 :MySQL 是宽容型方言,PostgreSQL 是严格型方言。从 MySQL 迁过去最大的事不是 SQL 改写,而是习惯了 MySQL 的"自动隐式转换"在 PG 上全部失效 。

下面把 11 个具体差异按破坏力分成大/中/小坑 ——按这个顺序看,能让你避开 80% 的踩坑时间:

等级
数量
特点
修改成本
🔴 大坑
3 个
迁移必踩、可能让你重写部分代码
改一周
🟡 中坑
3 个
用到了对应特性才会触发
改一两天
🟢 小坑
5 个
语法差异、一眼能改
改半小时

基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

  • 项目地址:https://github.com/YunaiV/ruoyi-vue-pro
  • 视频教程:https://doc.iocoder.cn/video/

切换流程:依赖、JDBC、模式概念

引入 PostgreSQL 驱动

<dependency>
    <groupId>org.postgresqlgroupId>
    <artifactId>postgresqlartifactId>
dependency>

修改 JDBC 连接信息

spring:
  datasource:
    driver-class-name:org.postgresql.Driver
     # PG 驱动只识别 PG 自己的参数——别把 useUnicode / characterEncoding / serverTimezone / useSSL 这些 MySQL 的私货带过来
    url:jdbc:postgresql://数据库地址/数据库名?currentSchema=模式名

    # 可选参数(按需开):
    #   ApplicationName=myApp   ← 在 pg_stat_activity 里能看到,方便定位连接来源
    #   connectTimeout=10       ← 连接超时(秒)
    #   socketTimeout=30        ← 读取超时(秒)
    #
    # 走 SSL 用 PG 自己的 ssl / sslmode:
    #   ssl=true&sslmode=require

这里有个 PG 特有的概念要先记住 :PostgreSQL 比 MySQL 多了一层"模式"(schema) ——一个数据库下可以有多个 schema。URL 里的 currentSchema 等价于以前 MySQL 里的"数据库名",不指定就走默认的 public

到这里改造看着完了——但你以为这就结束了?后面才是真正的灾难现场。

基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

  • 项目地址:https://github.com/YunaiV/yudao-cloud
  • 视频教程:https://doc.iocoder.cn/video/

3 个大坑:必踩 + 修改成本最高

🔴 大坑 1:类型转换异常——强类型对弱类型的清算

这是迁移踩坑次数最多、修改成本最高的差异 ——单这一个坑就够你改一周。

MySQL 是弱类型——字段是 tinyint,传 boolean 也能匹配,自动转。PG 是强类型 ,字段类型和参数类型必须一致,否则直接抛异常。最常见两种触发:

-- ❌ SELECT 时类型不匹配:is_active 是 smallint,传 true
SELECT * FROM orders WHERE is_active = true
-- 报错:operator does not exist: smallint = boolean

-- ❌ UPDATE 时类型不匹配
UPDATE orders SET is_premium = false WHERE is_premium = true
-- 报错:column "is_premium" is of type smallint but expression is of type boolean

两种解法 :

方案一 (首选):改 PG 字段类型为 boolean ,或改代码字段类型为 Integer——保证两边对齐。

方案二 (无源码修改权时):在 PG 里注册隐式类型转换函数:

-- smallint → boolean
CREATEORREPLACEFUNCTION smallint_to_boolean(i int2)
RETURNSboolAS $BODY$
BEGIN
RETURN (i::int2)::integer::bool;
END;
$BODY$ LANGUAGE plpgsql VOLATILE;

CREATECAST (smallintASbooleanWITHFUNCTION smallint_to_boolean AS ASSIGNMENT;

-- boolean → smallint
CREATEORREPLACEFUNCTION boolean_to_smallint(b bool)
RETURNS int2 AS $BODY$
BEGIN
RETURN (b::boolean)::bool::int;
END;
$BODY$ LANGUAGE plpgsql VOLATILE;

CREATECAST (booleanASsmallintWITHFUNCTION boolean_to_smallint AS IMPLICIT;

⚠️ 谨慎使用隐式转换 :注册过多 cast 可能让 PG 在解析操作符时找不到唯一最优匹配,抛 Could not choose a best candidate operator 或 operator is not unique——能改代码的优先改代码 。

🔴 大坑 2:TIMESTAMPTZ 与 LocalDateTime 不兼容

PSQLException: Cannot convert the column of type TIMESTAMPTZ to requested type java.time.LocalDateTime.

PG 的 TIMESTAMPTZ(带时区的时间戳)没法直接映射到 Java 的 LocalDateTime 。两个解法:

  • PG 表字段改成 timestamp (不带时区)——推荐
  • 或 Java 字段类型改成 OffsetDateTime / Date

最好建表时就避开 :MySQL 迁移工具默认生成 TIMESTAMPTZ手动改成 timestamp ——后续改代码会少踩很多坑。

🔴 大坑 3:事务异常会污染整个事务

Cause: org.postgresql.util.PSQLException: ERROR: current transaction is aborted, commands ignored until end of transaction block

这是 PG 一个特别让人迷惑的行为 :同一事务里只要某条 SQL 出错,整个事务就废了 ——之后所有语句都会被忽略,必须 ROLLBACK 才能继续。

MySQL 完全不会有这个问题。错误源头通常是这种代码:

if (assigneeStatistics == null) {
    StatisticsEntity entity = new StatisticsEntity();
    entity.setProcessDefKey(processDefKey);
    entity.setTaskDefKey(taskDefKey);
    entity.setUserId(userId);
    entity.setChannel(channel);
    entity.setQuantity(1);

    try {
        save(entity); // 这里 INSERT 可能因为唯一键冲突失败
    } catch (DuplicateKeyException e) {
        // MySQL 下可能还能继续执行;PostgreSQL 下事务已经进入 aborted 状态
        statisticsMapper.increaseByUnique(processDefKey, taskDefKey, userId, channel);
    }
else {
    statisticsMapper.increase(assigneeStatistics.getId());
}

问题点 :save(entity) 抛异常后,代码还在同一个事务里继续执行 increaseByUnique(...)

翻译过来就是 :业务代码 try-catch 了一个 SQL 异常,想"接着做下一步"——MySQL 默认放行,PG 直接拒绝。

解决思路 :别用数据库异常控制业务逻辑 ——改成手动判断(先 SELECT 看记录是否存在再决定 INSERT/UPDATE)。这是 PG 迁移最容易出"线上事故"的地方,所有 catch 了 SQLException 后还要继续 DB 操作的代码必须排查一遍 。

3 个中坑:业务用到就要改

🟡 中坑 1:JSON 字段语法不同




    
-- MySQL:JSON Path 语法 + ->
WHERE order_meta->'$.coupon_code' LIKE CONCAT('%', ?, '%')

-- PostgreSQL:直接 ->>'key' 取值
WHERE order_meta ->>'coupon_code' LIKE CONCAT('%', ?, '%')

MySQL 用 -> + $.xxx,PG 用 ->>'xxx' 直接拿值。项目里 JSON 字段越多,改的地方越多 。

🟡 中坑 2:date_format 不存在 → 用 to_char + 占位符对照

替换示例(保留代码方便读者直接拷贝):

to_char(create_time, 'YYYY-MM-DD')        <=>  DATE_FORMAT(create_time, '%Y-%m-%d')
to_char(create_time, 'YYYY-MM')           <=>  DATE_FORMAT(create_time, '%Y-%m')
to_char(create_time, 'YYYYMMDDHH24MISS')  <=>  DATE_FORMAT(create_time, '%Y%m%d%H%i%s')

🟡 中坑 3:GROUP BY 严格性

Cause: org.postgresql.util.PSQLException: ERROR: column "u.user_name" must appear in the GROUP BY clause or be used in an aggregate function

PG 严格遵循 SQL 标准:SELECT 字段必须在 GROUP BY 里,或者用聚合函数包起来 。MySQL 容忍非聚合列(随机取值),PG 直接报错。

-- ❌ 错(user_name 不在 GROUP BY 里、也没聚合)
SELECT user_name, age, COUNT(*)
FROM user_profile
GROUPBY age, score

-- ✅ 改法 1:把 user_name 加进 GROUP BY
SELECT user_name, age, COUNT(*)
FROM user_profile
GROUPBY user_name, age, score

-- ✅ 改法 2:用聚合包起来
SELECTMIN(user_name), age, COUNT(*)
FROM user_profile
GROUPBY age, score

真正坑的是 :MySQL 项目里 GROUP BY 写得越粗放,迁移到 PG 要改的查询越多——全文 SQL 搜索 GROUP BY 一一过一遍是必须的 。

小坑速查:5 个一眼能改的语法差异

这五个是纯语法差异 ——遇到了改一改就行,不影响业务逻辑:

差异
MySQL
PostgreSQL
修改方式
字符串值WHERE status = "paid"WHERE status = 'paid'
双引号改单引号
字段名包裹WHERE \
order_status` = 'paid'`
WHERE order_status = 'paid'
反引号改成无 / 双引号
类型转换convert(amount, DECIMAL(20, 2))CAST(amount AS DECIMAL(20, 2))convert
 换成 CAST
空值替换IFNULL(coupon_amount, 0)COALESCE(coupon_amount, 0)IFNULL
 换成 COALESCE
强制索引force index(idx_create_time)
(不支持)
直接删除——交给 PG 优化器

说白了 :这五个用 IDE 全文替换跑一遍就能搞定大半,剩下的让 PG 抛异常时再补刀。

迁移辅助脚本:批量改字段类型与默认值

批量把 timestamptz 改成 timestamp

ps:

  • timestamp without time zone 就是 timestamp
  • timestamp with time zone 就是 timestamptz

DO $$
DECLARE
    rec RECORD;
BEGIN
    FOR rec IN SELECT table_name, column_name, data_type
               FROM information_schema.columns
               WHERE table_schema = '要处理的模式名'
                 AND data_type = 'timestamp with time zone'
    LOOP
        EXECUTE 'ALTER TABLE ' || rec.table_name || ' ALTER COLUMN ' || rec.column_name || ' TYPE timestamp';
    END LOOP;
END $$;

批量给 create_time / update_time 设默认值

-- 注意:|| 拼接的字符串前面要有空格
DO $$
DECLARE
    rec RECORD;
BEGIN
    FOR rec INSELECT table_name, column_name, data_type
               FROM information_schema.columns
               WHERE table_schema = '要处理的模式名'
                 AND data_type = 'timestamp without time zone'
                 AND column_name in ('create_time''update_time')
    LOOP
         EXECUTE'ALTER TABLE ' || rec.table_name || ' ALTER COLUMN ' || rec.column_name || ' SET DEFAULT CURRENT_TIMESTAMP;';
    ENDLOOP;
END $$;

迁移注意事项:四条不踩坑原则

  1. 数据表迁移时字段类型严格对应 ——别想当然地变更
  2. MySQL 的 tinyint 对应 PG 的 smallint ——不要换成 bool 类型,代码侧的 Java 字段对应不上
  3. 如果 Java 用 LocalDateTime,PG 一定别用 TIMESTAMPTZ ——大坑 2 已经踩过这坑
  4. MySQL 的  tinyint ↔ Java Boolean 自动转换在 PG 失效 ——两个选择:
  • 在 PG 里注册隐式转换函数(每次部署后跑一遍)
  • 或修改代码所有表对象的字段类型与传参类型——但底层框架自己写 SQL 的部分改不了,只能从数据库侧解决

最后说一句 :这种迁移不要直接灰度上生产 。先用一个真实业务的子模块在预发跑两周,把所有 SQL 都过一遍——3 个大坑你不全踩到,就不算真正完成迁移评估 。



欢迎加入我的知识星球,全面提升技术能力。

👉 加入方式,长按”或“扫描”下方二维码噢

星球的内容包括:项目实战、面试招聘、源码解析、学习路线。

文章有帮助的话,在看,转发吧。

谢谢支持哟 (*^__^*)

Python社区是高质量的Python/Django开发社区
本文地址:http://www.python88.com/topic/195980