当前位置: 代码网 > it编程>数据库>Mysql > RuoYi从MySQL迁移到PostgreSQL数据库的踩坑实录

RuoYi从MySQL迁移到PostgreSQL数据库的踩坑实录

2026年02月13日 Mysql 我要评论
写在前面公司项目用的是postgresql,之前几个项目都在用,对它的事务隔离级别、mvcc这些特性还算熟悉。这次有个后台管理系统,我们选择用ruoyi框架。我下载的是mysql版本的ruoyi(不知

写在前面

公司项目用的是postgresql,之前几个项目都在用,对它的事务隔离级别、mvcc这些特性还算熟悉。这次有个后台管理系统,我们选择用ruoyi框架。我下载的是mysql版本的ruoyi(不知道有没有postgresql版本,如果有的话,请老铁们告诉我一声)。本来以为就是改个数据库连接的事儿,毕竟两种数据库的sql标准都差不多,结果还是遇到了一些问题。这里记录一下踩过的坑,给后面要做类似迁移的朋友们提个醒。

第一个坑:druid连不上

一开始启动项目,直接报错:

org.postgresql.util.psqlexception: error: relation "dual" does not exist

当时我还懵了,dual是啥?后来查了才知道,druid默认用的是oracle的验证sql select 1 from dual,mysql能兼容这个语法,但postgresql不认。

这个好办,去nacos配置中心改一下:

# 原来的配置
validationquery: select 1 from dual

# 改成这样就行
validationquery: select 1

改完重启,总算能连上数据库了。

数据迁移工具的选择

数据库连上了,接下来就是把mysql的数据迁移到postgresql。这块我研究了几个工具:

  1. pgloader:专门做数据库迁移的工具,功能很强大,但是二进制文件,安装比较麻烦(特别是在windows上)
  2. aws dms:如果在云上可以用,但我们是本地环境
  3. 各种商业工具:比如navicat的数据传输功能,不过都是黑盒操作,出了问题不好排查

最后我决定用python自己写一个迁移脚本。主要考虑几点:

  1. 代码清晰透明:整个迁移逻辑一目了然,出问题好调试
  2. 灵活可控:可以在迁移过程中对数据做清洗和转换
  3. 通用性好:python操作数据库很成熟,pymysqlpsycopg2都是久经考验的库
  4. 容易扩展:需要加日志、进度显示、异常处理,随时都能加

下面是完整的迁移脚本代码:

# coding=utf-8
import pymysql
import psycopg2
from psycopg2 import extras

# mysql配置
mysql_config = {
    'host': '192.168.1.100',
    'port': 3306,
    'user': 'root',
    'password': 'your_password',
    'database': 'ruoyi',
    'charset': 'utf8mb4'
}

# postgresql配置
pg_config = {
    'host': '192.168.1.101',
    'port': 5432,
    'user': 'postgres',
    'password': 'your_password',
    'database': 'ruoyi'
}

def get_all_tables(mysql_conn):
    """获取mysql中所有表名"""
    cursor = mysql_conn.cursor()
    cursor.execute("show tables")
    tables = [table[0] for table in cursor.fetchall()]
    cursor.close()
    return tables

def get_table_structure(mysql_conn, table_name):
    """获取表的列信息"""
    cursor = mysql_conn.cursor()
    cursor.execute(f"describe {table_name}")
    columns = cursor.fetchall()
    cursor.close()
    return columns

def migrate_table(mysql_conn, pg_conn, table_name):
    """迁移单个表的数据"""
    print(f"正在迁移表: {table_name}")
    
    # 获取mysql数据
    mysql_cursor = mysql_conn.cursor()
    mysql_cursor.execute(f"select * from {table_name}")
    rows = mysql_cursor.fetchall()
    
    if not rows:
        print(f"  表 {table_name} 为空,跳过")
        mysql_cursor.close()
        return
    
    # 获取列名
    columns = [desc[0] for desc in mysql_cursor.description]
    mysql_cursor.close()
    
    # 构建insert语句
    placeholders = ','.join(['%s'] * len(columns))
    columns_str = ','.join(columns)
    insert_sql = f"insert into {table_name} ({columns_str}) values ({placeholders})"
    
    # 批量插入到postgresql
    pg_cursor = pg_conn.cursor()
    try:
        # 使用execute_batch提高性能
        extras.execute_batch(pg_cursor, insert_sql, rows, page_size=1000)
        pg_conn.commit()
        print(f"  成功迁移 {len(rows)} 条记录")
    except exception as e:
        pg_conn.rollback()
        print(f"  迁移失败: {str(e)}")
        # 如果批量插入失败,尝试逐条插入(方便定位问题)
        error_count = 0
        for row in rows:
            try:
                pg_cursor.execute(insert_sql, row)
                pg_conn.commit()
            except exception as e:
                pg_conn.rollback()
                error_count += 1
                if error_count <= 5:  # 只打印前5个错误
                    print(f"    错误记录: {row[:3]}... 错误: {str(e)}")
        
        if error_count > 0:
            print(f"  共有 {error_count} 条记录插入失败")
    finally:
        pg_cursor.close()

def main():
    # 连接数据库
    print("连接mysql...")
    mysql_conn = pymysql.connect(**mysql_config)
    
    print("连接postgresql...")
    pg_conn = psycopg2.connect(**pg_config)
    
    try:
        # 获取所有表
        tables = get_all_tables(mysql_conn)
        print(f"共找到 {len(tables)} 个表\n")
        
        # 逐个迁移
        for i, table in enumerate(tables, 1):
            print(f"[{i}/{len(tables)}] ", end='')
            migrate_table(mysql_conn, pg_conn, table)
        
        print("\n所有表迁移完成!")
        
    finally:
        mysql_conn.close()
        pg_conn.close()
        print("数据库连接已关闭")

if __name__ == '__main__':
    main()

使用方法:

  1. 先安装依赖库:
pip install pymysql psycopg2-binary
  1. 修改脚本中的数据库配置

  2. 重要:在postgresql中先创建好表结构(可以用navicat等工具导出mysql的表结构,然后手动调整一下再导入pg)

  3. 运行脚本:

python migrate_data.py

脚本的几个亮点:

  • 批量插入:用execute_batch一次插入1000条,比逐条insert快很多
  • 错误处理:批量失败会回退,然后尝试逐条插入,方便定位问题数据
  • 进度显示:能看到每个表的迁移进度和结果
  • 事务控制:出错自动回滚,保证数据一致性

这个脚本虽然简单,但对于ruoyi这种规模的项目完全够用了。而且代码都在自己手里,需要加什么功能随时改。比如我后来还加了个功能,在迁移某些表之前先做一下数据清洗,把一些脏数据过滤掉。

如果你的项目数据量特别大(千万级以上),可能还是要用pgloader这种专业工具,性能会更好。但对于一般的中小型项目,python脚本足够了,关键是清晰、可控。

sql语法的各种不兼容

数据库连上之后,开始测试功能,发现各种sql报错。

反引号问题

postgresql不认mysql的反引号:

-- mysql这样写没问题
select `user_id`, `user_name` from sys_user

-- 到pg就报错了
error: syntax error at or near "`"

这个把反引号全删了就行。可以用编辑器的全局替换功能,或者写个脚本批量处理。

注意:批量替换时一定要用utf-8编码,不然中文会乱码。还有就是操作前先提交git,避免改错了不好恢复。

ifnull函数

postgresql没有ifnull(),要用coalesce()替代:

-- mysql
select ifnull(perms, '') as perms from sys_menu

-- postgresql
select coalesce(perms, '') as perms from sys_menu

这个也可以批量替换,具体的替换规则在后面会列出来。

sysdate函数

又是一个mysql特有的函数:

-- mysql
update sys_user set update_time = sysdate()

-- postgresql要换成这个
update sys_user set update_time = current_timestamp

这种函数不兼容的地方还挺多的,好在都能批量处理。

类型严格性的坑

这个坑比较隐蔽。postgresql对类型检查比mysql严格得多:

-- mysql里这样写没问题,字符类型的status字段可以和数字比较
where status = 0

-- postgresql直接报错
error: operator does not exist: character = integer

得改成字符串比较:

where status = '0'

这其实涉及到postgresql的类型系统设计,它不像mysql那样依赖大量的隐式转换,更接近标准sql的做法。从长远来看,这种严格性能避免很多隐藏的bug,但迁移的时候确实需要花点时间排查。

我在sysmenumapper.xmlsysdeptmapper.xml这些文件里找到好几处这样的问题,都要手动改。

find_in_set的替代方案

这个函数在mysql里挺常用的,但postgresql没有。我们的代码里有好几处用到:

-- mysql的写法
where find_in_set(#{deptid}, ancestors)

得改成这样:

-- postgresql的替代方案
where (',' || ancestors || ',') like '%,' || #{deptid} || ',%'

原理就是把字符串前后加上逗号,然后用like匹配。虽然有点笨,但好用。

graph lr
    a["ancestors: '100,101,103'"] --> b["加工: ',100,101,103,'"]
    b --> c["匹配: '%,101,%'"]
    c --> d["结果: true"]

后来想了想,其实postgresql有更好的方案,可以用数组类型。把ancestors存成int[],然后直接用any操作符:

where #{deptid} = any(ancestors)

这样性能会更好,还能利用gin索引。不过这次迁移就先不动表结构了,改动太大。如果后面有性能问题,可以考虑重构这块。

date_format的转换

这个改起来稍微麻烦点:

-- mysql
where date_format(create_time, '%y%m%d') >= date_format(#{params.begintime}, '%y%m%d')

-- postgresql得这么写
where to_char(create_time, 'yyyymmdd') >= to_char(to_timestamp(#{params.begintime}, 'yyyy-mm-dd'), 'yyyymmdd')

涉及到的文件有sysusermapper.xmlsysconfigmapper.xmlsysrolemapper.xml等好几个。

database函数

-- mysql
where table_schema = (select database())

-- postgresql
where table_schema = (select current_database())

这个在gentablemapper.xmlgentablecolumnmapper.xml里用到了。

truncate table语法

postgresql的truncate需要显式指定序列重置:

-- mysql
truncate table sys_job_log

-- postgresql要加上这个选项
truncate table sys_job_log restart identity

序列同步的大坑

sql语法都改完了,以为终于可以了。结果测试插入数据的时候,又遇到一个问题:

error: duplicate key value violates unique constraint "sys_user_pkey"
详细:key (user_id)=(2) already exists.

主键冲突?但我明明是插入新数据啊。

后来才明白,从mysql迁移数据过来后,postgresql的序列(sequence)没有自动更新。比如sys_user表里已经有id到100的数据了,但序列还停留在1,所以下次插入时会尝试使用id=2,当然就冲突了。

这个问题mysql不会有,因为mysql的auto_increment是直接存在表结构里的,导入数据时会自动更新。但postgresql的序列是独立的数据库对象,和表是分离的。这种设计其实更灵活,比如多个表可以共享一个序列,但迁移时就需要手动处理这个同步问题。

这个问题要手动修复所有表的序列。我写了个sql脚本来批量处理:

do $$
declare
    r record;
    max_id bigint;
begin
    -- 遍历所有带序列的表
    for r in 
        select 
            t.tablename,
            c.column_name,
            pg_get_serial_sequence(t.schemaname || '.' || t.tablename, c.column_name) as sequence_name
        from 
            pg_tables t
            join information_schema.columns c on t.tablename = c.table_name
        where 
            t.schemaname = 'public'
            and pg_get_serial_sequence(t.schemaname || '.' || t.tablename, c.column_name) is not null
    loop
        -- 获取表的最大id
        execute format('select coalesce(max(%i), 0) from %i', r.column_name, r.tablename) into max_id;
        
        if max_id > 0 then
            -- 把序列设置为最大id
            execute format('select setval(%l, %s)', r.sequence_name, max_id);
            raise notice '已修复表 %.%, 序列: %, 当前最大id: %', 
                'public', r.tablename, r.sequence_name, max_id;
        else
            -- 如果表是空的,序列从1开始
            execute format('select setval(%l, 1, false)', r.sequence_name);
            raise notice '表 %.% 为空, 序列 % 已重置为从1开始', 
                'public', r.tablename, r.sequence_name;
        end if;
    end loop;
    
    raise notice '=== 所有序列修复完成 ===';
end $$;

这个脚本会自动找到所有带序列的表,然后把序列值设置为表中的最大id。强烈建议把这个脚本加到迁移流程里,数据导入完就立即执行。

脚本逻辑很简单:遍历所有表 → 获取最大id → 如果有数据就把序列设为最大id,没数据就重置为1。

pagehelper的配置

差点忘了,pagehelper分页插件也要配置一下:

pagehelper:
  helperdialect: postgresql  # 指定数据库方言
  supportmethodsarguments: true
  params: count=countsql

这个不改的话,分页sql会按mysql的语法生成,postgresql就不认了。

几个要注意的地方

1. 其他可能遇到的问题

上面列举的是我在迁移过程中实际碰到的问题,但不同项目可能还会遇到其他情况:

  • 字段类型映射:mysql的某些类型在postgresql中需要转换,比如text类型、datetimevstimestamp
  • 大小写敏感:postgresql默认把标识符转换为小写,如果建表时用了双引号包裹的大小写混合字段名,查询时要特别注意
  • 自增主键:postgresql使用serial类型或序列,迁移时要确保这块正确映射
  • 索引和约束:索引名称、约束可能需要调整
  • 存储过程和触发器:如果用了这些,语法差异会更大,需要重写
  • json类型:postgresql的json支持更强大,但语法不太一样

建议在实际迁移前,先搭个测试环境跑一遍,把项目特有的问题都找出来。

2. 批量修改时注意编码

用脚本批量修改文件的时候,一定要注意编码格式。我一开始用默认编码,结果把mapper文件里的中文注释全搞乱了。后来改成强制utf-8才解决:

[system.io.file]::writealltext($file.fullname, $content, [system.text.encoding]::utf8)

3. 先git提交再批量修改

批量修改之前,一定要把代码先提交到git。我第一次测试脚本的时候,因为正则写错了,把一堆文件改坏了。幸好有git,直接reset回去重来。

4. nacos配置优先级高

这个也要注意,我们项目用了nacos做配置中心。本地配置文件改了半天没生效,后来才发现nacos的配置优先级更高,得去nacos里改。

5. 逐步测试

不要一次性把所有sql都改完再测试。我的做法是改一批就编译一次:

mvn clean compile -dskiptests

这样能及时发现问题,不至于最后一堆错误不知道从哪儿排查。

完整的改动清单

最后整理了一下,这次迁移主要改了这些地方:

配置类修改:

  • nacos的druid配置:validationquery
  • pagehelper配置:helperdialect

依赖升级:

  • mybatis spring boot starter: 3.0.5
  • 处理mybatis-spring依赖冲突

sql语法替换(46个mapper文件):

  • 反引号删除
  • ifnull → coalesce
  • sysdate() → current_timestamp
  • find_in_set → like模式匹配
  • date_format → to_char
  • database() → current_database()
  • truncate table加restart identity
  • 字符类型比较加引号

序列修复:

  • 执行批量序列重置脚本

迁移流程就是:配置修改 → 依赖升级 → sql语法替换 → 序列重置 → 功能测试,发现问题就回到对应步骤修复。

一些感悟

这次迁移下来,最大的感受就是两种数据库看起来差不多,细节差异还挺多的。mysql很多地方比较"宽松",隐式类型转换、特有函数啥的用起来很方便。但postgresql更严格,该是什么类型就是什么类型,不会自动帮你转。

说实话,刚开始各种报错确实有点烦。但后来想想,postgresql这种严格性其实是好事,能在开发阶段就发现很多潜在的问题。而且postgresql在事务处理、并发控制这些方面确实做得更扎实,特别是mvcc的实现,不会有mysql那种幻读的问题(即使在可重复读级别下)。

另外就是批量处理很重要。像反引号、ifnull这种重复性高的改动,手动改肯定要疯。用脚本批量处理能省很多时间。不过前提是要先在git里提交,避免改坏了不好恢复。

还有一个就是序列问题一定要重视。这个坑比较隐蔽,如果不是测试时恰好插入数据,可能上线后才会发现。到时候就麻烦了。

后面有时间想测试一下两边的执行计划差异,尤其是那些涉及子查询和join的sql。postgresql的查询优化器和mysql不太一样,有些sql可能需要针对性地调整索引策略。这块还在持续观察中。

批量替换的正则规则

如果你要批量修改文件,可以参考这些替换规则:

# 1. 替换sysdate()
查找: sysdate\(\)
替换: current_timestamp

# 2. 替换ifnull()
查找: ifnull\(
替换: coalesce(

# 3. 移除反引号
查找: `([a-z_]+)`
替换: $1

# 4. 替换database()
查找: select database\(\)
替换: select current_database()

用你熟悉的编辑器(比如vscode、intellij idea的全局替换功能)或者写脚本批量处理都行,记得先提交git再操作。

写在最后

整个迁移过程花了一些时间,主要是在排查各种sql语法不兼容的问题。如果你也要做类似的迁移,希望这篇文章能帮你少走点弯路。

有几点建议:

  1. 提前做好兼容性检查,列个清单
  2. 批量修改前先备份(用git)
  3. 逐步测试,别一次改太多
  4. 序列问题一定要处理

这块我也还在持续优化中,比如有些地方的sql性能可能还有提升空间。如果你在迁移过程中遇到了其他问题,或者有更好的解决方案,欢迎在评论区交流讨论!

另外,如果你对postgresql的一些高级特性感兴趣(比如json类型、全文搜索、分区表等),或者想了解更多数据库迁移的经验,也可以留言告诉我,我后面可以再写几篇详细的文章。

大家在用ruoyi的时候还遇到过哪些坑?一起来吐槽一下吧 😄

到此这篇关于ruoyi从mysql迁移到postgresql数据库的踩坑实录的文章就介绍到这了,更多相关ruoyi从mysql迁移postgresql内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2026  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com