2026-06-07 11:08:10 +08:00
|
|
|
|
---
|
2026-06-07 12:14:39 +08:00
|
|
|
|
tags: [go, golang, gorm, ORM, 数据库]
|
|
|
|
|
|
create time: 2026-06-07 15:25
|
2026-06-07 11:08:10 +08:00
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
# GORM
|
|
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
## 概述
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
GORM 是 Go 生态中最流行的 ORM 库,提供简洁的链式 API 操作数据库。本文从连接配置到 CRUD、关联关系和事务,覆盖日常开发的核心用法。
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
## 正文
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
### 安装
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
```bash
|
|
|
|
|
|
go get -gorm.io/gorm
|
|
|
|
|
|
go get -gorm.io/driver/mysql # MySQL 驱动(按需选择)
|
2026-06-07 11:08:10 +08:00
|
|
|
|
```
|
|
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
### 连接数据库
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
|
|
|
|
|
```go
|
2026-06-07 12:14:39 +08:00
|
|
|
|
dsn := "user:pass@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local"
|
|
|
|
|
|
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
|
2026-06-07 11:08:10 +08:00
|
|
|
|
if err != nil {
|
2026-06-07 12:14:39 +08:00
|
|
|
|
log.Fatal("failed to connect database:", err)
|
2026-06-07 11:08:10 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
// 可选:调优连接池
|
|
|
|
|
|
sqlDB, _ := db.DB()
|
|
|
|
|
|
sqlDB.SetMaxOpenConns(100) // 最大连接数
|
|
|
|
|
|
sqlDB.SetMaxIdleConns(10) // 最大空闲连接
|
2026-06-07 11:08:10 +08:00
|
|
|
|
```
|
|
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
> [!tip] 💡 性能建议
|
|
|
|
|
|
> - `SetMaxOpenConns` 应根据并发量和请求耗时调整——IO 密集型可适当放大
|
|
|
|
|
|
> - `SetMaxIdleConns` 一般设为 MaxOpenConns 的 1/4~1/2
|
|
|
|
|
|
> - 连接泄漏(未正确关闭)会导致连接池耗尽,务必注意
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
### 模型定义
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
|
type User struct {
|
2026-06-07 12:14:39 +08:00
|
|
|
|
ID uint `gorm:"primaryKey"`
|
|
|
|
|
|
Name string `gorm:"size:255;not null"`
|
|
|
|
|
|
Age int
|
|
|
|
|
|
Email string `gorm:"uniqueIndex"`
|
|
|
|
|
|
CreatedAt time.Time
|
2026-06-07 11:08:10 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
// 表名默认为复数形式 "users",可通过 TableName 自定义
|
|
|
|
|
|
func (User) TableName() string {
|
|
|
|
|
|
return "app_users"
|
|
|
|
|
|
}
|
2026-06-07 11:08:10 +08:00
|
|
|
|
```
|
|
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
> [!info] ℹ️ GORM 命名约定
|
|
|
|
|
|
> - 表名:结构体名的蛇形复数(User → users)
|
|
|
|
|
|
> - 字段名:驼峰转蛇形(UserID → user_id)
|
|
|
|
|
|
> - 主键:自动使用 ID / uuid / uid 作为主键并自增
|
|
|
|
|
|
> - 外键:关联字段名 + `_id`
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
### CRUD 操作
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
#### 创建
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
|
user := User{Name: "张三", Age: 25}
|
2026-06-07 12:14:39 +08:00
|
|
|
|
result := db.Create(&user) // 自动回填 user.ID
|
|
|
|
|
|
fmt.Println(user.ID) // 查看插入后的 ID
|
2026-06-07 11:08:10 +08:00
|
|
|
|
```
|
|
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
#### 查询
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
|
|
|
|
|
```go
|
2026-06-07 12:14:39 +08:00
|
|
|
|
// 根据主键查询
|
|
|
|
|
|
var u1 User
|
|
|
|
|
|
db.First(&u1, 1) // SELECT * FROM users WHERE id = 1
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
// 条件查询
|
|
|
|
|
|
var u2 []User
|
|
|
|
|
|
db.Where("age > ?", 18).Find(&u2)
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
// 链式组合
|
|
|
|
|
|
db.Where("name LIKE ?", "%张%").Order("age desc").Limit(10).Find(&u2)
|
2026-06-07 11:08:10 +08:00
|
|
|
|
```
|
|
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
> [!question] 💭 ORM vs 原生 SQL
|
|
|
|
|
|
> GORM 的链式调用确实优雅,但在复杂查询场景下是否值得牺牲 SQL 的表达能力?思考你的项目中哪些查询可以用 GORM 表达,哪些需要手写 SQL。
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
#### 更新
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
|
|
|
|
|
```go
|
2026-06-07 12:14:39 +08:00
|
|
|
|
// 更新单个字段
|
|
|
|
|
|
db.Model(&user).Update("name", "李四")
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
// 更新多个字段
|
|
|
|
|
|
db.Model(&user).Updates(User{Name: "王五", Age: 30})
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
// 全量更新(注意:会忽略零值字段!)
|
|
|
|
|
|
db.Save(&user)
|
2026-06-07 11:08:10 +08:00
|
|
|
|
```
|
|
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
> [!warning] ⚠️ Save 的零值陷阱
|
|
|
|
|
|
> `Save()` 会用结构体的所有字段执行 UPDATE。如果某个字段是零值(如 `Age: 0`),会被更新为 0!建议使用 `Updates` 并只传需要更新的字段。
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
#### 删除
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
|
|
|
|
|
```go
|
2026-06-07 12:14:39 +08:00
|
|
|
|
db.Delete(&user, 1) // 软删除(需启用)或硬删除
|
|
|
|
|
|
db.Where("age < ?", 18).Delete(&User{}) // 批量删除
|
2026-06-07 11:08:10 +08:00
|
|
|
|
```
|
|
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
> [!note] 📝 软删除
|
|
|
|
|
|
> GORM 默认启用软删除——调用 Delete 后记录不会被物理删除,而是设置 `deleted_at` 时间戳。如需物理删除:`db.Unscoped().Delete(&user)`
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
### 关联关系
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
|
|
|
|
|
```go
|
2026-06-07 12:14:39 +08:00
|
|
|
|
type Profile struct {
|
|
|
|
|
|
ID uint
|
|
|
|
|
|
UserID uint `gorm:"index"`
|
|
|
|
|
|
Bio string
|
2026-06-07 11:08:10 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
type Post struct {
|
|
|
|
|
|
ID uint
|
|
|
|
|
|
AuthorID uint `gorm:"index"`
|
|
|
|
|
|
Title string
|
|
|
|
|
|
Author User `gorm:"foreignKey:AuthorID"` // 指定外键
|
|
|
|
|
|
}
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
// 预加载关联数据
|
|
|
|
|
|
db.Preload("Profile").First(&user, 1)
|
|
|
|
|
|
// SELECT * FROM profiles WHERE user_id = 1
|
|
|
|
|
|
```
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
> [!tip] 💡 N+1 问题
|
|
|
|
|
|
> 在循环中逐个查询关联数据会产生 N+1 问题。始终使用 `Preload` 一次性加载:
|
|
|
|
|
|
> ```go
|
|
|
|
|
|
> // ❌ N+1
|
|
|
|
|
|
> for _, post := range posts {
|
|
|
|
|
|
> db.First(&post.Author)
|
|
|
|
|
|
> }
|
|
|
|
|
|
>
|
|
|
|
|
|
> // ✅ 一次加载
|
|
|
|
|
|
> db.Preload("Author").Find(&posts)
|
|
|
|
|
|
> ```
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
### 事务
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
```go
|
|
|
|
|
|
err := db.Transaction(func(tx *gorm.DB) error {
|
|
|
|
|
|
if err := tx.Create(&User{Name: "Alice"}).Error; err != nil {
|
|
|
|
|
|
return err // 返回非零错误触发回滚
|
2026-06-07 11:08:10 +08:00
|
|
|
|
}
|
2026-06-07 12:14:39 +08:00
|
|
|
|
if err := tx.Create(&User{Name: "Bob"}).Error; err != nil {
|
|
|
|
|
|
return err
|
2026-06-07 11:08:10 +08:00
|
|
|
|
}
|
2026-06-07 12:14:39 +08:00
|
|
|
|
return nil // 返回 nil 则提交
|
|
|
|
|
|
})
|
2026-06-07 11:08:10 +08:00
|
|
|
|
```
|
|
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
> [!warning] ⚠️ 事务中的常见错误
|
|
|
|
|
|
> - 不要在事务中使用全局 `db` 变量,应使用传入的 `tx`
|
|
|
|
|
|
> - 事务内的所有操作都必须检查 `.Error`
|
|
|
|
|
|
> - 事务必须在同一个连接上执行(GORM 默认保证这一点)
|
|
|
|
|
|
|
|
|
|
|
|
### ORM vs NORM
|
|
|
|
|
|
|
|
|
|
|
|
> [!question] 💭 思考
|
|
|
|
|
|
> GORM 属于 ORM(对象关系映射),而 Gin 搭配 sqlx 属于 NORM(无映射)。什么时候该用 ORM,什么时候该用 NORM?
|
|
|
|
|
|
|
|
|
|
|
|
| 维度 | ORM (GORM) | NORM (sqlx/raw SQL) |
|
|
|
|
|
|
|------|-----------|---------------------|
|
|
|
|
|
|
| 开发效率 | 高(链式 API) | 低(需写 SQL) |
|
|
|
|
|
|
| 类型安全 | 编译期部分保障 | 完全依赖 SQL 编写 |
|
|
|
|
|
|
| 复杂查询 | 有限支持 | 完全自由 |
|
|
|
|
|
|
| 性能 | 有额外开销 | 最优 |
|
|
|
|
|
|
| 学习曲线 | 低 | 需熟悉 SQL |
|
|
|
|
|
|
|
|
|
|
|
|
> [!tip] 💡 混合策略
|
|
|
|
|
|
> 大多数项目可以采用混合模式:简单 CRUD 用 GORM,复杂报表/分析查询用原生 SQL:
|
|
|
|
|
|
> ```go
|
|
|
|
|
|
> // 简单查询用 GORM
|
|
|
|
|
|
> db.Find(&users)
|
|
|
|
|
|
>
|
|
|
|
|
|
> // 复杂查询用原生 SQL
|
|
|
|
|
|
> var results []struct {
|
|
|
|
|
|
> Department string
|
|
|
|
|
|
> AvgAge float64
|
|
|
|
|
|
> }
|
|
|
|
|
|
> db.Raw(`SELECT department, AVG(age) as avg_age FROM users GROUP BY department`).Scan(&results)
|
|
|
|
|
|
> ```
|
|
|
|
|
|
|
|
|
|
|
|
## 关联笔记
|
|
|
|
|
|
|
|
|
|
|
|
- [[hzh/GolangStar/Go语言框架/gin]] — Gin + GORM 是 Go Web 全栈的经典搭档
|
|
|
|
|
|
- [[hzh/GolangStar/Go语言进阶/Context]] — HTTP 请求生命周期与数据库事务结合
|