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
- 地址:http://localhost:30010/h2-console
- JDBC URL:
jdbc:h2:file:./data/YOUR_DATABASE_NAME;DB_CLOSE_DELAY=-1
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.kt 中 system 改为 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 |
按表拆分的导入文件 |
注意事项
- 列顺序:H2 和 MySQL 的列顺序可能不同,INSERT 必须指定列名
- 保留字 :
system是 MySQL 保留字,需要改名或加反引号 - NOT NULL:H2 允许某些列为 NULL,MySQL 不允许,需要调整表结构
- Unicode :H2 使用
U&'\xxxx'语法,需要转换为 UTF-8 - BOOLEAN :H2 的
TRUE/FALSE需转换为 MySQL 的1/0 - TIMESTAMP/DATE :H2 的
TIMESTAMP 'xxx'需转换为'xxx'