Appearance
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 | 默认值 |
CreatedAt 和 UpdatedAt 是 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(¬Found).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) // panicGORM 方法需要传入结构体指针,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,