4. IsNotFound 实战:查询单条记录时如何判断记录不存在

在 Ent 的日常开发中,QueryOne 是最常用的查询模式之一。当查询条件不匹配任何记录时,Ent 默认返回 *NotFoundError。如果不能正确断言这个错误,程序可能会因为空指针解引用或未处理的错误而崩溃。本章将深入 IsNotFound 的实战用法,帮助你写出健壮的查询逻辑。

核心原则: 永远不要假设 QueryOne 一定能返回记录。每次调用后,都应使用 IsNotFound 进行分支处理。

4.1 基础用法:从 User 查询开始

假设我们有一个 User 模型,需要通过 ID 查询用户。如果用户不存在,我们希望返回一个明确的错误提示,而不是让程序 panic。

package main

import (
    "context"
    "fmt"
    "log"

    "<your-project>/ent"
    "<your-project>/ent/user"
    _ "github.com/mattn/go-sqlite3"
)

func main() {
    client, err := ent.Open("sqlite3", "file:ent?mode=memory&cache=shared&_fk=1")
    if err != nil {
        log.Fatalf("failed opening connection to sqlite: %v", err)
    }
    defer client.Close()
    ctx := context.Background()

    // 创建一个测试用户
    client.User.Create().SetName("Alice").SaveX(ctx)

    // 场景1:查询存在的用户
    u, err := client.User.Query().Where(user.ID(1)).Only(ctx)
    if err != nil {
        if ent.IsNotFound(err) {
            fmt.Println("用户 1 不存在")
        } else {
            log.Fatalf("查询失败: %v", err)
        }
    } else {
        fmt.Printf("找到用户: %s\n", u.Name)
    }

    // 场景2:查询不存在的用户
    u, err = client.User.Query().Where(user.ID(999)).Only(ctx)
    if err != nil {
        if ent.IsNotFound(err) {
            fmt.Println("用户 999 不存在")
        } else {
            log.Fatalf("查询失败: %v", err)
        }
    } else {
        fmt.Printf("找到用户: %s\n", u.Name)
    }
}
实战要点: Only(ctx) 方法在查询到 0 条或超过 1 条记录时都会返回错误。当记录数为 0 时,返回的错误一定可以通过 IsNotFound 断言为 true。

4.2 IsNotFound 与 First/Only 的配合

Ent 提供了 First 和 Only 两种单条查询方法,它们对 IsNotFound 的行为略有不同:

方法 记录数 = 0 记录数 = 1 记录数 > 1
First(ctx) 返回 *NotFoundError,IsNotFound 为 true 返回记录,err 为 nil 返回第一条记录,err 为 nil
Only(ctx) 返回 *NotFoundError,IsNotFound 为 true 返回记录,err 为 nil 返回 *NotSingularError,IsNotFound 为 false

从表格可以看出:

  • 使用 First:当你只关心“有没有记录”,且不介意多条记录时,使用 First + IsNotFound 判断是否存在。
  • 使用 Only:当你要求查询结果必须唯一时,使用 Only + IsNotFound 判断不存在,同时 Only 还会帮你校验唯一性。

4.3 错误断言流程图

下面的流程图展示了查询单条记录时完整的错误断言路径:

单条查询错误断言流程 调用 Query().Only(ctx) err != nil? 是 进入错误处理 IsNotFound(err)? 是 记录 不存在 否 其他 数据库错误 否 使用返回的记录 完成查询处理

4.4 实战模式:封装 Repository 方法

在实际项目中,我们通常会将 Ent 查询封装在 Repository 层。下面是一个推荐的模式:

// UserRepository 封装用户相关的数据库操作
type UserRepository struct {
    client *ent.Client
}

// GetByID 根据 ID 获取用户,如果不存在返回 nil, nil
func (r *UserRepository) GetByID(ctx context.Context, id int) (*ent.User, error) {
    u, err := r.client.User.Query().Where(user.ID(id)).Only(ctx)
    if err != nil {
        if ent.IsNotFound(err) {
            return nil, nil  // 返回 nil 表示不存在,上层无需处理错误
        }
        return nil, fmt.Errorf("查询用户 %d 失败: %w", id, err)
    }
    return u, nil
}

// MustGetByID 根据 ID 获取用户,如果不存在返回自定义错误
func (r *UserRepository) MustGetByID(ctx context.Context, id int) (*ent.User, error) {
    u, err := r.client.User.Query().Where(user.ID(id)).Only(ctx)
    if err != nil {
        if ent.IsNotFound(err) {
            return nil, fmt.Errorf("用户 %d 不存在", id)
        }
        return nil, fmt.Errorf("查询用户 %d 失败: %w", id, err)
    }
    return u, nil
}
常见陷阱: 不要将 IsNotFound 与 errors.Is 混用。Ent 的 NotFoundError 是一个结构体指针,errors.Is 无法正确匹配。始终使用 ent.IsNotFound(err) 函数进行断言。

4.5 多条件查询中的 IsNotFound

当查询条件包含多个字段时,IsNotFound 的行为完全一致。只要查询结果为空,就会返回 *NotFoundError。

// 查询名为 "Alice" 且年龄为 30 的用户
u, err := client.User.Query().
    Where(
        user.Name("Alice"),
        user.Age(30),
    ).
    Only(ctx)

if ent.IsNotFound(err) {
    // 没有找到名为 Alice 且年龄为 30 的用户
    fmt.Println("未找到匹配的用户")
} else if err != nil {
    // 其他数据库错误
    log.Fatalf("查询失败: %v", err)
} else {
    fmt.Printf("找到用户: %s (年龄: %d)\n", u.Name, u.Age)
}

4.6 性能与最佳实践

使用 IsNotFound 时,有几个性能相关的要点需要注意:

  • 避免不必要的查询:如果你只需要判断记录是否存在,而不是获取记录内容,优先使用 Exist(ctx) 方法,它返回 bool,性能更好。
  • 善用 Only 的约束:在业务逻辑要求唯一性的场景(如根据邮箱查询用户),使用 Only 可以同时获得“不存在”和“不唯一”的错误检查。
  • 日志记录:在生产环境中,当 IsNotFound 为 true 时,建议记录一条 debug 级别的日志,方便排查问题。
总结: IsNotFound 是 Ent 中最常用的错误断言函数。掌握它与 First、Only 的配合,能够让你写出既健壮又清晰的查询代码。记住:每次单条查询后,都要问自己——“如果记录不存在,我的代码会怎样?”