Files
cs-note/hzh/GolangStar/Go语言框架/gorm.md
T

201 lines
5.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
tags: [go, golang, gorm, ORM, 数据库]
create time: 2026-06-07 15:25
---
# GORM
## 概述
GORM 是 Go 生态中最流行的 ORM 库,提供简洁的链式 API 操作数据库。本文从连接配置到 CRUD、关联关系和事务,覆盖日常开发的核心用法。
## 正文
### 安装
```bash
go get -gorm.io/gorm
go get -gorm.io/driver/mysql # MySQL 驱动(按需选择)
```
### 连接数据库
```go
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{})
if err != nil {
log.Fatal("failed to connect database:", err)
}
// 可选:调优连接池
sqlDB, _ := db.DB()
sqlDB.SetMaxOpenConns(100) // 最大连接数
sqlDB.SetMaxIdleConns(10) // 最大空闲连接
```
> [!tip] 💡 性能建议
> - `SetMaxOpenConns` 应根据并发量和请求耗时调整——IO 密集型可适当放大
> - `SetMaxIdleConns` 一般设为 MaxOpenConns 的 1/4~1/2
> - 连接泄漏(未正确关闭)会导致连接池耗尽,务必注意
### 模型定义
```go
type User struct {
ID uint `gorm:"primaryKey"`
Name string `gorm:"size:255;not null"`
Age int
Email string `gorm:"uniqueIndex"`
CreatedAt time.Time
}
// 表名默认为复数形式 "users",可通过 TableName 自定义
func (User) TableName() string {
return "app_users"
}
```
> [!info] ℹ️ GORM 命名约定
> - 表名:结构体名的蛇形复数(User → users)
> - 字段名:驼峰转蛇形(UserID → user_id)
> - 主键:自动使用 ID / uuid / uid 作为主键并自增
> - 外键:关联字段名 + `_id`
### CRUD 操作
#### 创建
```go
user := User{Name: "张三", Age: 25}
result := db.Create(&user) // 自动回填 user.ID
fmt.Println(user.ID) // 查看插入后的 ID
```
#### 查询
```go
// 根据主键查询
var u1 User
db.First(&u1, 1) // SELECT * FROM users WHERE id = 1
// 条件查询
var u2 []User
db.Where("age > ?", 18).Find(&u2)
// 链式组合
db.Where("name LIKE ?", "%张%").Order("age desc").Limit(10).Find(&u2)
```
> [!question] 💭 ORM vs 原生 SQL
> GORM 的链式调用确实优雅,但在复杂查询场景下是否值得牺牲 SQL 的表达能力?思考你的项目中哪些查询可以用 GORM 表达,哪些需要手写 SQL。
#### 更新
```go
// 更新单个字段
db.Model(&user).Update("name", "李四")
// 更新多个字段
db.Model(&user).Updates(User{Name: "王五", Age: 30})
// 全量更新(注意:会忽略零值字段!)
db.Save(&user)
```
> [!warning] ⚠️ Save 的零值陷阱
> `Save()` 会用结构体的所有字段执行 UPDATE。如果某个字段是零值(如 `Age: 0`),会被更新为 0!建议使用 `Updates` 并只传需要更新的字段。
#### 删除
```go
db.Delete(&user, 1) // 软删除(需启用)或硬删除
db.Where("age < ?", 18).Delete(&User{}) // 批量删除
```
> [!note] 📝 软删除
> GORM 默认启用软删除——调用 Delete 后记录不会被物理删除,而是设置 `deleted_at` 时间戳。如需物理删除:`db.Unscoped().Delete(&user)`
### 关联关系
```go
type Profile struct {
ID uint
UserID uint `gorm:"index"`
Bio string
}
type Post struct {
ID uint
AuthorID uint `gorm:"index"`
Title string
Author User `gorm:"foreignKey:AuthorID"` // 指定外键
}
// 预加载关联数据
db.Preload("Profile").First(&user, 1)
// SELECT * FROM profiles WHERE user_id = 1
```
> [!tip] 💡 N+1 问题
> 在循环中逐个查询关联数据会产生 N+1 问题。始终使用 `Preload` 一次性加载:
> ```go
> // ❌ N+1
> for _, post := range posts {
> db.First(&post.Author)
> }
>
> // ✅ 一次加载
> db.Preload("Author").Find(&posts)
> ```
### 事务
```go
err := db.Transaction(func(tx *gorm.DB) error {
if err := tx.Create(&User{Name: "Alice"}).Error; err != nil {
return err // 返回非零错误触发回滚
}
if err := tx.Create(&User{Name: "Bob"}).Error; err != nil {
return err
}
return nil // 返回 nil 则提交
})
```
> [!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 请求生命周期与数据库事务结合