错误处理和事务

GORM 的传统 API 通常通过 .Error 获取错误。

事务用来保证一组数据库操作要么都成功,要么都失败。

一、基本错误处理

var user User

err := db.First(&user, 1).Error
if err != nil {
    return err
}

新增、更新、删除也一样:

err := db.Create(&user).Error
if err != nil {
    return err
}

二、记录不存在

var user User

err := db.First(&user, 100).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
    fmt.Println("用户不存在")
    return nil
}
if err != nil {
    return err
}

需要导入:

import (
    "errors"
    "gorm.io/gorm"
)

三、检查影响行数

result := db.Model(&User{}).
    Where("id = ?", 1).
    Update("status", "disabled")

if result.Error != nil {
    return result.Error
}

if result.RowsAffected == 0 {
    return gorm.ErrRecordNotFound
}

RowsAffected 适合判断更新或删除是否真的命中了数据。

四、为什么需要事务

比如创建文章时,同时要创建文章标签关系:

  1. 插入文章。
  2. 插入文章和标签的关系。

如果第 1 步成功,第 2 步失败,就会出现不完整数据。

事务可以保证这两步作为一个整体。

五、使用 db.Transaction

推荐使用 db.Transaction

err := db.Transaction(func(tx *gorm.DB) error {
    article := Article{
        UserID:  1,
        Title:   "GORM 事务",
        Content: "事务示例",
        Status:  "published",
    }

    if err := tx.Create(&article).Error; err != nil {
        return err
    }

    comment := Comment{
        ArticleID: article.ID,
        UserID:    1,
        Content:   "第一条评论",
    }

    if err := tx.Create(&comment).Error; err != nil {
        return err
    }

    return nil
})

if err != nil {
    return err
}

规则:

  • 函数返回 nil,事务提交。
  • 函数返回错误,事务回滚。
  • 事务里面必须使用 tx,不要继续用外面的 db

六、手动事务

也可以手动控制:

tx := db.Begin()
if tx.Error != nil {
    return tx.Error
}

if err := tx.Create(&user).Error; err != nil {
    tx.Rollback()
    return err
}

if err := tx.Create(&article).Error; err != nil {
    tx.Rollback()
    return err
}

if err := tx.Commit().Error; err != nil {
    return err
}

优先用 db.Transaction,代码更短,也更不容易忘记回滚。

七、库存扣减示例

err := db.Transaction(func(tx *gorm.DB) error {
    result := tx.Model(&Product{}).
        Where("id = ? AND stock > 0", productID).
        Update("stock", gorm.Expr("stock - ?", 1))

    if result.Error != nil {
        return result.Error
    }
    if result.RowsAffected == 0 {
        return errors.New("库存不足")
    }

    order := Order{
        ProductID: productID,
        UserID:    userID,
    }

    if err := tx.Create(&order).Error; err != nil {
        return err
    }

    return nil
})

这里用一条更新语句完成库存判断和扣减,避免先查库存再扣减导致并发问题。

八、事务使用建议

场景是否需要事务
单条查询通常不需要
单条简单更新通常不需要显式事务
创建主表和多条子表需要
下单、扣库存、写订单需要
多张表状态必须一致需要

事务不要包太久,不要在事务里调用慢接口或等待用户输入。