传统的错误处理方式

在 GORM 的使用过程中,gorm.ErrNotFound 是最基础且使用最广泛的错误类型。作为开发者,我们习惯性地使用这种方式来处理查询未找到记录的情况:

1
2
3
if errors.Is(err, gorm.ErrNotFound) {
    // 处理记录未找到的情况
}

这种错误处理方式简单直接,已经成为了 GORM 用户的一种常规实践。

v1.25 版本的新变化

新增的 ErrDuplicateKey

在 v1.25 版本中,GORM 引入了新的错误类型 ErrDuplicateKey,用于处理唯一键冲突的场景。这本应该是一个受欢迎的改进,但实际使用中却存在一些问题。

研发同学在升级了 GORM 之后,发现 GORM 支持了 ErrDuplicateKey 这个 error,于是直接在项目中使用

1
2
3
4
5
6
7
if err!=nil {
    if errors.Is(err, gorm.ErrDuplicateKey) {
        // 业务处理
    } else {
        // 错误日志
    }
}

在实际运行中,却走到了 else 分支中,导致了一些业务异常。

历史背景

在此之前,开发者需要判断唯一键冲突时,需要进行如下处理:

1
2
3
4
5
var mysqlErr *mysql.MySQLError

if errors.As(err, &mysqlErr) && mysqlErr.Number == 1062 {
    // 处理唯一键冲突
}

这个问题在 GitHub Issue #4037 中被提出并讨论。

Translate 机制

#PR 6004 中,有贡献者在 GORM 中新增了 ErrDuplicateKey,并抽象出 ErrorTranslate 接口。当对应的 driver 中当出现 dialect 错误并且开启 TranslateError 配置后,会尝试转成 GORM 内部的错误。

实现原理

在每个 driver 中实现一个 Translate 方法,在 MySQL driver 的实现中,则是维护一个 errCodes map,用于将 mysql 标准的错误码映射成 GORM 的内部 error.

以 MySQL 为例,支持 Translate 的 MySQL 错误码为:

  • 1062
  • 1451
  • 1452

具体定义见 https://github.com/go-gorm/mysql/blob/master/error_translator.go

TranslateError 配置的引入

PR 6004 中提到,为解决原始错误信息可能丢失的问题,GORM 引入了 TranslateError 配置项。这导致了一个现象:

如果没有启用该配置项,以下判断将无法生效

1
errors.Is(err, gorm.ErrDuplicateKey)

使用 ErrDuplicateKey 需要显示启动 TranslateError 配置:

1
2
3
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
    TranslateError: true
})

带来的困扰

这种设计给 GORM 的老用户带来了困扰:

  1. 违反了用户直觉 - 用户习惯了直接使用 errors.Is 来判断 GORM 的错误
  2. 需要额外的配置 - 必须显式启用 TranslateError 才能使用新的错误类型
  3. 不一致的体验 - ErrNotFoundErrDuplicateKey 的使用方式不同
  4. 开启配置后,会直接读取 map 中的 error,导致原始 error 丢失