Ent 基础回顾:Schema 定义、CRUD 操作、Client 与 Tx 的创建

在深入事务边界与可观测性之前,必须牢固掌握 Ent 的核心基础。本章将系统回顾 Schema 定义、CRUD 操作以及 Client 与 Tx 的创建方式,为后续事务边界的实战打下坚实的地基。

1. Schema 定义:数据模型的蓝图

Ent 的 Schema 是数据模型的声明式定义,位于 ent/schema/ 目录下。每个 Schema 对应数据库中的一张表,通过 Go 代码描述字段、索引、边(关系)和注解。

1.1 字段(Fields)

字段定义实体的属性,支持多种数据类型和可选配置:

字段类型 Go 类型 常用配置
String string Unique(), NotEmpty(), Default("")
Int int Min(0), Max(100), Default(0)
Float float64 Precision(10,2), Default(0.0)
Bool bool Default(false)
Time time.Time Default(time.Now), Immutable()
JSON map[string]interface{} Optional()
Enum string Values("active", "inactive")
实战要点: 使用 Immutable() 标记创建后不可修改的字段(如创建时间);使用 Annotations 为字段添加元数据,用于代码生成或可观测性标签。

1.2 边(Edges)

边定义实体之间的关联关系,支持一对一、一对多、多对多:

// ent/schema/user.go
func (User) Edges() []ent.Edge {
    return []ent.Edge{
        edge.To("posts", Post.Type),
        edge.From("group", Group.Type).
            Ref("users").
            Unique(),
    }
}

1.3 索引(Indexes)

索引用于加速查询和保证唯一性:

func (User) Indexes() []ent.Index {
    return []ent.Index{
        index.Fields("email").Unique(),
        index.Fields("name", "age"),
    }
}

2. CRUD 操作:数据生命周期的核心

Ent 生成的客户端 API 提供了类型安全的 CRUD 方法,无需手写 SQL。

2.1 创建(Create)

// 单条创建
u, err := client.User.
    Create().
    SetName("Alice").
    SetAge(30).
    SetEmail("alice@example.com").
    Save(ctx)

// 批量创建
users, err := client.User.
    CreateBulk(
        client.User.Create().SetName("Bob").SetAge(25),
        client.User.Create().SetName("Carol").SetAge(28),
    ).
    Save(ctx)

2.2 查询(Query)

// 按 ID 查询
u, err := client.User.Get(ctx, 1)

// 条件查询
users, err := client.User.
    Query().
    Where(user.Name("Alice")).
    All(ctx)

// 关联查询(Eager Loading)
users, err := client.User.
    Query().
    WithPosts().
    All(ctx)

2.3 更新(Update)

// 单条更新
u, err := client.User.
    UpdateOneID(1).
    SetAge(31).
    Save(ctx)

// 批量更新
n, err := client.User.
    Update().
    Where(user.AgeGT(30)).
    SetStatus("vip").
    Save(ctx)

2.4 删除(Delete)

// 单条删除
err := client.User.
    DeleteOneID(1).
    Exec(ctx)

// 批量删除
n, err := client.User.
    Delete().
    Where(user.AgeLT(18)).
    Exec(ctx)
性能提示: 批量操作(CreateBulk、Update、Delete)会合并为单条 SQL 语句,显著减少网络往返次数。

3. Client 与 Tx:事务与非事务的基石

Ent 提供两种操作模式:Client(自动提交)和 Tx(手动事务)。理解两者的区别是掌握事务边界的第一步。

3.1 Client:自动提交模式

每个 Save() 或 Exec() 调用都会立即提交到数据库,适合单条操作或无需原子性的场景。

// 创建数据库连接
client, err := ent.Open("postgres", "host=localhost port=5432 user=postgres dbname=mydb sslmode=disable")

// 自动迁移
if err := client.Schema.Create(ctx); err != nil {
    log.Fatalf("failed creating schema resources: %v", err)
}

// 每条 Save 都是独立事务
u1, _ := client.User.Create().SetName("Alice").Save(ctx)
u2, _ := client.User.Create().SetName("Bob").Save(ctx)

3.2 Tx:手动事务模式

使用 Tx 将多个操作包裹在同一个数据库事务中,保证原子性:

// 开启事务
tx, err := client.Tx(ctx)
if err != nil {
    return err
}

// 在事务内执行操作
u, err := tx.User.Create().SetName("Alice").Save(ctx)
if err != nil {
    tx.Rollback()
    return err
}

p, err := tx.Post.Create().SetTitle("Hello").SetAuthorID(u.ID).Save(ctx)
if err != nil {
    tx.Rollback()
    return err
}

// 提交事务
if err := tx.Commit(); err != nil {
    return err
}
常见陷阱: 事务内的所有操作必须使用 tx 而非 client,否则操作会在事务外执行,导致原子性失效。务必检查每个调用的接收者是否为 tx。

3.3 Client 与 Tx 的对比

特性 Client Tx
事务边界 单条操作自动提交 手动控制 Commit/Rollback
原子性 仅单条操作 跨多条操作
性能 每条操作一次网络往返 批量操作合并提交
适用场景 简单查询、单条写入 多表关联写入、数据一致性要求高
可观测性 每条操作独立追踪 整个事务作为单一追踪单元

4. 核心流程可视化

下图展示了 Client 与 Tx 在数据库操作中的核心流程差异:

Client 与 Tx 操作流程对比 Client 与 Tx 操作流程对比 Client 模式(自动提交) 操作 1:Create().Save() 自动提交事务 1 操作 2:Update().Save() 自动提交事务 2 Tx 模式(手动事务) client.Tx() 开启事务 操作 1:tx.Create().Save() 操作 2:tx.Update().Save() tx.Commit() 统一提交 关键区别:Client 模式每条操作独立事务;Tx 模式所有操作在同一个事务中,保证原子性

5. 事务边界实战:从 Schema 到 Tx 的完整链路

将 Schema 定义、CRUD 操作与事务模式串联起来,形成一个完整的实战链路:

  1. 定义 Schema:在 ent/schema/ 中声明字段、边和索引。
  2. 生成代码:运行 go generate ./ent 生成类型安全的客户端 API。
  3. 创建 Client:通过 ent.Open() 建立数据库连接。
  4. 执行迁移:调用 client.Schema.Create() 同步表结构。
  5. 选择模式:根据业务需求选择 Client(自动提交)或 Tx(手动事务)。
  6. 执行 CRUD:使用生成的 API 进行数据操作。
  7. 提交/回滚:Tx 模式下调用 Commit() 或 Rollback()。