H2 数据库到 MySQL 数据迁移

H2 数据库到 MySQL 数据迁移

概述

将某后端项目从 H2 数据库迁移到 MySQL 8.0。

迁移流程

第一步:配置修改

1.1 添加 MySQL 依赖 (pom.xml)

dev profile 中同时保留 H2 和 MySQL 依赖:

xml 复制代码
<profile>
  <id>dev</id>
  <dependencies>
    <dependency>
      <groupId>com.h2database</groupId>
      <artifactId>h2</artifactId>
      <scope>runtime</scope>
    </dependency>
    <dependency>
      <groupId>com.mysql</groupId>
      <artifactId>mysql-connector-j</artifactId>
      <scope>runtime</scope>
    </dependency>
  </dependencies>
</profile>
1.2 修改配置文件 (application-dev.yml)
yaml 复制代码
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/c3s_dev?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
    username: root
    password: root
    driver-class-name: com.mysql.cj.jdbc.Driver
  jpa:
    hibernate:
      ddl-auto: update
    properties:
      hibernate:
        dialect: org.hibernate.dialect.MySQLDialect

第二步:创建 MySQL 数据库

sql 复制代码
CREATE DATABASE YOUR_DATABASE_NAME CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

第三步:启动应用创建表结构

在 IntelliJ IDEA 中启动应用,JPA 会自动在 MySQL 中创建所有表。

第四步:从 H2 导出数据

4.1 临时切换回 H2 配置

application-dev.yml 临时改回 H2 配置:

yaml 复制代码
spring:
  datasource:
    url: jdbc:h2:file:./data/YOUR_DATABASE_NAME;DB_CLOSE_DELAY=-1
  h2:
    console:
      enabled: true
      path: /h2-console
4.2 启动应用并访问 H2 Console
4.3 导出数据

在 H2 Console 中执行:

sql 复制代码
SCRIPT TO 'C:\code\backend\backend\data\h2_export.sql';

第五步:转换 H2 导出文件

H2 导出的 SQL 使用 U&'\xxxx' 语法表示 Unicode 字符,需要转换为 UTF-8。

5.1 转换 Unicode (docs/convert-final.py)
python 复制代码
#!/usr/bin/env python3
with open('data/h2_export.sql', 'rb') as f:
    content = f.read()

result = bytearray()
i = 0
while i < len(content):
    if i + 2 < len(content) and content[i] == 0x55 and content[i+1] == 0x26 and content[i+2] == 0x27:
        j = i + 3
        result.append(0x27)
        while j < len(content):
            if content[j] == 0x27:
                break
            elif content[j] == 0x5C and j + 4 < len(content):
                hex_bytes = content[j+1:j+5]
                try:
                    codepoint = int(hex_bytes, 16)
                    result.extend(chr(codepoint).encode('utf-8'))
                    j += 5
                    continue
                except (ValueError, OverflowError):
                    result.append(content[j])
                    j += 1
            else:
                result.append(content[j])
                j += 1
        result.append(0x27)
        i = j + 1
    else:
        result.append(content[i])
        i += 1

with open('data/h2_export_final.sql', 'wb') as f:
    f.write(bytes(result))

运行:python docs/convert-final.py

5.2 提取 INSERT 语句 (docs/extract-inserts.py)
python 复制代码
#!/usr/bin/env python3
import re

with open('data/h2_export_final.sql', 'r', encoding='utf-8') as f:
    content = f.read()

# 从 CREATE TABLE 提取列名
table_columns = {}
create_pattern = r'CREATE CACHED TABLE "PUBLIC"\."(\w+)"\(\s*(.*?)\);'
for match in re.finditer(create_pattern, content, re.DOTALL):
    table_name = match.group(1)
    cols_text = match.group(2)
    columns = []
    for line in cols_text.strip().split('\n'):
        line = line.strip()
        if line and not line.startswith('ALTER') and not line.startswith('--'):
            col_name = line.split()[0].strip('"')
            columns.append(col_name)
    table_columns[table_name] = columns

# 提取 INSERT 语句
insert_pattern = r'INSERT INTO "PUBLIC"\."(\w+)" VALUES\s*\n?(.*?);'
matches = re.findall(insert_pattern, content, re.DOTALL)

output_lines = ['SET NAMES utf8mb4;', 'SET CHARACTER SET utf8mb4;', 'SET FOREIGN_KEY_CHECKS=0;', '']

for table_name, values in matches:
    clean_values = values.strip()
    clean_values = re.sub(r"TIMESTAMP '([^']+)'", r"'\1'", clean_values)
    clean_values = re.sub(r"DATE '([^']+)'", r"'\1'", clean_values)
    clean_values = clean_values.replace('"PUBLIC".', '')
    clean_values = clean_values.replace('TRUE', '1').replace('FALSE', '0')

    if table_name in table_columns:
        cols = ', '.join([f'`{c}`' for c in table_columns[table_name]])
        output_lines.append(f'INSERT INTO `{table_name}` ({cols}) VALUES')
    else:
        output_lines.append(f'INSERT INTO `{table_name}` VALUES')
    output_lines.append(clean_values + ';')
    output_lines.append('')

output_lines.append('SET FOREIGN_KEY_CHECKS=1;')

with open('data/h2_mysql_import.sql', 'w', encoding='utf-8') as f:
    f.write('\n'.join(output_lines))

运行:python docs/extract-inserts.py

第六步:处理 MySQL 兼容性问题

6.1 添加缺失列

H2 和 MySQL 表结构可能有差异,需要手动添加缺失列:

sql 复制代码
-- 某表的 system 是 MySQL 保留字,改为 sys_source
-- 按需创建
6.2 修改保留字

XXX.ktsystem 改为 sys_source

kotlin 复制代码
@Column(name = "sys_source")
var system: String? = null

第七步:逐表导入数据

将导入文件按表拆分后逐个导入:

bash 复制代码
# 拆分(docs/extract-inserts.py 已自动拆分到 data/sql/ 目录)
# 逐表导入
for table in YOUR_TABLE_NAME1、YOUR_TABLE_NAME2 ...; do
    mysql -u root -proot c3s_dev < "data/sql/${table}.sql"
done

第八步:恢复 MySQL 配置

application-dev.yml 恢复为 MySQL 配置,重启应用。


脚本清单

脚本 用途
docs/convert-final.py 将 H2 的 U& 语法转换为 UTF-8
docs/extract-inserts.py 从转换后的 SQL 提取 INSERT 语句并添加列名

产物清单

文件 说明
data/h2_export.sql H2 原始导出
data/h2_export_final.sql Unicode 转换后的 SQL
data/h2_mysql_import.sql 最终 MySQL 导入文件
data/sql/*.sql 按表拆分的导入文件

注意事项

  1. 列顺序:H2 和 MySQL 的列顺序可能不同,INSERT 必须指定列名
  2. 保留字system 是 MySQL 保留字,需要改名或加反引号
  3. NOT NULL:H2 允许某些列为 NULL,MySQL 不允许,需要调整表结构
  4. Unicode :H2 使用 U&'\xxxx' 语法,需要转换为 UTF-8
  5. BOOLEAN :H2 的 TRUE/FALSE 需转换为 MySQL 的 1/0
  6. TIMESTAMP/DATE :H2 的 TIMESTAMP 'xxx' 需转换为 'xxx'
相关推荐
小白勇闯网安圈3 小时前
Django 响应对象、文件上传与类视图
数据库·django·sqlite
努力的小雨4 小时前
同一份 SQL 跑多套环境:Ksql 变量怎么用才安全
数据库
嘻哈∠※4 小时前
0061基于 SpringBoot 的投稿与稿件处理系统设计与实现
java·spring boot·后端
科技绘图4 小时前
快鲸GEO vs 传统AI搜索优化:全链路自动化与高效内容生产在转化闭环上的对比
数据库·人工智能·自动化
ltl4 小时前
数据库作为 LLM 记忆体:语义缓存、RAG 与一致性
数据库
ltl4 小时前
WAL 与崩溃恢复:ARIES 协议怎么跑通
数据库
ltl5 小时前
TEE 数据库:EnclaveDB、Oblivious 原语与机密 SQL
数据库
白狐_7985 小时前
408数据结构第8章:排序②——性质对比秒杀、场景选择与外部排序
java·数据结构·算法
zander2585 小时前
LeetCode 84:柱状图中的最大矩形——单调栈如何确定左右边界
java·数据结构·算法
码匠许师傅5 小时前
【C++ 面试真题】聊聊 C++ 的拷贝构造与拷贝赋值
java·c++·面试