Skip to content

22|ORM 与 GORM

database/sql 提供了操作数据库的基础能力,但手写 SQL 和维护扫描逻辑比较繁琐。ORM(对象关系映射)把数据库表映射到结构体,把 CRUD 操作映射到方法调用,减少了手写 SQL 的工作量。GORM 是 Go 生态里最主流的 ORM,国内后端项目几乎都在用。它支持自动迁移、关联查询、钩子、批量操作和连接池管理,学习成本不高,功能足够覆盖大部分业务场景。

一、安装与连接

bash
go get -u gorm.io/gorm
go get -u gorm.io/driver/sqlite   # 或 mysql、postgres
go mod tidy

连接数据库:

go
package main

import (
	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
	"gorm.io/gorm/logger"
	"log"
)

func main() {
	db, err := gorm.Open(sqlite.Open("gateway.db"), &gorm.Config{
		Logger: logger.Default.LogMode(logger.Info),
	})
	if err != nil {
		log.Fatal("failed to connect database:", err)
	}
}

GORM 的 gorm.Open 封装了 database/sql 的连接逻辑,返回的 *gorm.DB 既是数据库对象,也是查询构造器。Logger 配置让 GORM 打印执行的 SQL,开发和调试时很有用,生产环境可以关掉或降低级别。

二、模型定义

GORM 用结构体定义数据库表。结构体名默认映射为蛇形复数表名,字段名映射为蛇形列名:

go
type Route struct {
	ID        uint           `gorm:"primaryKey"`
	Prefix    string         `gorm:"size:255;not null;uniqueIndex"`
	Backend   string         `gorm:"size:255;not null"`
	CreatedAt time.Time
	UpdatedAt time.Time
	DeletedAt gorm.DeletedAt `gorm:"index"` // 软删除
}

GORM 标签控制数据库行为:

标签作用
primaryKey主键
size:255字段长度
not null非空约束
uniqueIndex唯一索引
default:value默认值

CreatedAtUpdatedAt 是 GORM 约定的字段,自动在创建和更新时填充时间。DeletedAt 启用软删除——调用 Delete 时不会真正删除记录,只是设置 deleted_at 时间戳。查询时自动排除已软删除的记录。

三、自动迁移

AutoMigrate 根据模型定义创建或更新表结构:

go db.AutoMigrate(&Route

自动迁移只会增加缺失的字段和索引,不会删除已有字段,也不会修改字段类型。生产环境里的 schema 变更需要更谨慎,通常用专门的迁移工具(如 golang-migrate)管理。

四、CRUD

创建

go
route := Route{Prefix: "/api/users", Backend: "http://127.0.0.1:8081"}
result := db.Create(&route)
if result.Error != nil {
	log.Println("create failed:", result.Error)
}
fmt.Println(route.ID) // 自动填充自增 ID

查询

go
// 查询全部
var routes []Route
db.Find(&routes)

// 条件查询
var route Route
db.Where("prefix = ?", "/api/users").First(&route)

// 按 ID 查询
db.First(&route, 3) // SELECT * FROM routes WHERE id = 3

// 不存在时返回 ErrRecordNotFound
var notFound Route
err := db.Where("prefix = ?", "/notexist").First(&notFound).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
	fmt.Println("not found")
}

First 按主键升序取第一条,Last 取最后一条,Take 取一条不排序。查询不到记录时返回 gorm.ErrRecordNotFound,需要显式检查。

更新

go
// 更新单个字段
db.Model(&Route{}).Where("id = ?", 1).Update("backend", "http://127.0.0.1:8083")

// 更新多个字段
db.Model(&route).Updates(Route{Prefix: "/api/v2/users", Backend: "http://new"})

Updates 只会更新非零值字段。如果要把某个字段更新为零值(比如把 backend 更新为空字符串),需要用 Select 指定:

go
db.Model(&route).Select("Backend").Updates(Route{Backend: ""})

删除

go
db.Delete(&route, 1) // 软删除:设置 deleted_at

// 硬删除
 db.Unscoped().Delete(&route, 1)

五、连接池配置

GORM 底层使用 database/sql 的连接池,可以通过 sql.DB 获取原生对象来配置:

go
sqlDB, err := db.DB()
if err != nil {
	log.Fatal(err)
}

sqlDB.SetMaxOpenConns(25)
sqlDB.SetMaxIdleConns(5)
sqlDB.SetConnMaxLifetime(5 * time.Minute)

六、常见性能问题

N+1 查询

关联查询时,如果先查主表再循环查关联表,会产生大量查询:

go
// N+1 问题
var routes []Route
db.Find(&routes)
for _, r := range routes {
	// 每次循环都发一次查询
	db.Where("route_id = ?", r.ID).Find(&backends)
}

解决方法是预加载 Preload

go
var routes []Route
db.Preload("Backends").Find(&routes) // 只发两条 SQL

大量数据查询

Find 会把所有结果加载到内存。数据量大时用 FindInBatches 分批处理:

go db.FindInBatches(&routes,
		for _, route := range routes {
			// 处理每批 100 条
		}
		return nil
	})

七、常见错误

传入 nil 指针

go
db.Create(nil) // panic

GORM 方法需要传入结构体指针,nil 会导致 panic。

忽略返回的错误

go db.Create(&route)

数据库约束冲突、连接失败等错误都在 result.Error 里,必须检查。

在事务外使用 db 对象

go
tx := db.Begin()
tx.Create(&route)
db.Create(&other) // 不在事务内

事务里的操作必须用 tx 执行。

软删除后查不到记录

go db.Delete(&route)
db.First(&route, route.ID) // 查不到,因为 DeletedAt 不为空

需要查已软删除的记录时用 Unscoped

go db.Unscoped().First(&route,