Go 项目 SQLite → 南大通用 GBase 8c 适配全记录(适配完成后由claude整理生成文档,以后若再适配直接上下文喂给他即可)
Laiyong Wang Lv6

Go 项目 SQLite → 南大通用 GBase 8c 适配全记录

适配背景:原项目使用 SQLite + GORM,现需要在不改动核心业务逻辑的前提下,同时支持 SQLite 和 GBase 8c(南大通用数据库)。


一、方案选型

核心思路:一套代码,两套 SQL 文件

方案 说明 选择
共用一套 SQL 改造所有 DDL/DML 语法兼容两种数据库 ❌ 差异过大,维护成本高
两套 SQL + 一套 GORM SQLite 和 GBase 各自一套初始化/迁移 SQL,GORM 层通过驱动切换自动兼容 ✅ 采用
1
2
3
4
5
6
db/migration/rawsql/
├── init.sql # SQLite 初始化
├── issu/ # SQLite 增量迁移
└── gbase/
├── init.sql # GBase 初始化
└── issu/ # GBase 增量迁移

配置文件中通过 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
2
3
4
5
6
7
-- 创建数据库(必须在 gbasedbt 超级用户下执行)
CREATE DATABASE crypto_evaluate DBCOMPATIBILITY = 'B';
ALTER DATABASE crypto_evaluate OWNER TO gwa;

-- 切换到目标库后授权
\c crypto_evaluate
GRANT ALL ON SCHEMA public TO gwa;

三、SQL 方言差异与改写

3.1 类型映射

SQLite 类型 GBase B 模式对应类型 说明
INTEGER PRIMARY KEY AUTOINCREMENT BIGSERIAL PRIMARY KEY 自增主键
DATETIME TIMESTAMPTEXT 取决于 Go 字段类型(见下文)
TEXT TEXT 相同
REAL FLOAT8 相同
BLOB BYTEA 二进制
VARCHAR(n) VARCHAR(n) 相同,注意长度是否够用

3.2 关键词冲突(B 模式是 MySQL 兼容)

GBase B 模式保留了 MySQL 关键字,列名与关键字重名时必须加双引号:

1
2
3
4
5
-- ❌ 错误(key 是 MySQL 保留字)
CREATE TABLE sys_config (key VARCHAR(64), value TEXT);

-- ✅ 正确
CREATE TABLE sys_config ("key" VARCHAR(64), value TEXT);

常见冲突关键字:keyvaluestatuslevelrankorder 等。

3.3 NOT NULL 与空字符串

GBase A 模式下 '' 被视为 NULL,在 A 模式中对允许为空的字段不要加 NOT NULL。B 模式相对宽松,但仍需注意业务数据中是否存在空字符串写入 NOT NULL 字段的情况。

1
2
3
4
5
-- GBase B 模式 init.sql:去掉 NOT NULL 以防 '' 被当作 NULL 触发约束
CREATE TABLE sys_config (
"key" VARCHAR(64) PRIMARY KEY,
value TEXT -- 不加 NOT NULL
);

3.4 DDL 幂等性

GBase 不支持 SQLite 的 CREATE TABLE IF NOT EXISTS ... ON CONFLICT IGNORE 风格简写,增量迁移 SQL 需要显式处理幂等:

1
2
3
4
5
6
7
8
9
10
11
12
-- 列不存在时才添加
ALTER TABLE task ADD COLUMN IF NOT EXISTS assets_keys TEXT;

-- 索引不存在时才创建
CREATE INDEX IF NOT EXISTS idx_team_name ON project(team_name);

-- 列存在时才删除
ALTER TABLE project_report DROP COLUMN IF EXISTS relative_path;

-- INSERT 幂等
INSERT INTO db_version (version) VALUES ('1.0.2.1')
ON CONFLICT (version) DO NOTHING;

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
2
3
4
5
-- ❌ 错误:Go 字段是 string,列类型却是 TIMESTAMP
sync_time TIMESTAMP,

-- ✅ 正确
sync_time TEXT,

3.6 VARCHAR 长度溢出

SQLite 的 VARCHAR(n) 超长不报错(自动截断或忽略),GBase 严格执行长度限制:

1
ERROR: value too long for type character varying(128)

解决方案:审查可能存放 JSON 数组、长路径等的 VARCHAR 字段,按需改为 TEXT

1
2
-- init.sql 中改为 TEXT
assets_keys TEXT,

同时对已有数据库需要执行 ALTER 补丁:

1
2
-- fix_version_data.go 中运行
ALTER TABLE task ALTER COLUMN assets_keys TYPE TEXT;

四、GORM 兼容性问题

4.1 驱动替换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// go.mod 新增
require gorm.io/driver/postgres v1.5.9

// initialize/gorm_gbase.go
import "gorm.io/driver/postgres"

db, err := gorm.Open(
postgres.New(postgres.Config{
DSN: s.Dsn(),
PreferSimpleProtocol: true, // GBase 需要
WithoutReturning: true, // GBase B 模式不支持 RETURNING
}),
gormCfg,
)

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
2
3
4
5
db.Callback().Create().After("gorm:create").Register("gbase:clear_lastinsertid_error", func(db *gorm.DB) {
if db.Error != nil && strings.Contains(db.Error.Error(), "LastInsertId is not supported") {
db.Error = nil
}
})

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// ❌ GORM v1 写法,v2 下可能推断失败
type SysConfig struct {
Key string `gorm:"column:key;primary_key"`
}

// ✅ 显式声明,v2 兼容
type SysConfig struct {
Key string `gorm:"column:key;primaryKey"`
}

// 以 md5 作为冲突目标的 upsert
type Certificate struct {
Md5 string `gorm:"column:md5;primaryKey"`
}

type PcapStreamTls struct {
PcapStreamId int64 `gorm:"column:pcap_stream_id;primaryKey"`
}

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
2
3
4
5
6
7
8
9
10
11
// project_import.go
oProj, err := p.Detail(all.Project.Id)
// ...

if oProj.Id != 0 {
// 已存在:UPDATE
err = global.Db.Save(all.Project).Error
} else {
// 不存在:INSERT
err = global.Db.Create(all.Project).Error
}

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
2
3
4
5
6
7
8
9
10
11
gormCfg := Gorm.Config(s.Prefix, s.Singular, s.LogMode)
gormCfg.SkipDefaultTransaction = true // ← 关键

db, err := gorm.Open(
postgres.New(postgres.Config{
DSN: s.Dsn(),
PreferSimpleProtocol: true,
WithoutReturning: true,
}),
gormCfg,
)

⚠️ 注意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 EXISTSON CONFLICT DO NOTHING),因为在测试阶段可能多次重建库并重跑迁移。

5.3 迁移执行方式

迁移器对 GBase 采用逐语句执行方式,避免 GBase 不支持多语句批量执行的问题:

1
2
3
4
5
6
7
8
9
10
// migrator.go - 逐条执行 SQL 语句
func gbaseExecSQL(db *gorm.DB, sqlContent string) error {
stmts := splitSQL(sqlContent)
for i, stmt := range stmts {
if err := db.Exec(stmt).Error; err != nil {
return fmt.Errorf("stmt[%d] failed: %w", i, err)
}
}
return nil
}

六、数据库权限配置

1
2
3
4
5
6
7
-- 以 gbasedbt 超级用户连接
CREATE DATABASE crypto_evaluate DBCOMPATIBILITY = 'B';
ALTER DATABASE crypto_evaluate OWNER TO gwa;

\c crypto_evaluate

GRANT ALL ON SCHEMA public TO gwa;

若使用 A 模式(Oracle 兼容)连接会报:
FATAL: Please use the original role to connect A-compatibility database first
这是因为 A 模式要求使用创建该数据库的超级用户连接,应改用 B 模式(DBCOMPATIBILITY = 'B')。


七、完整配置示例

1
2
3
4
5
6
7
8
9
10
11
12
13
gbase:
host: 192.168.100.246
port: 5432
database: crypto_evaluate
username: gwa
password: your_password
sslmode: disable
timezone: Asia/Shanghai
max-idle-conns: 10
max-open-conns: 100
log-mode: "warn" # 生产建议 warn,调试用 info
singular: false
sql-file-dir: ./db/migration/rawsql/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()

九、关键结论

  1. GBase B 模式 + pgx 驱动下,禁用 GORM 自动事务是必须的SkipDefaultTransaction: true),否则所有写操作静默失败。

  2. WithoutReturning: true 是必须的,GBase B 模式不支持 INSERT ... ON CONFLICT ... RETURNING

  3. PreferSimpleProtocol: true 是必须的,GBase 8c 的 PostgreSQL 兼容层不完全支持 pgx 的扩展查询协议。

  4. GORM v2 的 Save() 不是 upsert,对非零主键只生成 UPDATE。需要 upsert 语义时,应显式判断后分别调用 Create()Updates()

  5. SQL 文件中所有 DDL/DML 操作必须幂等,增量迁移在测试过程中会被多次执行。

  6. Go struct 的字段类型必须与 GBase 列类型严格对齐,GBase 不做隐式类型转换(如 string → TIMESTAMP)。