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 作为聚合根,通过 Edge 关联 PaymentItem 和 RefundRecord 两个子实体;索引和注解作为元数据层,为查询优化和代码生成提供支撑。所有 Field 类型、Edge 方向、索引策略都严格遵循聚合边界的设计原则。