7.6 GORM 框架入门
简介
GORM 是 Go 语言中常用的数据库抽象层(DBAL),API 简洁灵活,支持 MySQL、PostgreSQL、SQLite、SQL Server 等多种数据库,并在此之上提供了模型定义、CRUD、事务、关联、钩子、自动迁移、软删除、预加载等完整能力,是目前 Go 生态中使用最广泛的 ORM 框架。
使用 GORM 时只需掌握两个核心概念:
- 模型(Model):用一个 Go 结构体对应一张数据表,字段对应列,通过操作结构体实例完成对表的增删改查;
- 数据库会话(
*gorm.DB):gorm.Open返回的会话对象,所有查询、创建、更新、删除方法都挂载在它之上。
本节仅介绍 GORM 最基础的用法;关联、预加载、钩子、复合主键、Sharding 等进阶特性请参阅 GORM 官方文档。
一、为什么使用 ORM
在前面几节中,我们使用 database/sql 搭配驱动直接操作 SQLite / MySQL,CRUD 语句完全需要手写 SQL,再将结果逐字段 Scan 到变量中。这种方式性能好、透明度高,但在业务复杂度上升之后,常常会出现几个问题:
- SQL 字符串散落在各处,字段增减时容易漏改;
rows.Scan的字段顺序必须与 SQL 严格一致,维护成本高;- 重复代码多,建连接、
Prepare、遍历Rows、错误处理的样板很冗长; - 跨数据库(SQLite、MySQL、PostgreSQL)迁移时,方言差异需要手动适配。
ORM 用结构体表达表、用对象表达行,使开发者可以以面向对象的方式操作数据库。以"插入一条用户记录"为例,对比两种写法:
原生 database/sql:
stmt, err := db.Prepare("INSERT INTO users(name, age) VALUES (?, ?)")
if err != nil {
return err
}
defer stmt.Close()
res, err := stmt.Exec("John", 30)
if err != nil {
return err
}
id, _ := res.LastInsertId()GORM:
user := User{Name: "John", Age: 30}
err := db.Create(&user).Error
// user.ID 会自动回填ORM 带来的主要价值:
- 以结构体为中心:模型即表,字段即列,IDE 补全友好,重构安全。
- 样板代码少:不再手写
Prepare、Scan、Rows遍历。 - 跨数据库一致:同一份业务代码可以在 MySQL / SQLite / PostgreSQL 间切换。
- 功能完备:自动迁移、关联加载、钩子、软删除、事务、日志等开箱即用。
当然,ORM 并非银弹:对性能极端敏感或需要复杂 SQL 的场景,原生 SQL 仍是更好的选择。实际项目中二者常常结合使用——GORM 承担大多数 CRUD,必要时用 db.Raw(...) 执行原生 SQL。
二、安装与连接
安装核心库与驱动(以 MySQL 为例):
go get -u gorm.io/gorm
go get -u gorm.io/driver/mysql建立连接:
package main
import (
"log"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
func main() {
dsn := "user:password@tcp(127.0.0.1:3306)/mydb?charset=utf8mb4&parseTime=True&loc=Local"
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
log.Fatal("连接数据库失败:", err)
}
_ = db // 后续操作均基于 *gorm.DB
}gorm.Open 返回的 *gorm.DB 是整个 GORM 的核心会话对象,所有增删改查都挂载在它之上。以下示例为突出重点,均省略建连代码,默认已拥有 db *gorm.DB。
三、定义模型
模型即是对应数据库表的 Go 结构体。GORM 使用约定:字段 ID 自动作为主键且自增,结构体名 User 默认映射为表名 users(复数形式)。
type User struct {
ID uint
Name string
Age int
}需要自定义主键或列名时,使用结构体 tag:
type User struct {
UserID uint `gorm:"primaryKey;column:user_id"`
Name string `gorm:"type:varchar(64);not null"`
Age int
}GORM 还提供了常用的内嵌模型 gorm.Model,自带 ID、CreatedAt、UpdatedAt、DeletedAt 四个字段,后者可启用软删除:
type Product struct {
gorm.Model
Code string
Price uint
}通过 db.AutoMigrate 可以根据模型自动建表或补齐缺失字段:
db.AutoMigrate(&User{}, &Product{})更详尽的 tag 约定与字段类型请参考 模型定义。
四、基础 CRUD
以下示例均围绕上面的 User 模型展开。
1. 创建
user := User{Name: "John", Age: 30}
if err := db.Create(&user).Error; err != nil {
log.Println("创建失败:", err)
}
// 主键 ID 会自动回填到 user.ID批量创建:
users := []User{{Name: "A", Age: 20}, {Name: "B", Age: 21}}
db.Create(&users)2. 查询
var user User
db.First(&user, 1) // 按主键查询
db.First(&user, "name = ?", "John") // 按条件查询第一条
var users []User
db.Where("age > ?", 18).Find(&users) // 查询多条判断"未找到"使用 errors.Is(err, gorm.ErrRecordNotFound),不要把它视为异常错误。
3. 更新
典型的"读—改—写"流程:先取出目标记录,修改字段后整体保存:
var user User
if err := db.First(&user, 1).Error; err != nil {
log.Println("用户不存在:", err)
return
}
user.Age = 31
if err := db.Save(&user).Error; err != nil { // 保存整条记录的所有字段
log.Println("更新失败:", err)
}如果只改个别字段,推荐 Update / Updates,直接走 UPDATE ... SET 而不是全字段回写:
db.Model(&user).Update("age", 31) // 单列
db.Model(&user).Updates(User{Name: "John", Age: 31}) // 结构体:仅更新非零字段
db.Model(&user).Updates(map[string]any{"name": "John", "age": 0}) // map:可写入零值使用结构体更新时,GORM 会忽略零值字段(如
0、""、false);若需要将字段显式置为零值,请改用map。
按条件批量更新,或使用 SQL 表达式做原子自增:
result := db.Model(&User{}).
Where("age < ?", 18).
Update("status", "minor")
fmt.Println("受影响行数:", result.RowsAffected)
db.Model(&user).Update("age", gorm.Expr("age + ?", 1)) // 原子自增为防止误更新全表,GORM 要求批量更新必须带条件,否则返回
ErrMissingWhereClause。
4. 删除
按主键或条件删除:
result := db.Delete(&User{}, 1) // 按主键
if result.Error != nil {
log.Println("删除失败:", result.Error)
return
}
if result.RowsAffected == 0 {
log.Println("没有匹配的记录被删除")
}
db.Where("age < ?", 18).Delete(&User{}) // 按条件批量若模型内嵌了 gorm.Model(包含 DeletedAt 字段),上述 Delete 会触发软删除:仅把 deleted_at 置为当前时间,后续普通查询会自动过滤这些记录。需要绕过软删除过滤或进行物理删除时,使用 Unscoped():
var users []User
db.Unscoped().Where("age < ?", 18).Find(&users) // 包含已软删的记录
db.Unscoped().Delete(&user) // 真正从表中移除更丰富的查询能力(Select、Joins、Preload、分页、聚合等)请见 查询文档。
五、事务
多条语句需要原子性保证时使用事务。GORM 推荐使用闭包写法,提交与回滚逻辑由框架自动处理:
err := db.Transaction(func(tx *gorm.DB) error {
if err := tx.Create(&User{Name: "Alice", Age: 25}).Error; err != nil {
return err // 返回错误将自动回滚
}
if err := tx.Create(&User{Name: "Bob", Age: 28}).Error; err != nil {
return err
}
return nil // 返回 nil 自动提交
})如需更精细的控制,也可以使用 db.Begin() / tx.Commit() / tx.Rollback() 的传统写法,用法与 database/sql 一致。
六、关联与钩子
GORM 支持 Has One、Has Many、Belongs To、Many To Many 以及多态关联,并能通过 Preload 预加载关联数据。一个最简单的"一对多"例子:
type Author struct {
ID uint
Name string
Books []Book // 一对多
}
type Book struct {
ID uint
Title string
AuthorID uint // 外键
}
author := Author{
Name: "J.K. Rowling",
Books: []Book{{Title: "Harry Potter 1"}, {Title: "Harry Potter 2"}},
}
db.Create(&author) // 作者与书籍将一并落库GORM 还提供了钩子(Hooks),可以在创建、查询、更新、删除等操作的前后插入自定义逻辑,典型场景如在入库前加密密码:
func (u *User) BeforeCreate(tx *gorm.DB) error {
hashed, err := bcrypt.GenerateFromPassword([]byte(u.Password), 14)
if err != nil {
return err
}
u.Password = string(hashed)
return nil
}关联与钩子是 GORM 中内容最丰富的两块主题,完整语义与用法请参阅官方文档:
七、连接池与最佳实践
1. 连接池配置
GORM 底层仍使用 database/sql 的连接池。可以通过 db.DB() 拿到底层 *sql.DB 再配置:
sqlDB, err := db.DB()
if err != nil {
log.Fatal(err)
}
sqlDB.SetMaxOpenConns(100)
sqlDB.SetMaxIdleConns(10)
sqlDB.SetConnMaxLifetime(time.Hour)2. 模型设计
- 单一职责:一个模型对应一种业务实体,避免结构体承载过多无关字段。
- 约定优于配置:尽量遵循 GORM 默认命名规则(驼峰字段对应下划线列),减少 tag 噪音。
- 可扩展性:预留常用的
CreatedAt/UpdatedAt/DeletedAt字段,可直接内嵌gorm.Model。
3. 日志与错误处理
- GORM 内置
logger包,可配置慢查询阈值、输出级别,生产环境建议关闭Info级别以避免刷屏。 - 业务层应始终检查
result.Error;对于"未找到"使用errors.Is(err, gorm.ErrRecordNotFound)单独分支处理。
八、MySQL 与 SQLite 的差异
得益于 GORM 的抽象,业务层 API(Create、First、Find、Save、Delete、事务、关联、钩子等)在两种数据库下完全一致。主要差异集中在驱动与连接配置:
| 对比项 | MySQL | SQLite |
|---|---|---|
| 驱动包 | gorm.io/driver/mysql | gorm.io/driver/sqlite |
| 打开方式 | mysql.Open(dsn) | sqlite.Open("test.db") |
| DSN | user:pass@tcp(host:port)/dbname?charset=utf8mb4... | 数据库文件路径,或 :memory: 使用内存库 |
| 部署形态 | 独立数据库服务,支持多机、高并发 | 单文件嵌入式库,零配置部署 |
| 连接池 | 关键参数,需按业务压力调优 | 意义有限,SQLite 写操作串行 |
| 数据类型 | 显式指定 varchar(n)、decimal 等更合理 | 类型系统宽松,常用 TEXT、INTEGER、REAL |
示例——切换到 SQLite 仅需两处改动:
import (
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})除此之外,若使用到原生 SQL 或某个数据库特有的函数、锁语义,仍需按目标数据库的方言适配;GORM 负责的是通用 CRUD 抽象,并不会完全抹平所有底层差异。
小结
GORM 用结构体与对象的方式包装了数据库访问,将开发者从样板式的 SQL 拼接和 Scan 中解放出来,同时提供事务、关联、钩子、自动迁移等完整能力,是 Go 生态中构建中大型业务系统的首选 ORM 方案。
本节仅覆盖了入门所需的最小知识面——连接、模型、CRUD、事务、关联与钩子的基本形态,以及 MySQL 与 SQLite 在 GORM 下的差异。当项目进入真实开发阶段后,建议配合 GORM 官方文档 深入学习预加载、复合主键、Session、Scope、性能优化等主题。