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 字段声明的完整流程:

Ent Schema 字段声明流程 业务需求 选择字段类型 配置约束 必填 / 可选 默认值 字段标签 ent generate → 代码生成

3.8 常见陷阱与避坑指南

  • 零值歧义: 对于 int、float 等类型,.Optional() 省略时写入零值(0),无法区分“未设置”和“值为0”。需要区分时请使用 .Optional().Nillable() 配合指针类型。
  • 默认值与可选字段的交互: 如果字段同时设置了 .Optional() 和 .Default(),创建时省略该字段会使用默认值,而不是写入 NULL。只有同时使用 .Optional().Nillable() 且不设置默认值时,省略才会写入 NULL。
  • 标签冲突: 多个标签使用同一个键时,后设置的会覆盖先设置的。建议在团队中统一标签键的命名规范,避免无意覆盖。
  • 动态默认值的性能: .DefaultFunc() 在每次创建记录时都会调用,如果函数执行耗时较长(如加密、网络请求),会显著影响写入性能。建议只使用轻量级函数。
小结: 本章涵盖了 Ent Schema 的核心基础——从定义结构、字段类型选择,到标签、必填/可选控制,再到默认值配置。掌握这些基础后,你就能用 Ent 的声明式风格快速搭建出符合业务需求的数据库模型。下一章将深入字段验证与高级类型,进一步提升 Schema 的表达能力。