Skip to content

7.6 GORM 框架入门

简介

GORM 是 Go 语言中常用的数据库抽象层(DBAL),API 简洁灵活,支持 MySQL、PostgreSQL、SQLite、SQL Server 等多种数据库,并在此之上提供了模型定义、CRUD、事务、关联、钩子、自动迁移、软删除、预加载等完整能力,是目前 Go 生态中使用最广泛的 ORM 框架。

使用 GORM 时只需掌握两个核心概念:

  • 模型(Model):用一个 Go 结构体对应一张数据表,字段对应列,通过操作结构体实例完成对表的增删改查;
  • 数据库会话(*gorm.DBgorm.Open 返回的会话对象,所有查询、创建、更新、删除方法都挂载在它之上。

本节仅介绍 GORM 最基础的用法;关联、预加载、钩子、复合主键、Sharding 等进阶特性请参阅 GORM 官方文档

一、为什么使用 ORM

在前面几节中,我们使用 database/sql 搭配驱动直接操作 SQLite / MySQL,CRUD 语句完全需要手写 SQL,再将结果逐字段 Scan 到变量中。这种方式性能好、透明度高,但在业务复杂度上升之后,常常会出现几个问题:

  • SQL 字符串散落在各处,字段增减时容易漏改;
  • rows.Scan 的字段顺序必须与 SQL 严格一致,维护成本高;
  • 重复代码多,建连接、Prepare、遍历 Rows、错误处理的样板很冗长;
  • 跨数据库(SQLite、MySQL、PostgreSQL)迁移时,方言差异需要手动适配。

ORM 用结构体表达表、用对象表达行,使开发者可以以面向对象的方式操作数据库。以"插入一条用户记录"为例,对比两种写法:

原生 database/sql

go
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:

go
user := User{Name: "John", Age: 30}
err := db.Create(&user).Error
// user.ID 会自动回填

ORM 带来的主要价值:

  • 以结构体为中心:模型即表,字段即列,IDE 补全友好,重构安全。
  • 样板代码少:不再手写 PrepareScanRows 遍历。
  • 跨数据库一致:同一份业务代码可以在 MySQL / SQLite / PostgreSQL 间切换。
  • 功能完备:自动迁移、关联加载、钩子、软删除、事务、日志等开箱即用。

当然,ORM 并非银弹:对性能极端敏感或需要复杂 SQL 的场景,原生 SQL 仍是更好的选择。实际项目中二者常常结合使用——GORM 承担大多数 CRUD,必要时用 db.Raw(...) 执行原生 SQL。

二、安装与连接

安装核心库与驱动(以 MySQL 为例):

bash
go get -u gorm.io/gorm
go get -u gorm.io/driver/mysql

建立连接:

go
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(复数形式)。

go
type User struct {
    ID   uint
    Name string
    Age  int
}

需要自定义主键或列名时,使用结构体 tag:

go
type User struct {
    UserID uint   `gorm:"primaryKey;column:user_id"`
    Name   string `gorm:"type:varchar(64);not null"`
    Age    int
}

GORM 还提供了常用的内嵌模型 gorm.Model,自带 IDCreatedAtUpdatedAtDeletedAt 四个字段,后者可启用软删除:

go
type Product struct {
    gorm.Model
    Code  string
    Price uint
}

通过 db.AutoMigrate 可以根据模型自动建表或补齐缺失字段:

go
db.AutoMigrate(&User{}, &Product{})

更详尽的 tag 约定与字段类型请参考 模型定义

四、基础 CRUD

以下示例均围绕上面的 User 模型展开。

1. 创建

go
user := User{Name: "John", Age: 30}
if err := db.Create(&user).Error; err != nil {
    log.Println("创建失败:", err)
}
// 主键 ID 会自动回填到 user.ID

批量创建:

go
users := []User{{Name: "A", Age: 20}, {Name: "B", Age: 21}}
db.Create(&users)

2. 查询

go
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. 更新

典型的"读—改—写"流程:先取出目标记录,修改字段后整体保存:

go
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 而不是全字段回写:

go
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 表达式做原子自增:

go
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. 删除

按主键或条件删除:

go
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()

go
var users []User
db.Unscoped().Where("age < ?", 18).Find(&users) // 包含已软删的记录
db.Unscoped().Delete(&user)                     // 真正从表中移除

更丰富的查询能力(SelectJoinsPreload、分页、聚合等)请见 查询文档

五、事务

多条语句需要原子性保证时使用事务。GORM 推荐使用闭包写法,提交与回滚逻辑由框架自动处理:

go
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 OneHas ManyBelongs ToMany To Many 以及多态关联,并能通过 Preload 预加载关联数据。一个最简单的"一对多"例子:

go
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),可以在创建、查询、更新、删除等操作的前后插入自定义逻辑,典型场景如在入库前加密密码:

go
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 再配置:

go
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(CreateFirstFindSaveDelete、事务、关联、钩子等)在两种数据库下完全一致。主要差异集中在驱动与连接配置:

对比项MySQLSQLite
驱动包gorm.io/driver/mysqlgorm.io/driver/sqlite
打开方式mysql.Open(dsn)sqlite.Open("test.db")
DSNuser:pass@tcp(host:port)/dbname?charset=utf8mb4...数据库文件路径,或 :memory: 使用内存库
部署形态独立数据库服务,支持多机、高并发单文件嵌入式库,零配置部署
连接池关键参数,需按业务压力调优意义有限,SQLite 写操作串行
数据类型显式指定 varchar(n)decimal 等更合理类型系统宽松,常用 TEXTINTEGERREAL

示例——切换到 SQLite 仅需两处改动:

go
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、性能优化等主题。

参考资料

  1. GORM 官方文档(中文)
  2. GORM GitHub 仓库
  3. 模型定义
  4. 关联总览
  5. 钩子 Hooks