2026-04-28 20:23:33 +08:00
---
tags: [GORM, Go, ORM, MySQL, PostgreSQL, SQLite, SQL Server]
create time: 2026-04-28 00:00
---
# 多数据库支持
## 概述
2026-04-28 20:56:51 +08:00
GORM 采用「驱动适配器(Driver Adapter)」架构——你写一份 GORM 代码,通过切换 `gorm.Open()` 的 dialector 就能连接不同的数据库后端。**理想情况下**所有操作都应该无缝兼容,但现实是:**不同数据库的方言差异**会让某些功能表现出微妙甚至明显的行为不同。
> [!question] 核心问题
> 如果代码要在三种数据库上都能跑,那是不是意味着每个查询都要写三遍?答案当然是否定的——GORM 的优势恰恰在于"一次编写,多处运行"。那么问题来了:**哪些操作能保证一致?哪些地方需要特别注意?** 本章就是为了解答这些问题。
2026-04-28 20:23:33 +08:00
```mermaid
flowchart TD
Code["你的 GORM 代码"] --> Adapter{"Driver Adapter"}
Adapter --> |mysql| MySQL["MySQL / MariaDB"]
Adapter --> |postgres| PG["PostgreSQL"]
Adapter --> |sqlite| SQLite["SQLite"]
Adapter --> |mssql| MSSQL["SQL Server"]
style Code fill:#4FC08D,color:#fff
style Adapter fill:#EAB308,color:#fff
style MySQL fill:#3B82F6,color:#fff
style PG fill:#8B5CF6,color:#fff
style SQLite fill:#A0AEC0,color:#fff
style MSSQL fill:#F59E0B,color:#000
```
2026-04-28 20:56:51 +08:00
> [!tip] 驱动适配器的本质
> GORM 内部会为每种数据库维护一个 **dialector(方言鉴别器)**。调用 `Create`、`Find` 等操作时,dialector 会负责将 GORM 的中间表达式翻译成对应数据库的 SQL 方言。理解这一点就能明白:为什么同样的 `db.Limit(10)` 在不同数据库中生成的 SQL 完全不同。
2026-04-28 20:23:33 +08:00
## 驱动安装与初始化
### MySQL / MariaDB
```go
2026-04-28 20:56:51 +08:00
import "gorm.io/driver/mysql"
2026-04-28 20:23:33 +08:00
dsn := "user:password@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local"
db , err := gorm . Open ( mysql . Open ( dsn ), & gorm . Config {})
2026-04-28 20:56:51 +08:00
// 注意:MySQL 是最常见的选择,DSN 参数也是最多的
2026-04-28 20:23:33 +08:00
```
2026-04-28 20:56:51 +08:00
**DSN 必配参数详解** :
2026-04-28 20:23:33 +08:00
2026-04-28 20:56:51 +08:00
| 参数 | 优先级 | 推荐值 | 说明 |
|------|--------|--------|------|
| `charset` | ⭐⭐⭐ | `utf8mb4` | 必须用 `utf8mb4` ,传统的 `utf8` 在 MySQL 中只支持 3 字节,无法存储 emoji |
| `parseTime` | ⭐⭐⭐ | `True` | 自动把数据库的 datetime 转成 Go `time.Time` ,否则你会拿到 `driver.Value` |
| `loc` | ⭐⭐⭐ | `Local` 或 `Asia/Shanghai` | 解决北京时间偏移问题;不设置的话可能拿到 UTC 时间 |
| `timeout` | ⭐⭐ | `30s` | TCP 握手阶段超时 |
| `readTimeout` | ⭐⭐ | `30s` | 从服务器读取响应超时 |
| `writeTimeout` | ⭐⭐ | `30s` | 向服务器发送请求超时 |
> [!important] utf8 vs utf8mb4 常见陷阱
> 很多开发者不知道 MySQL 的 `utf8` 实际上是 "utf8mb3"——它最多支持 3 字节的字符。遇到 emoji、生僻汉字时会直接报错。这就是为什么 DSN 里必须明确指定 `utf8mb4`(真正的全量 UTF-8)。
>
> ```go
> // 如果你在代码中也设置了 CharacterSet,别忘了:
> db.Set("gorm:table_options", "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4")
> ```
2026-04-28 20:23:33 +08:00
### PostgreSQL
```go
2026-04-28 20:56:51 +08:00
import "gorm.io/driver/postgres"
2026-04-28 20:23:33 +08:00
dsn := "host=localhost user=gorm password=gorm dbname=gorm port=5432 sslmode=require TimeZone=Asia/Shanghai"
db , err := gorm . Open ( postgres . Open ( dsn ), & gorm . Config {})
2026-04-28 20:56:51 +08:00
// PostgreSQL 使用标准的 Connection String 格式
2026-04-28 20:23:33 +08:00
```
**PostgreSQL 特有注意** :
2026-04-28 20:56:51 +08:00
- `sslmode` 在开发环境可设为 `disable` ,生产环境保持 `require` (最低要求)或 `verify-full` (最强校验)
- `TimeZone` 要与服务端一致,否则 `time.Time` 字段会出现几小时的偏移
- PG 对大小写敏感:未加引号的标识符会自动转为小写,这意味着 struct 字段名映射时要格外小心
> [!warning] PostgreSQL 的大写陷阱
> 如果你在建表时用的是双引号 `"UserName"`,那后续所有查询都必须也带上双引号,否则 PG 会去找 `username`(小写)。**建议:建表一律用小写,避免此坑。**
2026-04-28 20:23:33 +08:00
### SQLite
```go
2026-04-28 20:56:51 +08:00
import "gorm.io/driver/sqlite"
2026-04-28 20:23:33 +08:00
db , err := gorm . Open ( sqlite . Open ( "test.db" ), & gorm . Config {})
2026-04-28 20:56:51 +08:00
// 最简单的一种——只需要一个文件路径
2026-04-28 20:23:33 +08:00
```
**SQLite 注意事项** :
2026-04-28 20:56:51 +08:00
- 默认是文件级排他锁,并发写入会有阻塞
- 高并发场景使用 WAL + 共享缓存模式:`file:test.db?cache=shared&_journal=WAL`
- **不支持外键约束**——`AutoMigrate` 不会创建外键,需要在原始 SQL 中手动添加
- 所有列本质上都是「无类型」的(type affinity),GORM 的类型映射在某些边界情况下可能有意外表现
> [!tip] SQLite 开启 WAL 模式
> WAL( Write-Ahead Logging)允许读写并发,性能提升显著:
> ```go
> db, _ := gorm.Open(sqlite.Open("file:test.db?cache=shared&_journal=WAL&_timeout=5000"), &gorm.Config{})
> // cache=shared:允许多个连接共享缓存
> // _journal=WAL:启用预写日志
> // _timeout:锁定等待超时(毫秒)
> ```
2026-04-28 20:23:33 +08:00
### SQL Server
```go
2026-04-28 20:56:51 +08:00
import "gorm.io/driver/mssql"
2026-04-28 20:23:33 +08:00
2026-04-28 20:56:51 +08:00
dsn := "sqlserver://user:password@host:1433?database=dbname&encrypt=disable"
2026-04-28 20:23:33 +08:00
db , err := gorm . Open ( mssql . Open ( dsn ), & gorm . Config {})
```
2026-04-28 20:56:51 +08:00
**SQL Server 特有注意** :
- 连接字符串使用 URI 格式,与其他驱动风格不同
- SQL Server 默认强制 TLS 加密,开发环境下需加 `encrypt=disable`
- 数据类型和自增逻辑与 MySQL/PG 都不同(详见下方方言差异表)
2026-04-28 20:23:33 +08:00
## 方言差异速查表
2026-04-28 20:56:51 +08:00
这是跨数据库迁移时最容易踩坑的部分。下面这张表汇总了最常见的差异场景:
2026-04-28 20:23:33 +08:00
| 特性 | MySQL | PostgreSQL | SQLite | SQL Server |
|------|-------|-----------|--------|------------|
| `time.Time` | DATETIME / TIMESTAMP | TIMESTAMPTZ | TEXT(序列化后) | DATETIME2 |
| `string` | VARCHAR(n) | VARCHAR / TEXT | TEXT | NVARCHAR(n) |
| `uint` | INT UNSIGNED | BIGINT | INTEGER | INT |
| `float64` | DOUBLE | DOUBLE PRECISION | REAL | FLOAT |
| 自增主键 | AUTO_INCREMENT | SERIAL / BIGSERIAL | AUTOINCREMENT | IDENTITY(1,1) |
| NOW() 函数 | NOW() | NOW() | DATETIME('now') | GETUTCDATE() |
| LIMIT 语法 | LIMIT N OFFSET M | LIMIT N OFFSET M | LIMIT N OFFSET M | TOP N / OFFSET FETCH |
2026-04-28 20:56:51 +08:00
| 软删除索引 | ⚠️ InnoDB 唯一约束 Bug | ✅ 正常 | ✅ 正常 | ✅ 正常 |
| ON DELETE | CASCADE / SET NULL 等 | CASCADE / SET NULL 等 | ❌ 不支持 | CASCADE / SET NULL 等 |
> [!question] 为什么 uint 在 PG 中变成 BIGINT?
> Go 的 `uint` 在 64 位系统上是 8 字节(与 `int64` 同宽),而 MySQL 有独立的 `INT UNSIGNED` 正好对齐。但 PostgreSQL 没有无符号整数类型,所以 GORM 将其映射为最大的匹配类型 `BIGINT`。这就引出了一个问题:**如果你的代码中写了 `uint`,迁移到 PG 后会不会出现溢出?** 答案是:只要数据范围在 BIGINT 内就没问题,但如果业务依赖的是 32 位上限,就需要改用 `uint32`。
2026-04-28 20:23:33 +08:00
2026-04-28 20:56:51 +08:00
### CREATE TABLE 实际输出对比
2026-04-28 20:23:33 +08:00
2026-04-28 20:56:51 +08:00
同一个 struct 在不同数据库中生成的建表语句差异很大:
2026-04-28 20:23:33 +08:00
```go
type User struct {
ID uint `gorm:"primaryKey;autoIncrement"`
Name string `gorm:"size:64;not null"`
Age int `gorm:"not null;default:0"`
CreatedAt time . Time `gorm:"autoTime"`
}
```
| 数据库 | 实际建表语句摘要 |
|--------|----------------|
| MySQL | `id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(64) NOT NULL, age INT NOT NULL DEFAULT 0, created_at DATETIME` |
| PostgreSQL | `id BIGSERIAL PRIMARY KEY, name VARCHAR(64) NOT NULL, age INT NOT NULL DEFAULT 0, created_at TIMESTAMP WITH TIME ZONE` |
| SQLite | `id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, age INTEGER NOT NULL DEFAULT 0, created_at TEXT` |
2026-04-28 20:56:51 +08:00
> [!warning] autoTime 的隐藏机制
> `autoTime` tag 在 SQLite 中的表现尤其值得注意:因为 SQLite 没有原生的 DATETIME 类型,GORM 会把 `time.Time` 序列化为 RFC 3339 格式的字符串存入 TEXT 列。读出来的时候再反序列化回来。这意味着你在 SQLite 中无法使用数据库层面的时间函数(如 `WHERE created_at > '2024-01-01'` 来利用索引)。
2026-04-28 20:23:33 +08:00
2026-04-28 20:56:51 +08:00
## AutoMigrate 跨数据库的注意事项
2026-04-28 20:23:33 +08:00
```go
2026-04-28 20:56:51 +08:00
// MySQL 下执行迁移
2026-04-28 20:23:33 +08:00
err := db . AutoMigrate ( & User {}, & Order {})
2026-04-28 20:56:51 +08:00
// PostgreSQL 下执行同样的迁移
2026-04-28 20:23:33 +08:00
pgDB , _ := gorm . Open ( postgres . Open ( pgDSN ), & gorm . Config {})
err = pgDB . AutoMigrate ( & User {}, & Order {})
// 注意:同样一个 User struct,在 PG 中会自动用 BIGSERIAL 而非 AUTO_INCREMENT
```
2026-04-28 20:56:51 +08:00
> [!warning] AutoMigrate 的本质局限
> `AutoMigrate` 的设计原则是 **"只加不改"**——它能新增表和新增列,但**无法修改已有列的结构**(比如改列名、改类型、改约束)。这在任何数据库中都一样,但在 SQLite 下表现得最极端:SQLite 根本不支持 ALTER TABLE 的大部分操作,所以 AutoMigrate 实际上只能新建一张表然后把旧数据搬过来。
2026-04-28 20:23:33 +08:00
>
2026-04-28 20:56:51 +08:00
> **最佳实践**:使用专门的迁移工具(Goose、golang-migrate、Migration),手写版本化的 SQL 脚本。这样你可以精确控制每个版本的迁移和回滚,而不是依赖 AutoMigrate 的黑盒行为。
2026-04-28 20:23:33 +08:00
## 查询语法差异
2026-04-28 20:56:51 +08:00
### LIMIT / OFFSET 分页
2026-04-28 20:23:33 +08:00
```go
2026-04-28 20:56:51 +08:00
// 标准分页写法——GORM 会自动处理底层方言差异
2026-04-28 20:23:33 +08:00
db . Limit ( 10 ). Offset ( 20 ). Find ( & users )
2026-04-28 20:56:51 +08:00
// 底层翻译后的 SQL:
// MySQL: SELECT * FROM users LIMIT 10 OFFSET 20
// PostgreSQL: SELECT * FROM users LIMIT 10 OFFSET 20
// SQLite: SELECT * FROM users LIMIT 10 OFFSET 20
// SQL Server: SELECT * FROM users ORDER BY id OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY
```
> [!important] SQL Server 分页的性能陷阱
> 老版本的 GORM 驱动会把 SQL Server 的分页转换为子查询方案(`TOP N WHERE id NOT IN (SELECT TOP M ...)`),在大数据量下性能较差。升级到最新版驱动后已改用 `OFFSET FETCH` 标准语法。升级前务必确认你用的驱动版本 ≥ v1.5.0。
### UUID 与原生函数
2026-04-28 20:23:33 +08:00
2026-04-28 20:56:51 +08:00
不同数据库生成 UUID 的方式完全不同,GORM 只能做基本映射,具体的默认值必须由你指定:
```go
2026-04-28 20:23:33 +08:00
type Order struct {
2026-04-28 20:56:51 +08:00
ID uuid . UUID `gorm:"type:uuid;default:gen_random_uuid()"` // PostgreSQL:内置函数 gen_random_uuid()
// MySQL 要用 type:char(36);default:(UUID())
// SQL Server 要用 type:uniqueidentifier;default:newid()
// SQLite 需要用 Hook 或在 Go 层生成
2026-04-28 20:23:33 +08:00
}
```
2026-04-28 20:56:51 +08:00
> [!tip] SQLite 下的 UUID 生成方案
> 由于 SQLite 没有内置 UUID 函数,推荐的两种方式:
> 1. **Go 层生成**:在 `BeforeCreate` Hook 中用 `github.com/google/uuid` 生成
> 2. **触发器**:用 SQLite 的 BEFORE INSERT TRIGGER 自动生成
>
> ```go
> func (o *Order) BeforeCreate(tx *gorm.DB) error {
> if o.ID == uuid.Nil {
> o.ID = uuid.New()
> }
> return nil
> }
> ```
2026-04-28 20:23:33 +08:00
2026-04-28 20:56:51 +08:00
### 软删除与唯一索引
这是最隐蔽的一个坑——MySQL InnoDB 引擎在处理软删除(`DeletedAt` 字段)时,**唯一约束不会自动排除已软删除的行**。换句话说:
```go
type Product struct {
gorm . Model
SKU string `gorm:"uniqueIndex"`
}
// 假设 SKU="ABC" 的记录被软删除了
// 此时你还能再次插入 SKU="ABC" 的新记录吗?
// MySQL: ✅ 可以(唯一的 bug 行为——软删除行不参与唯一约束检查)
// PG: ❌ 不可以(正确行为——唯一约束包含软删除行)
```
> [!danger] MySQL 软删除唯一约束 Bug
> 如果你用了软删除 + 唯一索引,在 MySQL 下可能出现"同一 SKU 多条有效记录"的数据不一致问题。
>
> **解决方案**:
> 1. 用复合唯一索引:`uniqueIndex:idx_sku_active`,并在查询时总是带上 `DeletedAt` 条件
> 2. 或者换用 PG/SQL Server——它们的行为是正确的
## 高级用法
### 多数据库连接管理
生产环境中通常需要同时连接多个数据库(比如 MySQL 存业务数据、Redis 做缓存、ES 做搜索),以下是常见模式:
```go
var (
masterDB * gorm . DB // 主库
slaveDB * gorm . DB // 从库 / 其他数据库
)
func initMultiDB () error {
var err error
masterDB , err = gorm . Open ( mysql . Open ( os . Getenv ( "MASTER_DSN" )), & gorm . Config {
Logger : logger . Default . LogMode ( logger . Info ),
})
if err != nil {
return fmt . Errorf ( "master db: %w" , err )
}
slaveDB , err = gorm . Open ( postgres . Open ( os . Getenv ( "SLAVE_DSN" )), & gorm . Config {
Logger : logger . Default . LogMode ( logger . Silent ),
})
if err != nil {
return fmt . Errorf ( "slave db: %w" , err )
}
// 配置连接池(两种数据库都可以用 DB.SqlDB() 访问底层 *sql.DB)
sqlDB , _ := masterDB . DB ()
sqlDB . SetMaxOpenConns ( 50 )
sqlDB . SetMaxIdleConns ( 10 )
sqlDB . SetConnMaxLifetime ( time . Hour )
return nil
}
```
> [!tip] 连接池调优经验值
> - `MaxOpenConns`:根据 QPS × 平均查询耗时估算。一般 Web 应用 20~100 足够
> - `MaxIdleConns`:设为 `MaxOpenConns` 的 10%~25%,过多空闲连接会浪费资源
> - `ConnMaxLifetime`:建议设 1 小时,避免与数据库侧的连接超时策略冲突
### 环境自适应连接
2026-04-28 20:23:33 +08:00
```go
func newDB ( env string ) ( * gorm . DB , error ) {
2026-04-28 20:56:51 +08:00
var driverFunc func ( string ) gorm . Dialector
2026-04-28 20:23:33 +08:00
switch env {
case "dev" :
2026-04-28 20:56:51 +08:00
driverFunc = func ( dsn string ) gorm . Dialector {
return sqlite . Open ( dsn )
}
default :
driverFunc = func ( dsn string ) gorm . Dialector {
return postgres . Open ( dsn )
}
2026-04-28 20:23:33 +08:00
}
2026-04-28 20:56:51 +08:00
dsn := map [ string ] string {
"dev" : "file:test_dev.db?cache=shared" ,
"staging" : os . Getenv ( "DATABASE_URL" ),
"prod" : os . Getenv ( "PROD_DATABASE_URL" ),
}[ env ]
2026-04-28 20:23:33 +08:00
2026-04-28 20:56:51 +08:00
return gorm . Open ( driverFunc ( dsn ), & gorm . Config {
Logger : ternary ( env == "prod" , logger . Default . LogMode ( logger . Silent ), logger . Default ),
2026-04-28 20:23:33 +08:00
})
}
2026-04-28 20:56:51 +08:00
func ternary [ T any ]( cond bool , a , b T ) T {
if cond { return a }; return b
}
```
### 多数据库事务
当需要在一个事务中操作多个数据库时,GORM 本身不提供分布式事务支持,但可以分别管理各自的事务:
```go
// 分别在不同的 db 实例上开启事务
tx1 := masterDB . Begin ()
tx2 := slaveDB . Begin ()
// 独立提交
if err := tx1 . Create ( & product ). Error ; err != nil {
tx1 . Rollback ()
tx2 . Rollback () // 两个都需要回滚
return err
2026-04-28 20:23:33 +08:00
}
2026-04-28 20:56:51 +08:00
if err := tx2 . Create ( & auditLog ). Error ; err != nil {
tx1 . Rollback ()
tx2 . Rollback ()
return err
}
tx1 . Commit ()
tx2 . Commit ()
2026-04-28 20:23:33 +08:00
```
2026-04-28 20:56:51 +08:00
> [!important] 跨数据库事务不是 ACID 的
> 上面的模式叫做 **"两阶段提交"的非正式实现**——本质上两个事务是独立的。如果 tx1 成功但 tx2 失败,你就有了数据不一致状态。真正的分布式事务需要使用 XA 协议或 Saga 模式,但这超出了 GORM 的能力范围。
>
> **建议**:能在一个数据库内完成的操作就不要跨库,减少一致性复杂度。
## 最佳实践总结
### 数据库选型决策矩阵
| 场景 | 推荐数据库 | 理由 |
|------|-----------|------|
| 快速原型 / CLI 工具 | SQLite | 零配置,单文件 |
| 个人项目 / 博客 | PostgreSQL | 免费、功能全、JSONB 强大 |
| 企业级业务系统 | MySQL / PostgreSQL | 社区成熟,生态丰富 |
| Windows 技术栈企业 | SQL Server | Active Directory 集成好 |
### 开发环境 vs 生产环境
2026-04-28 20:23:33 +08:00
> [!tip] 为什么推荐开发用 SQLite?
2026-04-28 20:56:51 +08:00
>
> - **零配置**:不需要启动任何数据库服务,`go run` 即可
> - **单文件**:方便分享测试数据集,CI/CD 中直接用内存数据库
> - **语法兼容性**:大部分标准 SQL 都能工作
2026-04-28 20:23:33 +08:00
>
2026-04-28 20:56:51 +08:00
> **但请记住**: SQLite **不能**替代生产数据库做压力测试——它不支持行级锁、事务隔离级别可调、复杂聚合优化器等关键特性。**开发用 SQLite 验证逻辑,上线前必须在真实数据库上做一轮完整回归测试。**
### 防坑 Checklist
在准备多数据库部署时,逐项核对:
- [ ] 所有 `time.Time` 字段的时区设置是否已明确
- [ ] 是否避免了 `uint` 类型(改用 `int` / `int64` 降低迁移风险)
- [ ] 软删除 + 唯一索引的场景是否在目标数据库上过测
- [ ] 自定义 SQL( `db.Raw()` )是否做了方言检查
- [ ] `AutoMigrate` 之外是否有版本化的迁移脚本
- [ ] 连接池参数是否针对生产环境调优
- [ ] 是否在所有目标数据库上都跑过完整的 CI 测试
2026-04-28 20:23:33 +08:00
## 关联笔记
- [[01-安装与初始化]]
- [[02-模型定义]]
- [[17-迁移工具]]