3. Schema 基础:Schema 定义、字段类型、字段标签、必填与可选字段、默认值
在 Ent 中,Schema 是数据模型的唯一真相来源。每个 Schema 对应数据库中的一张表,通过 Go 代码声明字段、索引、边(关系)和注解。本章聚焦 Schema 最核心的组成部分:定义结构、字段类型体系、字段标签、必填与可选字段以及默认值。掌握这些基础,你就能用 Ent 的声明式风格快速搭建出符合业务需求的数据库模型。
3.1 Schema 定义结构
一个标准的 Ent Schema 文件位于 ent/schema/ 目录下,每个文件对应一个模型。Schema 结构由 Fields()、Edges()、Indexes()、Mixin() 和 Annotations() 五个方法组成。其中 Fields() 是唯一必须实现的方法。
package schema
import (
"entgo.io/ent"
"entgo.io/ent/schema/field"
)
// User 持有用户模型的定义。
type User struct {
ent.Schema
}
// Fields 返回用户模型的字段。
func (User) Fields() []ent.Field {
return []ent.Field{
field.String("name"),
field.Int("age"),
}
}
// Edges 返回用户模型的关系。
func (User) Edges() []ent.Edge {
return nil
}
// Indexes 返回用户模型的索引。
func (User) Indexes() []ent.Index {
return nil
}
每个 Schema 结构体必须嵌入 ent.Schema,Ent 代码生成工具会识别该嵌入并自动生成对应的 CRUD 代码。
3.2 字段类型体系
Ent 提供了丰富的内置字段类型,覆盖了绝大多数业务场景。下表列出最常用的字段类型及其对应数据库类型和 Go 类型:
| Ent 字段类型 | 数据库类型 | Go 类型 | 典型场景 |
|---|---|---|---|
field.String |
VARCHAR / TEXT | string | 用户名、邮箱、描述 |
field.Int |
INT / BIGINT | int | 年龄、计数、状态码 |
field.Float |
FLOAT / DOUBLE | float64 | 价格、评分、坐标 |
field.Bool |
BOOLEAN | bool | 是否激活、是否删除 |
field.Time |
TIMESTAMP / DATETIME | time.Time | 创建时间、更新时间 |
field.JSON |
JSON / JSONB | 指定结构体 | 配置、元数据、动态属性 |
field.Enum |
VARCHAR / ENUM | string | 状态枚举、分类 |
field.UUID |
UUID / CHAR(36) | uuid.UUID | 主键、分布式 ID |
field.Text |
TEXT / LONGTEXT | string | 长文本内容、文章正文 |
field.Text 而非 field.String,避免数据库层面截断;对于枚举值优先使用 field.Enum 并显式列出所有合法值,提升代码可读性和数据完整性。
3.3 字段标签
字段标签(Tags)用于控制字段在序列化、验证、数据库列名等方面的行为。Ent 通过 .Tag() 方法为字段添加标签,支持 json、gqlgen、graphql、validate 等常见标签。
func (User) Fields() []ent.Field {
return []ent.Field{
field.String("name").
Tag("json", "user_name").
Tag("validate", "required,min=2,max=50"),
field.Int("age").
Tag("json", "age").
Tag("gqlgen", "Age"),
}
}
常用标签及其作用:
| 标签键 | 作用 | 示例值 |
|---|---|---|
json |
控制 JSON 序列化时的字段名 | "user_name" |
validate |
字段验证规则(需配合 validator 库) | "required,min=2" |
gqlgen |
GraphQL 生成时的字段名覆盖 | "UserName" |
graphql |
GraphQL schema 中的字段名 | "userName" |
ent |
Ent 内部行为控制(如列名覆盖) | "column:user_name" |
.Tag()。如果同一个标签键被多次设置,后设置的会覆盖先设置的。建议在项目初期就统一标签命名规范,避免后期大规模重构。
3.4 必填与可选字段
Ent 通过 .Optional() 和 .Nillable() 方法控制字段的必填/可选行为。默认情况下,所有字段都是必填的,即数据库列不允许为 NULL,且创建时必须提供值。
| 方法 | 数据库列 | Go 类型 | 创建时行为 |
|---|---|---|---|
| 默认(无修饰) | NOT NULL | string / int | 必须提供值 |
.Optional() |
NULL | string / int | 可省略,省略时写入零值 |
.Optional().Nillable() |
NULL | *string / *int | 可省略,省略时写入 NULL |
func (User) Fields() []ent.Field {
return []ent.Field{
// 必填字段:创建时必须提供
field.String("name"),
// 可选字段:创建时可省略,省略时写入空字符串
field.String("nickname").Optional(),
// 可选 + 可空字段:创建时可省略,省略时写入 NULL
field.String("email").Optional().Nillable(),
}
}
.Optional().Nillable(),这样 Go 代码中可以通过指针判断是否显式设置过值,避免零值歧义。
3.5 默认值
Ent 通过 .Default() 和 .DefaultFunc() 方法为字段设置默认值。默认值在数据库层面和 Ent 代码生成层面同时生效。
| 方法 | 适用类型 | 示例 | 说明 |
|---|---|---|---|
.Default(v) |
所有标量类型 | .Default(0) |
静态默认值,编译期确定 |
.DefaultFunc(fn) |
所有类型 | .DefaultFunc(func() int { return time.Now().Unix() }) |
动态默认值,每次创建时执行函数 |
.UpdateDefault(fn) |
所有类型 | .UpdateDefault(func() int { return time.Now().Unix() }) |
更新时的默认值(仅用于 UpdateOne 未设置时) |
func (User) Fields() []ent.Field {
return []ent.Field{
// 静态默认值
field.Int("age").Default(18),
// 动态默认值:创建时自动填充当前时间戳
field.Time("created_at").
DefaultFunc(func() time.Time {
return time.Now()
}),
// 可选字段 + 默认值:省略时使用默认值
field.String("status").
Optional().
Default("active"),
}
}
.DefaultFunc() 中的函数会在每次创建记录时调用,适合生成时间戳、UUID、随机数等。但要注意函数不能有副作用(如网络请求),且必须是纯函数或依赖当前时间等本地状态。
3.6 综合示例:用户模型 Schema
下面是一个综合了字段类型、标签、必填/可选、默认值的用户模型 Schema:
package schema
import (
"time"
"entgo.io/ent"
"entgo.io/ent/schema/field"
)
type User struct {
ent.Schema
}
func (User) Fields() []ent.Field {
return []ent.Field{
// 必填:用户名,JSON 序列化时使用 "user_name"
field.String("name").
Tag("json", "user_name").
Tag("validate", "required,min=2,max=50"),
// 可选 + 可空:邮箱,省略时写入 NULL
field.String("email").
Optional().
Nillable().
Tag("json", "email"),
// 必填 + 默认值:年龄,默认 18
field.Int("age").
Default(18).
Tag("json", "age"),
// 可选 + 默认值:状态,默认 "active"
field.String("status").
Optional().
Default("active").
Tag("json", "status"),
// 动态默认值:创建时间
field.Time("created_at").
DefaultFunc(func() time.Time {
return time.Now()
}).
Tag("json", "created_at"),
// 动态默认值:更新时间,更新时自动刷新
field.Time("updated_at").
DefaultFunc(func() time.Time {
return time.Now()
}).
UpdateDefault(func() time.Time {
return time.Now()
}).
Tag("json", "updated_at"),
}
}
3.7 Schema 字段声明流程
下图展示了从业务需求到 Ent Schema 字段声明的完整流程:
3.8 常见陷阱与避坑指南
- 零值歧义: 对于
int、float等类型,.Optional()省略时写入零值(0),无法区分“未设置”和“值为0”。需要区分时请使用.Optional().Nillable()配合指针类型。 - 默认值与可选字段的交互: 如果字段同时设置了
.Optional()和.Default(),创建时省略该字段会使用默认值,而不是写入 NULL。只有同时使用.Optional().Nillable()且不设置默认值时,省略才会写入 NULL。 - 标签冲突: 多个标签使用同一个键时,后设置的会覆盖先设置的。建议在团队中统一标签键的命名规范,避免无意覆盖。
- 动态默认值的性能:
.DefaultFunc()在每次创建记录时都会调用,如果函数执行耗时较长(如加密、网络请求),会显著影响写入性能。建议只使用轻量级函数。