Go 项目 SQLite → 南大通用 GBase 8c 适配全记录
适配背景:原项目使用 SQLite + GORM,现需要在不改动核心业务逻辑的前提下,同时支持 SQLite 和 GBase 8c(南大通用数据库)。
一、方案选型
核心思路:一套代码,两套 SQL 文件
| 方案 | 说明 | 选择 |
|---|---|---|
| 共用一套 SQL | 改造所有 DDL/DML 语法兼容两种数据库 | ❌ 差异过大,维护成本高 |
| 两套 SQL + 一套 GORM | SQLite 和 GBase 各自一套初始化/迁移 SQL,GORM 层通过驱动切换自动兼容 | ✅ 采用 |
1 | db/migration/rawsql/ |
配置文件中通过 db-type: "gbase" 切换,GORM 驱动从 gorm.io/driver/sqlite 切换到 gorm.io/driver/postgres(GBase 兼容 PostgreSQL 协议)。
二、GBase 8c 关键信息
- 协议:兼容 PostgreSQL 协议,Go 驱动可直接使用
gorm.io/driver/postgres(pgx 底层) - 兼容模式:
DBCOMPATIBILITY 'A'(Oracle 兼容)和'B'(MySQL 兼容) - 选用 B 模式:本项目 SQLite DDL 风格更接近 MySQL,使用
DBCOMPATIBILITY = 'B'创建数据库
1 | -- 创建数据库(必须在 gbasedbt 超级用户下执行) |
三、SQL 方言差异与改写
3.1 类型映射
| SQLite 类型 | GBase B 模式对应类型 | 说明 |
|---|---|---|
INTEGER PRIMARY KEY AUTOINCREMENT |
BIGSERIAL PRIMARY KEY |
自增主键 |
DATETIME |
TIMESTAMP 或 TEXT |
取决于 Go 字段类型(见下文) |
TEXT |
TEXT |
相同 |
REAL |
FLOAT8 |
相同 |
BLOB |
BYTEA |
二进制 |
VARCHAR(n) |
VARCHAR(n) |
相同,注意长度是否够用 |
3.2 关键词冲突(B 模式是 MySQL 兼容)
GBase B 模式保留了 MySQL 关键字,列名与关键字重名时必须加双引号:
1 | -- ❌ 错误(key 是 MySQL 保留字) |
常见冲突关键字:key、value、status、level、rank、order 等。
3.3 NOT NULL 与空字符串
GBase A 模式下 '' 被视为 NULL,在 A 模式中对允许为空的字段不要加 NOT NULL。B 模式相对宽松,但仍需注意业务数据中是否存在空字符串写入 NOT NULL 字段的情况。
1 | -- GBase B 模式 init.sql:去掉 NOT NULL 以防 '' 被当作 NULL 触发约束 |
3.4 DDL 幂等性
GBase 不支持 SQLite 的 CREATE TABLE IF NOT EXISTS ... ON CONFLICT IGNORE 风格简写,增量迁移 SQL 需要显式处理幂等:
1 | -- 列不存在时才添加 |
3.5 时间类型字段的 Go ↔ GBase 类型对齐
GORM 在 GBase B 模式下写 TIMESTAMP 类型时,Go 的 string 字段会触发:
1 | ERROR: invalid input syntax for type timestamp: "" |
解决方案:让 schema 类型与 Go 字段类型严格对应:
| Go 字段类型 | GBase 列类型 |
|---|---|
time.Time |
TIMESTAMP |
string |
TEXT |
int64 |
BIGINT |
1 | -- ❌ 错误:Go 字段是 string,列类型却是 TIMESTAMP |
3.6 VARCHAR 长度溢出
SQLite 的 VARCHAR(n) 超长不报错(自动截断或忽略),GBase 严格执行长度限制:
1 | ERROR: value too long for type character varying(128) |
解决方案:审查可能存放 JSON 数组、长路径等的 VARCHAR 字段,按需改为 TEXT:
1 | -- init.sql 中改为 TEXT |
同时对已有数据库需要执行 ALTER 补丁:
1 | -- fix_version_data.go 中运行 |
四、GORM 兼容性问题
4.1 驱动替换
1 | // go.mod 新增 |
DSN 格式:
1 | postgres://gwa:password@192.168.100.246:5432/crypto_evaluate?sslmode=disable |
4.2 RETURNING 子句问题
GBase B 模式不支持 INSERT ... ON CONFLICT DO UPDATE ... RETURNING:
1 | ERROR: RETURNING clause is not yet supported within INSERT ON DUPLICATE KEY UPDATE statement. |
修复:在 postgres.Config 中设置 WithoutReturning: true。
4.3 LastInsertId 问题
设置 WithoutReturning: true 后,GORM 在 INSERT 之后会调用 LastInsertId(),pgx 驱动返回:
1 | LastInsertId is not supported by this driver |
GORM 把这个错误提升为 db.Error,导致后续操作误判失败。
修复:注册一个 After Create 回调,清除这个能力性报错:
1 | db.Callback().Create().After("gorm:create").Register("gbase:clear_lastinsertid_error", func(db *gorm.DB) { |
4.4 ON CONFLICT 需要明确冲突列
GBase 要求 ON CONFLICT DO UPDATE 必须指定冲突列或约束名:
1 | ERROR: ON CONFLICT DO UPDATE requires inference specification or constraint name |
GORM 通过 struct 的 primaryKey tag 推断冲突列。如果 struct 使用了旧写法 primary_key(GORM v1 风格),GORM v2 可能无法正确推断,导致生成不带列名的 ON CONFLICT DO UPDATE。
修复:为所有用于 upsert 的 struct 字段添加明确的 primaryKey tag:
1 | // ❌ GORM v1 写法,v2 下可能推断失败 |
4.5 Save() 与 Create() 的行为差异(核心坑)
GORM v2 中 Save() 的行为:
- 主键为零 → 走
creates()回调(INSERT) - 主键非零 → 走
updates()回调(只生成 UPDATE,不自动回退 INSERT)
在未加 WithoutReturning 之前,PostgreSQL 方言会为 Save() 注入 ON CONFLICT (pk) DO UPDATE ... RETURNING id,相当于 upsert。加了 WithoutReturning: true 后,这条 upsert 路径消失,Save() 对新记录只生成 UPDATE,匹配 0 行也不报错,数据就此丢失。
修复:在已知记录是否存在的场景下,分别使用 Create() 和 Save():
1 | // project_import.go |
4.6 显式事务不落库(最终根因)
现象:所有通过 GORM 的 Create() / Save() / Updates() 操作均返回”成功”,但数据库查询始终为空。而通过 SQL 文件(db.Exec())执行的初始化数据正常落库。
对比分析:
| 调用方式 | GORM 是否包裹显式事务 | GBase B 模式结果 |
|---|---|---|
db.Exec(sql) |
❌ 无(Raw 回调,直接自动提交) | ✅ 正常落库 |
db.Create(&model) |
✅ 有(BEGIN … COMMIT) | ❌ COMMIT 未生效,数据丢失 |
根本原因:GBase 8c B 模式(MySQL 兼容)+ pgx 驱动的 PreferSimpleProtocol: true 组合下,pgx 发出的显式 BEGIN / COMMIT 命令没有被 GBase 正确处理,导致事务内的写入被静默回滚或忽略。
修复:在 GBase 的 gorm.Config 中设置 SkipDefaultTransaction: true,让所有 CRUD 操作绕过 GORM 的自动事务包裹,与 db.Exec() 保持一致的自动提交行为:
1 | gormCfg := Gorm.Config(s.Prefix, s.Singular, s.LogMode) |
⚠️ 注意:
SkipDefaultTransaction: true会让单条 CRUD 操作失去 GORM 的自动事务保护。如有多步原子操作,需要在业务层手动使用db.Transaction()或db.Begin()/tx.Commit()。
五、数据库初始化与迁移策略
5.1 首次初始化
1 | 版本号为空(新库)→ 执行 init.sql(全量建表) → 写入最新版本号 |
5.2 增量迁移(issu)
1 | 读取 db_version → 找出未执行的版本文件 → 按序执行 issu/*.sql → 更新版本号 |
注意:GBase 版本的 issu SQL 每条语句均需幂等(IF NOT EXISTS、ON CONFLICT DO NOTHING),因为在测试阶段可能多次重建库并重跑迁移。
5.3 迁移执行方式
迁移器对 GBase 采用逐语句执行方式,避免 GBase 不支持多语句批量执行的问题:
1 | // migrator.go - 逐条执行 SQL 语句 |
六、数据库权限配置
1 | -- 以 gbasedbt 超级用户连接 |
若使用 A 模式(Oracle 兼容)连接会报:
FATAL: Please use the original role to connect A-compatibility database first
这是因为 A 模式要求使用创建该数据库的超级用户连接,应改用 B 模式(DBCOMPATIBILITY = 'B')。
七、完整配置示例
1 | gbase: |
八、错误速查表
| 错误信息 | 原因 | 修复方案 |
|---|---|---|
null value in column "value" violates not-null constraint |
GBase A 模式把 '' 当 NULL |
去掉该列的 NOT NULL,或改用 B 模式 |
ON CONFLICT DO UPDATE requires inference specification |
GORM 未推断出冲突列 | 给 struct 主键字段加 gorm:"primaryKey" tag |
FATAL: Please use the original role to connect A-compatibility database |
数据库是 A 模式但用普通用户连接 | 用 DBCOMPATIBILITY='B' 重建数据库 |
syntax error at or near "," |
列名与 MySQL 关键字冲突 | 给列名加双引号,如 "key" |
invalid input syntax for type timestamp: "" |
Go string 字段写入 TIMESTAMP 列 | 将列类型改为 TEXT |
relation "idx_xxx" already exists |
索引重复创建 | CREATE INDEX 加 IF NOT EXISTS |
duplicate key value violates unique constraint |
INSERT 重复执行 | INSERT 末尾加 ON CONFLICT DO NOTHING |
RETURNING clause is not yet supported within INSERT ON DUPLICATE KEY UPDATE |
GBase B 不支持 RETURNING | WithoutReturning: true |
LastInsertId is not supported by this driver |
pgx 不实现 LastInsertId | 注册 After Create 回调清除该错误 |
value too long for type character varying(128) |
VARCHAR 长度不足 | 将列类型改为 TEXT |
column "xxx" of relation "yyy" does not exist |
增量 DROP COLUMN 列已不存在 | DROP COLUMN 加 IF EXISTS |
empty slice found |
GORM Save 空切片 | Save 前加 if len(slice) == 0 { return } |
| CREATE/Save 成功但数据库无数据 | GBase B 模式下显式事务 COMMIT 未生效 | GORM Config 设置 SkipDefaultTransaction: true |
| Save() 新记录无数据(非主键零值) | GORM v2 Save 非零主键只走 UPDATE | 判断记录是否存在,新建用 Create(),更新用 Save() |
九、关键结论
GBase B 模式 + pgx 驱动下,禁用 GORM 自动事务是必须的(
SkipDefaultTransaction: true),否则所有写操作静默失败。WithoutReturning: true是必须的,GBase B 模式不支持INSERT ... ON CONFLICT ... RETURNING。PreferSimpleProtocol: true是必须的,GBase 8c 的 PostgreSQL 兼容层不完全支持 pgx 的扩展查询协议。GORM v2 的
Save()不是 upsert,对非零主键只生成 UPDATE。需要 upsert 语义时,应显式判断后分别调用Create()或Updates()。SQL 文件中所有 DDL/DML 操作必须幂等,增量迁移在测试过程中会被多次执行。
Go struct 的字段类型必须与 GBase 列类型严格对齐,GBase 不做隐式类型转换(如
string → TIMESTAMP)。