2. Ent ORM 基础:Ent 简介、Schema 定义、代码生成、CRUD 操作

2.1 Ent 简介:面向支付的实体框架

Ent 是 Facebook 开源的一款 Go 语言实体框架(Entity Framework),专为复杂数据模型与高并发场景设计。在支付中间件工程中,Ent 的核心价值体现在以下三个维度:

  • 静态类型与代码生成:通过 entc 编译器从 Schema 定义生成强类型代码,杜绝运行时反射与类型断言,编译期即可捕获字段名拼写、关联关系错误。
  • 图遍历与关联查询:支付领域天然存在订单→支付单→退款单→对账记录的网状关联,Ent 的 Eager Loading 与 Edge Traversal 可一次查询完成多层嵌套加载。
  • 事务与钩子(Hooks):Ent 提供 Tx 事务闭包与生命周期钩子,适合在支付核心链路中嵌入审计、幂等、分库分表逻辑。
选型建议:若团队已采用 gRPC + Protobuf 微服务体系,Ent 的 Schema 与 Protobuf 可共用字段约束,减少模型层重复定义。支付场景下优先选择 Ent 而非 GORM,因为 Ent 的代码生成能提前暴露字段变更风险。

2.2 Schema 定义:支付核心模型

支付中间件中最核心的实体是 支付订单(PaymentOrder) 与 交易流水(Transaction)。以下 Schema 定义展示了字段、索引、唯一约束与关联边:

// ent/schema/payment_order.go
package schema

import (
    "entgo.io/ent"
    "entgo.io/ent/schema/field"
    "entgo.io/ent/schema/index"
    "entgo.io/ent/schema/edge"
)

type PaymentOrder struct {
    ent.Schema
}

func (PaymentOrder) Fields() []ent.Field {
    return []ent.Field{
        field.String("order_no").MaxLen(32).Unique().
            Comment("业务订单号,全局唯一"),
        field.Int64("merchant_id").Comment("商户ID"),
        field.Int64("amount").Comment("支付金额,单位分"),
        field.Int8("status").Default(0).
            Comment("0-待支付 1-支付中 2-成功 3-失败 4-退款中"),
        field.Time("created_at"),
        field.Time("updated_at"),
    }
}

func (PaymentOrder) Indexes() []ent.Index {
    return []ent.Index{
        index.Fields("merchant_id", "status"),
        index.Fields("order_no").Unique(),
    }
}

func (PaymentOrder) Edges() []ent.Edge {
    return []ent.Edge{
        edge.To("transactions", Transaction.Type),
    }
}
// ent/schema/transaction.go
package schema

import (
    "entgo.io/ent"
    "entgo.io/ent/schema/field"
    "entgo.io/ent/schema/edge"
)

type Transaction struct {
    ent.Schema
}

func (Transaction) Fields() []ent.Field {
    return []ent.Field{
        field.String("trans_no").MaxLen(64).Unique().
            Comment("支付渠道流水号"),
        field.Int64("payment_order_id").Comment("关联支付订单ID"),
        field.Int64("channel_fee").Default(0).
            Comment("渠道手续费,单位分"),
        field.String("channel").MaxLen(16).
            Comment("渠道:wechat/alipay/unionpay"),
        field.Time("trans_time"),
    }
}

func (Transaction) Edges() []ent.Edge {
    return []ent.Edge{
        edge.From("payment_order", PaymentOrder.Type).
            Ref("transactions").
            Unique().Field("payment_order_id").
            Required(),
    }
}
字段/约束 支付场景说明 Ent 实现方式
order_no 唯一索引 防止重复下单,幂等拦截 field.String().Unique()
merchant_id + status 联合索引 商户后台按状态查询订单列表 index.Fields("merchant_id", "status")
payment_order_id 外键 支付订单→交易流水 1:N 关联 edge.From().Ref().Field().Required()
amount 单位分 避免浮点数精度丢失 field.Int64()
支付字段避坑:金额字段禁止使用 float64 或 field.Float(),必须使用 int64 以分为单位存储。渠道流水号 trans_no 必须设置唯一索引,用于对账时的幂等匹配。

2.3 代码生成:entc 工作流

Ent 通过 entc 命令行工具从 Schema 目录生成实体代码、客户端 API 与 SQL 迁移文件。标准生成流程如下:

  1. 初始化项目:go run entgo.io/ent/cmd/entc init PaymentOrder Transaction
  2. 编写 Schema:在 ent/schema/ 目录下定义字段、索引、边。
  3. 执行生成:go generate ./ent 或 entc generate ./ent/schema
  4. 检查产出:生成 ent/client.go、ent/paymentorder.go、ent/transaction.go 等文件。
生成产物清单:
  • ent/client.go — 数据库客户端,包含事务、批量操作入口
  • ent/paymentorder_create.go — 创建订单的链式构造器
  • ent/paymentorder_query.go — 查询订单的 Where/Order/Limit 方法
  • ent/migrate/ — 自动生成的 SQL 迁移文件(支持 MySQL/PostgreSQL/SQLite)
Ent 代码生成流程 Schema 定义 ent/schema/*.go entc generate go generate ./ent 生成客户端 ent/client.go 迁移文件 ent/migrate/*.sql 实体 CRUD paymentorder.go 💡 每次修改 Schema 后必须重新执行 entc generate,否则生成的客户端代码与数据库表结构不一致。 建议将 go generate ./ent 加入 CI 流水线,确保合并前 Schema 与代码同步。

2.4 CRUD 操作:支付订单生命周期

以下代码演示了使用 Ent 客户端完成支付订单的创建、查询、更新与删除操作。所有操作均基于生成的强类型 API。

2.4.1 创建支付订单

func CreatePaymentOrder(ctx context.Context, client *ent.Client) (*ent.PaymentOrder, error) {
    return client.PaymentOrder.
        Create().
        SetOrderNo("PO20250321001").
        SetMerchantID(10086).
        SetAmount(2999).   // 29.99 元
        SetStatus(0).      // 待支付
        SetCreatedAt(time.Now()).
        SetUpdatedAt(time.Now()).
        Save(ctx)
}

2.4.2 查询订单并加载关联流水

func QueryOrderWithTransactions(ctx context.Context, client *ent.Client, orderNo string) (*ent.PaymentOrder, error) {
    return client.PaymentOrder.
        Query().
        Where(paymentorder.OrderNoEQ(orderNo)).
        WithTransactions(func(q *ent.TransactionQuery) {
            q.Order(ent.Asc(transaction.FieldTransTime))
        }).
        Only(ctx)
}

2.4.3 更新订单状态(支付成功回调)

func UpdateOrderStatus(ctx context.Context, client *ent.Client, orderID int, newStatus int8) error {
    return client.PaymentOrder.
        UpdateOneID(orderID).
        SetStatus(newStatus).
        SetUpdatedAt(time.Now()).
        Exec(ctx)
}

2.4.4 事务内创建订单与流水

func CreateOrderWithTransaction(ctx context.Context, client *ent.Client) error {
    tx, err := client.Tx(ctx)
    if err != nil {
        return err
    }
    defer tx.Rollback() // 出错时回滚

    order, err := tx.PaymentOrder.
        Create().
        SetOrderNo("PO20250321002").
        SetMerchantID(10086).
        SetAmount(5000).
        SetStatus(1). // 支付中
        SetCreatedAt(time.Now()).
        SetUpdatedAt(time.Now()).
        Save(ctx)
    if err != nil {
        return err
    }

    _, err = tx.Transaction.
        Create().
        SetTransNo("TXN202503210001").
        SetPaymentOrderID(order.ID).
        SetChannel("wechat").
        SetChannelFee(30).
        SetTransTime(time.Now()).
        Save(ctx)
    if err != nil {
        return err
    }

    return tx.Commit()
}
操作 Ent 方法 支付场景示例
创建 client.PaymentOrder.Create().SetXxx().Save(ctx) 用户发起支付,生成待支付订单
查询 .Query().Where().WithTransactions().Only(ctx) 查询订单详情并加载所有交易流水
更新 .UpdateOneID(id).SetStatus().Exec(ctx) 支付回调更新订单状态为成功
删除 .DeleteOneID(id).Exec(ctx) 超时未支付订单软删除(建议用状态标记)
事务 client.Tx(ctx) + tx.Commit() 创建订单 + 流水必须原子提交
支付事务要点:支付核心链路中,订单创建与流水写入必须在同一个数据库事务内完成。Ent 的 Tx 对象与普通 Client 接口完全一致,只需将 client 替换为 tx 即可。事务提交前务必调用 defer tx.Rollback() 防止异常泄漏连接。
支付订单 CRUD 生命周期 待支付 支付中 支付成功 支付失败 退款中 🔄 创建订单 → 调用支付渠道 → 回调更新状态 → 失败可重试或退款 Ent 的 UpdateOneID 配合事务可保证状态变更原子性,避免并发覆盖。

2.5 本章小结

  • Ent 通过 Schema 定义 + 代码生成提供强类型 ORM,适合支付场景的高可靠性要求。
  • 支付核心模型包括 PaymentOrder 与 Transaction,通过 Edge 建立 1:N 关联。
  • 代码生成流程为:Schema → entc generate → 客户端 + 迁移文件 + 实体 CRUD。
  • CRUD 操作全部基于生成的链式 API,事务支持保证订单与流水的原子写入。