GORM ORM 基础
核心概念
GORM 是 Go 生态中最流行的 ORM 框架。它的设计哲学是:用 Go 结构体描述数据库表,用方法链表达查询逻辑,让开发者用面向对象的方式操作数据库,同时保留逃生通道(Raw SQL)。
ORM vs Raw SQL
go
// Raw SQL
rows, _ := db.Query("SELECT id, name, age FROM users WHERE age > ? AND name LIKE ?", 18, "张%")
// GORM
var users []User
db.Where("age > ? AND name LIKE ?", 18, "张%").Find(&users)
ORM 的价值在于:结构体即表结构(减少手写 SQL 的拼写错误)、自动类型转换、关联查询自动化、迁移工具。代价是学习曲线和一定的性能损耗。
连接与初始化
go
import (
"gorm.io/gorm"
"gorm.io/driver/sqlite" // 或 mysql, postgres
)
db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
GORM 的驱动层与 database/sql 不同,它自己封装了连接管理,但底层仍然可以接入 *sql.DB。
模型定义
GORM 约定:结构体名蛇形化为表名,字段名蛇形化为列名。
go
type User struct {
ID uint `gorm:"primaryKey"` // 主键
Name string `gorm:"size:100;not null"` // VARCHAR(100), NOT NULL
Email string `gorm:"uniqueIndex"` // 唯一索引
Age int `gorm:"default:0"` // 默认值 0
Birthday *time.Time // 指针类型 → 可 NULL
CreatedAt time.Time // GORM 自动管理创建时间
UpdatedAt time.Time // GORM 自动管理更新时间
DeletedAt gorm.DeletedAt `gorm:"index"` // 软删除
}
约定优于配置
| Go 字段 | 数据库列 | 说明 |
|---|---|---|
ID |
id |
名为 ID 的字段自动作为主键 |
CreatedAt |
created_at |
记录创建时自动写入 |
UpdatedAt |
updated_at |
每次更新自动刷新 |
DeletedAt |
deleted_at |
软删除标记,查询时自动过滤 |
标签语法
go
type Product struct {
gorm.Model // 内嵌 ID + CreatedAt + UpdatedAt + DeletedAt
Code string `gorm:"column:product_code;type:varchar(50);uniqueIndex:idx_code"`
Price uint `gorm:"check:price > 0"` // CHECK 约束
}
常用标签:column(列名)、type(列类型)、size(长度)、primaryKey、uniqueIndex、index、not null、default、comment、check。
CRUD 操作
Create
go
user := User{Name: "张三", Email: "zs@test.com", Age: 28}
db.Create(&user) // 插入并回填自增 ID
// 批量插入
db.Create(&[]User{
{Name: "李四", Email: "ls@test.com", Age: 32},
{Name: "王五", Email: "ww@test.com", Age: 25},
})
// 选择性插入(只写入指定字段)
db.Select("Name", "Email").Create(&User{Name: "赵六", Email: "zl@test.com", Age: 0})
Read
go
// 单条查询
var user User
db.First(&user, 1) // 按主键
db.First(&user, "email = ?", "zs@test.com") // 按条件
// 多条查询
var users []User
db.Find(&users) // 全部
db.Where("age >= ?", 25).Find(&users) // 条件
db.Where("name LIKE ?", "张%").Find(&users)
// 排序 + 分页
db.Order("age DESC").Limit(10).Offset(0).Find(&users)
// 计数
var count int64
db.Model(&User{}).Where("age > ?", 20).Count(&count)
Update
go
// 更新单个字段
db.Model(&user).Update("age", 29)
// 更新多个字段(struct 方式,零值字段会被忽略)
db.Model(&user).Updates(User{Name: "张三丰", Age: 30})
// 更新多个字段(map 方式,零值也会更新)
db.Model(&user).Updates(map[string]interface{}{"age": 0, "name": "张三"})
// 批量更新
db.Model(&User{}).Where("age < ?", 25).Update("age", 25)
Delete
gorm
// 软删除(有 DeletedAt 字段时自动启用)
db.Delete(&user) // 设置 deleted_at = now()
db.Unscoped().Delete(&user) // 真正删除
// 查询包含已软删除的记录
db.Unscoped().Find(&users)
// 批量删除
db.Where("age < ?", 18).Delete(&User{})
链式调用与方法链
GORM 的查询 API 是链式构建器模式,方法分三类:
| 类型 | 方法 | 说明 |
|---|---|---|
| 链式方法 | Where, Order, Limit, Select, Joins |
构建查询条件,返回 *gorm.DB |
| Finisher 方法 | Find, First, Create, Update, Delete, Count |
触发执行 |
| 新会话方法 | Session, WithContext, Scopes |
创建新会话上下文 |
go
// 链式 + Finisher
db.Where("age > ?", 20).
Order("created_at DESC").
Limit(20).
Offset(0).
Find(&users)
完整练习代码
go
// gorm_basics.go
package main
import (
"fmt"
"log"
"time"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
// User 用户模型
type User struct {
ID uint `gorm:"primaryKey"`
Name string `gorm:"size:100;not null"`
Email string `gorm:"size:200;uniqueIndex"`
Age int `gorm:"default:0"`
Birthday *time.Time
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
Products []Product `gorm:"foreignKey:UserID"`
}
// Product 产品模型(演示外键关联)
type Product struct {
ID uint `gorm:"primaryKey"`
Name string
Price float64
Stock int
// 一对多:一个用户有多个产品
UserID uint // 外键
User User `gorm:"foreignKey:UserID"` // 关联
}
func main() {
// 1. 连接数据库
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{
Logger: logger.Default.LogMode(logger.Info),
})
if err != nil {
log.Fatal(err)
}
// 2. 自动迁移(建表)
if err := db.AutoMigrate(&User{}, &Product{}); err != nil {
log.Fatal(err)
}
fmt.Println("=== 自动迁移完成 ===")
// ============================================
// 3. Create
// ============================================
fmt.Println("\n=== Create ===")
// 单条插入
birthday := time.Date(1998, 5, 15, 0, 0, 0, 0, time.UTC)
user := User{Name: "张三", Email: "zs@test.com", Age: 28, Birthday: &birthday}
result := db.Create(&user)
fmt.Printf(" 插入: ID=%d, 影响行数=%d\n", user.ID, result.RowsAffected)
// 批量插入
users := []User{
{Name: "李四", Email: "ls@test.com", Age: 32},
{Name: "王五", Email: "ww@test.com", Age: 25},
{Name: "赵六", Email: "zl@test.com", Age: 35},
{Name: "孙七", Email: "sq@test.com", Age: 22},
}
db.Create(&users)
fmt.Printf(" 批量插入: %d 条\n", len(users))
// ============================================
// 4. Read
// ============================================
fmt.Println("\n=== Read ===")
// 按主键查询
var u User
db.First(&u, 1)
fmt.Printf(" First(1): %s, %d岁\n", u.Name, u.Age)
// 条件查询
var youngUsers []User
db.Where("age < ?", 30).Order("age ASC").Find(&youngUsers)
fmt.Printf(" age<30 的用户 (%d人):\n", len(youngUsers))
for _, yu := range youngUsers {
fmt.Printf(" %s, %d岁\n", yu.Name, yu.Age)
}
// Select 指定字段
var names []struct {
Name string
Age int
}
db.Model(&User{}).Select("name", "age").Where("age >= ?", 28).Scan(&names)
fmt.Printf(" age>=28 (Select+Scan): ")
for _, n := range names {
fmt.Printf("%s(%d) ", n.Name, n.Age)
}
fmt.Println()
// 分页查询
var pageUsers []User
var total int64
db.Model(&User{}).Count(&total)
db.Limit(2).Offset(0).Order("id").Find(&pageUsers)
fmt.Printf(" 分页(page=1, size=2): 共%d条, 本页%d条\n", total, len(pageUsers))
// ============================================
// 5. Update
// ============================================
fmt.Println("\n=== Update ===")
// struct 更新(零值字段被忽略)
db.Model(&u).Updates(User{Name: "张三丰", Age: 30})
fmt.Printf(" Updates(struct): %s, %d岁\n", u.Name, u.Age)
// map 更新(零值也会更新)
db.Model(&u).Updates(map[string]interface{}{"age": 0})
db.First(&u, u.ID)
fmt.Printf(" Updates(map): %s, %d岁 (age=0 生效)\n", u.Name, u.Age)
// 批量更新
db.Model(&User{}).Where("age < ?", 25).Update("age", 25)
var updated []User
db.Where("age = ?", 25).Find(&updated)
fmt.Printf(" 批量更新 age<25 → age=25: %d人\n", len(updated))
// ============================================
// 6. Delete(软删除)
// ============================================
fmt.Println("\n=== Delete(软删除) ===")
var zhao User
db.Where("name = ?", "赵六").First(&zhao)
db.Delete(&zhao)
// 普通查询看不到已删除的
var count int64
db.Model(&User{}).Count(&count)
fmt.Printf(" 删除赵六后,剩余用户数: %d\n", count)
// Unscoped 可以查到软删除的记录
var allUsers []User
db.Unscoped().Find(&allUsers)
fmt.Printf(" Unscoped 查询(含软删除): %d条\n", len(allUsers))
// ============================================
// 7. 关联关系(一对多)
// ============================================
fmt.Println("\n=== 关联关系 ===")
// 为张三创建产品
db.Create(&Product{Name: "Go编程书", Price: 89.5, Stock: 100, UserID: 1})
db.Create(&Product{Name: "机械键盘", Price: 459.0, Stock: 50, UserID: 1})
db.Create(&Product{Name: "鼠标", Price: 99.0, Stock: 200, UserID: 2})
// 预加载查询(避免 N+1 问题)
var userWithProducts User
db.Preload("Products").First(&userWithProducts, 1)
fmt.Printf(" %s 的产品 (%d件):\n", userWithProducts.Name, len(userWithProducts.Products))
for _, p := range userWithProducts.Products {
fmt.Printf(" %s: ¥%.2f (库存%d)\n", p.Name, p.Price, p.Stock)
}
// ============================================
// 8. 钩子(Hooks)
// ============================================
fmt.Println("\n=== Hooks 演示 ===")
// BeforeCreate 钩子会在插入前自动调用
newUser := User{Name: "测试钩子", Email: "hook@test.com", Age: -1}
db.Create(&newUser)
fmt.Printf(" 创建前 Age=-1, 创建后: %d (钩子修正了负数)\n", newUser.Age)
}
// BeforeCreate GORM 钩子:插入前自动调用
func (u *User) BeforeCreate(tx *gorm.DB) (err error) {
// 年龄不能为负数,自动修正为 0
if u.Age < 0 {
u.Age = 0
}
return nil
}
运行方式
bash
mkdir -p demo4 && cd demo4
go mod init demo4
go get gorm.io/gorm gorm.io/driver/sqlite
go run main.go
要点总结
- GORM 用结构体描述表 ,标签控制列属性;
gorm.Model内嵌了 ID + 时间戳 + 软删除四件套 - 链式方法构建条件 ,Finisher 方法触发执行;链式方法返回
*gorm.DB可继续链式调用 - struct Updates 零值被忽略 ,需要更新零值时用
map[string]interface{} - 软删除通过
DeletedAt实现 ,查询自动过滤;Unscoped()可查已删除记录 - Preload 解决 N+1 问题:一条 SQL JOIN 加载关联数据,避免循环查询
- Hooks 在生命周期点自动触发 :
BeforeCreate/AfterCreate/BeforeUpdate/BeforeDelete等 - AutoMigrate 只增不减:新增字段和表会自动创建,但删除字段需要手动操作或用迁移工具