GORM 常见操作从入门到能写 DAO

🥰个人主页:会编程的土豆(欢迎来访)

💎作者简介:后端学习者

❄️个人专栏:数据结构与算法数据库leetcode

那些你一个人走过的夜路,终将化作照亮未 来的光

适合对象:会一点 Go 和 SQL,刚开始用 GORM 的同学

结合项目:影院票务(Gin + GORM + MySQL)

读完目标:能看懂 dao/ 里大部分代码,并能自己写出简单增删改查


写在前面

很多人第一次碰 GORM,会有两个错觉:

  1. 「这不就是把 SQL 换成英文链式调用吗?」------对,入门可以这么理解
  2. 「会了 Create/Find 就算会 GORM 了」------不够,课设/工作里还常碰到:条件拼接、Omit、联表、Scan、事务、FOR UPDATE

本文不追求官方文档全文搬运,而是按票务项目真实写法,把最常用的操作讲透


一、GORM 是什么?在项目里处于哪一层?

GORM = Go 的 ORM 框架:用结构体和链式 API 操作数据库,少手写拼接 SQL

在本项目中的位置:

复制代码
Handler(handlers/)
    ↓ 调用
DAO(dao/)  ←── 几乎所有 GORM 写在这里
    ↓ 使用
db.DB(*gorm.DB)
    ↓
MySQL

记住一条规矩:

尽量不要在 Handler 里直接写 db.DB.xxx,GORM 操作收敛到 dao


二、连接数据库:项目怎么初始化 GORM

对应文件:db/db.go

复制代码
var DB *gorm.DB

func Init(dsn string) error {
    var err error
    DB, err = gorm.Open(mysql.Open(dsn), &gorm.Config{
        Logger: logger.Default.LogMode(logger.Warn),
    })
    if err != nil {
        return err
    }
    sqlDB, _ := DB.DB()
    sqlDB.SetMaxOpenConns(20)
    sqlDB.SetMaxIdleConns(5)
    sqlDB.SetConnMaxLifetime(30 * time.Minute)
    return sqlDB.Ping()
}

要点:

说明
gorm.Open 打开连接,得到全局 *gorm.DB
mysql.Open(dsn) 用 MySQL 驱动;dsn 一般来自配置
DB.DB() 拿到底层 *sql.DB,用来配连接池
全局变量 db.DB dao 里到处 db.DB.Create(...)

新手注意:本项目表结构主要靠 sql/schema.sql 建好,不是 一上来就全靠 AutoMigrate 生成(课设这样更可控)


三、模型(Model):结构和表怎么对应

GORM 靠 struct + tag 知道字段怎么映射

示例(简化自 models.User):

复制代码
type User struct {
    ID        int64     `gorm:"primaryKey;autoIncrement" json:"id"`
    Username  string    `gorm:"size:50;uniqueIndex;not null" json:"username"`
    Password  string    `gorm:"size:32;not null" json:"-"`
    Nickname  string    `gorm:"size:50;not null;default:''" json:"nickname"`
    Role      int       `gorm:"not null;default:0" json:"role"`
    CreatedAt time.Time `json:"created_at"`
}

func (User) TableName() string { return "users" }

3.1 常见 gorm tag

tag 含义 例子
primaryKey 主键 id
autoIncrement 自增
size:50 字符串长度 varchar(50) 语义
not null 非空
uniqueIndex 唯一索引 用户名
index 普通索引
column:rows_num 字段名和列名不同 影厅行数
type:text 指定列类型 简介
-> 只读:查出来可以填,写入时忽略 GroupNameMovieTitle
json:"-" JSON 不输出(不是 gorm 专属) 密码

3.2 TableName() 干嘛?

告诉 GORM:这个结构体对应哪张表

不写的话,GORM 常会按复数规则猜表名;课设里显式写更稳

3.3 为什么有「假字段」?

比如 Movie.GroupNameOrder.MovieTitle

  • 表里不一定有这一列
  • 联表查询时 SELECT ... AS group_name 扫进结构体
  • gorm:"->" + Omit("GroupName") 避免 Create 时乱写不存在的列

这是本项目非常实用的技巧。


四、CRUD 总览(先建立地图)

操作 SQL 直觉 GORM 常见写法
增 Create INSERT db.Create(&obj)
查 Read SELECT First / Find / Scan
改 Update UPDATE Update / Updates
删 Delete DELETE Delete

下面按「增 → 查 → 改 → 删 → 条件 → 联表 → 事务」展开。


五、增(Create):插入一条 / 批量插入

5.1 插入一条

复制代码
// dao/user.go
func CreateUser(u *models.User) error {
    return db.DB.Create(u).Error
}

等价思路:

复制代码
INSERT INTO users (...) VALUES (...);

成功后,若主键自增,u.ID 通常会被 GORM 回填。

5.2 为什么经常看到 Omit

复制代码
// 创建电影时,忽略联表读出来的 GroupName
return db.DB.Omit("GroupName").Create(m).Error

// 创建订单时,忽略展示用字段
return tx.Omit("MovieTitle", "HallName", "StartTime").Create(order).Error

Omit = 插入/更新时跳过这些字段。

联表展示字段、统计字段、不想被客户端篡改的字段,都适合 Omit。

5.3 批量插入

生成场次座位时:

复制代码
return db.DB.CreateInBatches(seats, 100).Error

意思:切片里很多 Seat,每 100 条一批插入,减轻单次 SQL 过大。


六、查(Read):First / Find / 主键查询 / 找不到怎么办

6.1 按主键查一条:First(&obj, id)

复制代码
err := db.DB.First(&h, id).Error

大致等价:

复制代码
SELECT * FROM halls WHERE id = ? LIMIT 1;

6.2 带条件查一条:Where(...).First

复制代码
err := db.DB.Where("username = ?", username).First(&u).Error

永远用 ? 占位符传参数,不要自己拼字符串,防 SQL 注入。

6.3 查列表:Find

复制代码
var list []models.MovieGroup
err := db.DB.Order("id").Find(&list).Error

SELECT * FROM movie_groups ORDER BY id;

6.4 记录不存在怎么判断?

复制代码
err := db.DB.Where("username = ?", username).First(&u).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
    return nil, nil // 本项目约定:没找到返回 (nil, nil)
}

新手坑:

  • err != nil 不一定是「系统挂了」,也可能是「没这条数据」
  • 业务上要分清:ErrRecordNotFound vs 真正的数据库错误

6.5 First vs Find vs Scan(很重要)

API 典型用途 说明
First 取一条,按模型表查 找不到 → ErrRecordNotFound
Find 取多条,按模型表查 找不到常常是空切片,不一定当错误
Scan 把查询结果扫进结构体 联表自定义 Select 时最常用

本项目电影列表是联表,所以多用 Scan

复制代码
err := q.Order("m.id DESC").Scan(&list).Error

七、改(Update):改一个字段 / 改多个字段

7.1 改单个字段:Update

复制代码
// 取消场次:只改 status
return db.DB.Model(&models.Schedule{}).
    Where("id = ?", id).
    Update("status", models.ScheduleCancelled).Error

7.2 改多个字段:Updates

复制代码
return db.DB.Model(g).Updates(map[string]interface{}{
    "name":        g.Name,
    "description": g.Description,
}).Error

锁座时也是 Updates

复制代码
tx.Model(&models.Seat{}).Where("id = ?", sid).Updates(map[string]interface{}{
    "status":       models.SeatLocked,
    "lock_user_id": userID,
    "lock_until":   lockUntil,
    "order_id":     order.ID,
})

7.3 Update / Updates 使用建议

写法 适合
Update("col", val) 只改一列
Updates(map[string]interface{}{...}) 改多列;零值也能更新(如把某字段更新成 0)
Updates(结构体) 也行,但注意:结构体零值字段可能被跳过

课设里多字段更新,用 map 往往更不容易踩零值坑

7.4 一定要有 Model + Where 吗?

更新时强烈建议:

复制代码
db.Model(&models.Seat{}).Where("id = ?", sid).Updates(...)

避免误更新范围过大。没有条件的更新非常危险


八、删(Delete)

8.1 按主键删

复制代码
return db.DB.Delete(&models.Movie{}, id).Error

大致:

复制代码
DELETE FROM movies WHERE id = ?;

8.2 按条件删

复制代码
res := db.DB.Where("end_time < ?", before).Delete(&models.Schedule{})
return res.RowsAffected, res.Error

RowsAffected = 影响了多少行,cleaner 打日志会用到。

8.3 软删除?

GORM 支持 DeletedAt 软删除。本票务项目座位/订单等主要是改状态 ,不是软删;别和 Delete 物理删除混为一谈。


九、条件构造:Where / Order / Limit / Count

9.1 动态拼接条件(列表筛选神器)

电影列表:

复制代码
q := movieQuery()
if onlyOn {
    q = q.Where("m.status = ?", models.MovieOn)
}
if keyword != "" {
    kw := "%" + keyword + "%"
    q = q.Where("(m.title LIKE ? OR m.director LIKE ? OR m.actors LIKE ?)", kw, kw, kw)
}
if groupID > 0 {
    q = q.Where("m.group_id = ?", groupID)
}
err := q.Order("m.id DESC").Scan(&list).Error

这就是「有参数才加条件」------对应首页 ?q=?group=1

9.2 Order / Limit

复制代码
q.Order("m.rating_avg DESC, m.rating_count DESC").Limit(limit).Scan(&list)

推荐列表:按评分排序,只取前 N 条。

9.3 Count

复制代码
var n int64
err := db.DB.Model(&models.Seat{}).
    Where("schedule_id = ?", scheduleID).
    Count(&n).Error

用来统计某场次座位数量等。

9.4 Pluck:只取一列

超时订单清理:

复制代码
var ids []int64
tx.Model(&models.Order{}).
    Where("status = ? AND pay_deadline < NOW()", models.OrderPending).
    Pluck("id", &ids)

只抽出 id 列表,再逐个处理。


十、联表查询:Table + Select + Joins + Scan

简单 Find 只适合「单表且字段都在模型里」。

订单要带电影名、影厅名,就要联表。

本项目典型写法:

复制代码
func orderQuery() *gorm.DB {
    return db.DB.Table("orders AS o").
        Select(`o.id,o.order_no,o.user_id,o.schedule_id,
            IFNULL(m.title,'') AS movie_title,
            IFNULL(h.name,'') AS hall_name,
            IFNULL(s.start_time,NOW()) AS start_time,
            o.seat_info,o.amount,o.status,o.pay_deadline,o.created_at,o.paid_at`).
        Joins("LEFT JOIN schedules s ON o.schedule_id=s.id").
        Joins("LEFT JOIN movies m ON s.movie_id=m.id").
        Joins("LEFT JOIN halls h ON s.hall_id=h.id")
}

func GetOrder(id int64) (*models.Order, error) {
    var o models.Order
    err := orderQuery().Where("o.id = ?", id).Scan(&o).Error
    // ...
}

怎么读:

  1. Table("orders AS o"):主表起别名
  2. Select(...):要哪些列,联表字段用 AS movie_title 对上结构体
  3. Joins(...)LEFT JOIN / JOIN
  4. Scan(&o):扫进结构体

电影联分组同理:movies LEFT JOIN movie_groups

这比硬背 Association / Preload 更贴近很多课设的 SQL 思维,也好排查。


十一、链式调用怎么理解?

GORM 很多方法返回的还是 *gorm.DB,所以可以一路点下去:

复制代码
db.DB.
    Where("schedule_id = ?", scheduleID).
    Order("row_no, col_no").
    Find(&list)

可以想成在拼 SQL ,最后 Find/First/Scan/Create... 才真正执行。

注意:

复制代码
q := db.DB.Where("status = ?", 1)
q2 := q.Where("id = ?", 10) // 一般是在原条件下继续加

列表筛选时,把 q 重新赋回去(q = q.Where(...))是本项目的常见写法。


十二、事务 Transaction:多步要么全成要么全败

下单要「建订单 + 锁多个座位」,不能订单建了座位没锁成功还留下半成品。

复制代码
err := db.DB.Transaction(func(tx *gorm.DB) error {
    // 注意:事务里用 tx,不要用 db.DB
    // 1) 锁行并检查座位
    // 2) Create order
    // 3) Updates seats
    return nil // 返回 nil → 提交;返回 error → 回滚
})

规则:

规则 说明
回调返回 nil 提交事务
回调返回 error 回滚
里面用 tx 所有操作挂在同一事务上

支付、取消订单、释放超时订单,本项目同样包在 Transaction


十三、行锁 FOR UPDATE:防超卖的关键写法

复制代码
import "gorm.io/gorm/clause"

err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
    Where("id = ? AND schedule_id = ?", sid, scheduleID).
    First(&seat).Error

生成的 SQL 直觉:

复制代码
SELECT * FROM seats WHERE id=? AND schedule_id=? FOR UPDATE;

含义:在事务中锁定这一行,别人想改同一行会等待,避免两人同时读到「空闲」再一起下单。

配合业务字段(status=锁定lock_user_idlock_until)才完整:

  • 行锁:管并发瞬间
  • 业务锁字段:管支付窗口期内占座

十四、Upsert / 冲突更新(评分场景)

同一用户对同一电影重复评分,希望「有则更新,无则插入」:

复制代码
tx.Clauses(clause.OnConflict{
    // 按唯一键冲突时更新某些列
}).Create(&r)

(具体 Columns 以你们 dao/comment.go 为准。)

这比先查再决定 insert/update,在并发下更干净。


十五、错误处理固定套路(建议背下来)

复制代码
err := db.DB.Where("username = ?", name).First(&u).Error
if err != nil {
    if errors.Is(err, gorm.ErrRecordNotFound) {
        // 业务:用户不存在
        return nil, nil
    }
    // 系统:数据库真出错
    return nil, err
}
return &u, nil

以及执行类:

复制代码
if err := db.DB.Create(u).Error; err != nil {
    return err
}

几乎所有 dao 函数都是:(结果, error)error


十六、一张「看见代码就知道在干嘛」速查表

你想做的事 优先写法
插入 Create / CreateInBatches
主键取一条 First(&m, id)
条件取一条 Where.First
条件取多条 Where.Find
联表自定义列 Table+Select+Joins+Scan
改一列 Model.Where.Update
改多列 Model.Where.Updates(map)
删除 Delete / Where.Delete
统计 Count
抽一列 Pluck
跳过字段 Omit
多步骤原子 Transaction
行锁 Clauses(Locking{UPDATE})
排序/条数 Order / Limit

相关推荐
A15362551 小时前
2026 电商物流系统推荐:多渠道仓配履约数字化选型指南
大数据·数据库·人工智能
ClouGence3 小时前
CloudDM:开源免费!一站式数据库访问、SQL审核、权限与脱敏管控平台
数据库·sql·开源
(轻舟已过万重山)3 小时前
第39章 评估迭代:上线只是开始,迭代才是关键
大数据·数据库·人工智能
oradh3 小时前
Oracle参数文件(PFILE与SPFILE)维护操作总结
数据库·oracle·oracle参数·spfile·pfile
l1t3 小时前
kryonix提交的DuckDB 统一并优化标量执行器基础设施 - #24564 PR
开发语言·数据库·数据仓库·sql
鸽芷咕4 小时前
【金仓数据库征文】从 Oracle 到金仓:一次零误差的数据库国产化迁移实录
数据库·oracle
0566465 小时前
RAG 向量检索:从“查字“到“查意“
数据库·人工智能·学习·oracle
bug嘛我经常写5 小时前
dbf文件UTF-8转GBK编码,以及两编码文件互转
数据库·python
知行合一。。。5 小时前
LangGraph--03--本地服务与 Studio 调试
数据库