4. Ent Schema 设计基础:定义 Field 类型、定义 Edge 关系、Schema 的注解与索引

在支付订单聚合的领域边界内,Ent Schema 是领域模型的“代码化映射”。本章聚焦三个核心设计动作:Field 类型决定聚合根属性的存储形态;Edge 关系定义聚合内实体之间的关联与约束;注解与索引则用于补充元数据、优化查询路径。三者共同构成聚合边界的“骨架”。

设计原则:Schema 中的每个字段都应直接对应领域概念,避免将数据库优化细节(如冗余字段、反范式设计)直接暴露在 Field 定义中。Edge 关系应严格限制在聚合边界内部,跨聚合的关联应通过领域服务或事件驱动。

4.1 Field 类型:领域属性的精确映射

Ent 提供了丰富的内置 Field 类型,覆盖支付订单聚合中的常见数据类型。选择正确的 Field 类型,不仅影响存储效率,更决定了领域行为的表达力。

领域属性示例 推荐 Field 类型 说明
订单 ID UUID 全局唯一标识,避免自增 ID 暴露业务量
支付金额 Float64 或 Decimal 金额精度要求高时使用 Decimal(需扩展)
订单状态 Enum 强类型约束,避免魔法字符串
创建时间 Time 自动记录,配合 Immutable 防止篡改
支付渠道返回码 String 长度不限,用于记录外部系统原始响应
商品快照 JSON 存储非结构化数据,避免多表关联

以下是一个支付订单聚合根 Schema 的 Field 定义示例:

// ent/schema/payment_order.go
package schema

import (
    "time"
    "github.com/google/uuid"
    "entgo.io/ent"
    "entgo.io/ent/schema/field"
)

// PaymentOrder 支付订单聚合根
type PaymentOrder struct {
    ent.Schema
}

func (PaymentOrder) Fields() []ent.Field {
    return []ent.Field{
        field.UUID("id", uuid.UUID{}).
            Default(uuid.New).
            Immutable(),
        field.String("order_no").
            Unique().
            MaxLen(32).
            Comment("业务订单号,全局唯一"),
        field.Float("amount").
            Positive().
            Comment("支付金额,单位:元"),
        field.Enum("status").
            Values("pending", "paid", "failed", "refunded").
            Default("pending").
            Comment("订单状态"),
        field.Time("created_at").
            Default(time.Now).
            Immutable(),
        field.Time("updated_at").
            Default(time.Now).
            UpdateDefault(time.Now),
        field.JSON("extra", map[string]interface{}{}).
            Optional().
            Comment("扩展字段,用于存储渠道返回的额外信息"),
    }
}
Field 设计要点:
  • Immutable:对创建后不可变的字段(如 ID、创建时间)添加该修饰,防止意外更新。
  • Optional:对非核心字段(如扩展信息)标记可选,避免领域对象构造时强制填充。
  • Validators:利用 Positive()、MaxLen() 等内置验证器,在 Schema 层即保证数据完整性。

4.2 Edge 关系:聚合内部的关联约束

Edge 是 Ent 中定义实体间关系的核心机制。在支付订单聚合中,Edge 必须严格遵循“聚合内部强一致,聚合之间最终一致”的原则。以下是一个支付订单与支付明细的 Edge 关系示例:

// ent/schema/payment_order.go
func (PaymentOrder) Edges() []ent.Edge {
    return []ent.Edge{
        edge.To("items", PaymentItem.Type).
            Comment("订单包含的支付明细列表"),
        edge.To("refunds", RefundRecord.Type).
            Comment("订单关联的退款记录"),
    }
}

// ent/schema/payment_item.go
func (PaymentItem) Edges() []ent.Edge {
    return []ent.Edge{
        edge.From("order", PaymentOrder.Type).
            Ref("items").
            Unique().
            Required().
            Comment("所属支付订单"),
    }
}

Edge 关系的关键属性对比:

属性 用途 支付聚合中的典型场景
Unique() 标记为唯一关联(1:1 或 N:1) 一个支付订单只能有一个“当前处理中的退款”
Required() 关联不能为空 支付明细必须属于某个订单
Ref() 建立双向引用的反向边 从明细反向查询所属订单
Through() 通过中间表建立多对多关系 订单与优惠券的多对多关联
聚合边界警告:不要在支付订单 Schema 中直接定义指向“用户”或“商品”的 Edge。用户和商品属于其他聚合,应通过 order.user_id 字段(值对象)或领域服务来引用,而非通过 Ent Edge 建立数据库外键关联。这能防止聚合之间的强耦合。

4.3 Schema 的注解与索引

注解(Annotations)用于向代码生成器或运行时注入元数据;索引(Indexes)则用于优化查询性能,同时可以在数据库层面实现唯一约束。

4.3.1 注解:元数据的声明式注入

Ent 内置了多种注解,如 entgo.io/ent/schema/annotation 包中的 Annotation 接口。自定义注解可用于传递领域特定元数据。

// ent/schema/payment_order.go
import (
    "entgo.io/ent/schema"
    "entgo.io/ent/schema/annotation"
)

func (PaymentOrder) Annotations() []schema.Annotation {
    return []schema.Annotation{
        annotation.Comment("支付订单聚合根,包含支付明细和退款记录"),
        // 自定义注解:标记该 Schema 属于支付上下文
        PaymentAnnotation{
            Domain: "payment",
            Aggregate: "order",
        },
    }
}

// 自定义注解结构体
type PaymentAnnotation struct {
    Domain    string
    Aggregate string
}

func (a PaymentAnnotation) Name() string {
    return "PaymentAnnotation"
}

4.3.2 索引:查询性能与唯一约束

索引定义在 Schema 的 Indexes() 方法中。支付订单聚合中常见的索引场景包括:

func (PaymentOrder) Indexes() []ent.Index {
    return []ent.Index{
        // 唯一索引:保证业务订单号不重复
        index.Fields("order_no").
            Unique(),
        // 复合索引:按状态和创建时间查询待处理订单
        index.Fields("status", "created_at").
            Comment("加速待支付订单的轮询查询"),
        // 边缘索引:按用户 ID 查询该用户的所有订单
        index.Fields("user_id"),
    }
}
索引类型 语法 支付场景示例
单字段索引 index.Fields("user_id") 按用户查询订单列表
复合索引 index.Fields("status", "created_at") 查询“待支付”且“超时”的订单
唯一索引 index.Fields("order_no").Unique() 保证业务订单号全局唯一
边缘索引 index.Edges("items") 通过支付明细反向查询订单(较少用)
索引设计建议:优先为 WHERE 子句中频繁出现的字段建立索引;复合索引的字段顺序应遵循“高选择性在前”原则;唯一索引应直接对应领域不变量(如订单号唯一),而非数据库层面的随意约束。

4.4 综合示例:支付订单聚合的完整 Schema

以下将 Field、Edge、注解与索引整合为一个完整的支付订单聚合 Schema:

// ent/schema/payment_order.go
package schema

import (
    "time"
    "github.com/google/uuid"
    "entgo.io/ent"
    "entgo.io/ent/schema/field"
    "entgo.io/ent/schema/edge"
    "entgo.io/ent/schema/index"
    "entgo.io/ent/schema/annotation"
)

type PaymentOrder struct {
    ent.Schema
}

func (PaymentOrder) Fields() []ent.Field {
    return []ent.Field{
        field.UUID("id", uuid.UUID{}).Default(uuid.New).Immutable(),
        field.String("order_no").Unique().MaxLen(32),
        field.Float("amount").Positive(),
        field.Enum("status").Values("pending", "paid", "failed", "refunded").Default("pending"),
        field.String("user_id").Comment("用户标识,值对象引用"),
        field.Time("created_at").Default(time.Now).Immutable(),
        field.Time("updated_at").Default(time.Now).UpdateDefault(time.Now),
        field.JSON("extra", map[string]interface{}{}).Optional(),
    }
}

func (PaymentOrder) Edges() []ent.Edge {
    return []ent.Edge{
        edge.To("items", PaymentItem.Type),
        edge.To("refunds", RefundRecord.Type),
    }
}

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

func (PaymentOrder) Annotations() []schema.Annotation {
    return []schema.Annotation{
        annotation.Comment("支付订单聚合根"),
    }
}
支付订单聚合 Schema 结构图 PaymentOrder (聚合根) id: UUID (Immutable) order_no: String (Unique) amount: Float64 (Positive) status: Enum (pending/paid/...) user_id: String (值对象) created_at: Time (Immutable) extra: JSON (Optional) PaymentItem (子实体) id: UUID item_name: String quantity: Int unit_price: Float64 RefundRecord (子实体) id: UUID refund_amount: Float64 reason: String status: Enum 索引定义 • order_no (Unique) • status + created_at (复合) • user_id (单字段) 注解 @Comment("支付订单聚合根") Edge: items Edge: refunds

上图展示了支付订单聚合的 Schema 结构:PaymentOrder 作为聚合根,通过 Edge 关联 PaymentItem 和 RefundRecord 两个子实体;索引和注解作为元数据层,为查询优化和代码生成提供支撑。所有 Field 类型、Edge 方向、索引策略都严格遵循聚合边界的设计原则。