3. Ent Schema 基础:定义 User、Role、Permission 核心 Schema

在构建分布式鉴权系统时,数据模型是权限验证的基石。Ent 通过 Schema 文件定义数据模型,每个 Schema 对应数据库中的一张表。本章将定义 User(用户)、Role(角色)、Permission(权限) 三个核心 Schema,并深入理解字段类型与注解的使用。

3.1 核心 Schema 关系概览

在 RBAC(基于角色的访问控制)模型中,User 通过 Role 获得 Permission,形成“用户 → 角色 → 权限”的多对多关联关系。下图展示了三个 Schema 之间的核心结构:

User-Role-Permission 核心 Schema 关系图 User-Role-Permission 核心 Schema 关系 User id (UUID) username (string) password_hash (string) Role id (UUID) name (string, unique) description (string) Permission id (UUID) resource (string) action (string) M2M M2M 关系说明 • User 与 Role 是多对多关系:一个用户可拥有多个角色,一个角色可被多个用户拥有。 • Role 与 Permission 是多对多关系:一个角色可包含多个权限,一个权限可被多个角色引用。 • 通过 Role 中间层,实现用户与权限的解耦,便于批量授权与权限审计。

3.2 定义 User Schema

User Schema 是鉴权系统的入口实体。除了基本字段外,还需要存储密码哈希值,并通过 edges 定义与 Role 的多对多关系。

// ent/schema/user.go
package schema

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

type User struct {
    ent.Schema
}

func (User) Fields() []ent.Field {
    return []ent.Field{
        field.UUID("id", uuid.UUID{}).
            Default(uuid.New),
        field.String("username").
            Unique().
            MaxLen(50).
            Comment("用户名,全局唯一"),
        field.String("password_hash").
            MaxLen(255).
            Sensitive().
            Comment("bcrypt 哈希后的密码"),
        field.Bool("is_active").
            Default(true).
            Comment("用户是否激活"),
        field.Time("created_at").
            Default(time.Now).
            Immutable(),
        field.Time("updated_at").
            Default(time.Now).
            UpdateDefault(time.Now),
    }
}

func (User) Edges() []ent.Edge {
    return []ent.Edge{
        edge.To("roles", Role.Type).
            Through("user_roles", UserRole.Type),
    }
}
字段设计要点:
  • field.UUID:使用 UUID 作为主键,避免自增 ID 在分布式场景下的冲突。
  • Sensitive():标记密码字段为敏感字段,Ent 在日志和序列化时会自动脱敏。
  • Immutable():创建时间一旦写入不可修改。
  • Through():通过中间表 UserRole 实现多对多关系,便于后续扩展关联属性(如角色分配时间)。

3.3 定义 Role Schema

Role 是权限聚合的载体。一个角色代表一组权限的集合,例如“管理员”、“编辑者”、“查看者”。

// ent/schema/role.go
package schema

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

type Role struct {
    ent.Schema
}

func (Role) Fields() []ent.Field {
    return []ent.Field{
        field.UUID("id", uuid.UUID{}).
            Default(uuid.New),
        field.String("name").
            Unique().
            MaxLen(100).
            Comment("角色名称,如 admin、editor"),
        field.String("description").
            MaxLen(255).
            Optional().
            Comment("角色描述"),
        field.Time("created_at").
            Default(time.Now).
            Immutable(),
    }
}

func (Role) Edges() []ent.Edge {
    return []ent.Edge{
        edge.From("users", User.Type).
            Ref("roles").
            Through("user_roles", UserRole.Type),
        edge.To("permissions", Permission.Type).
            Through("role_permissions", RolePermission.Type),
    }
}
命名规范:角色名称建议使用小写字母加下划线(snake_case),如 super_admin、data_analyst。避免使用空格或特殊字符,确保在 URL 和代码中可直接引用。

3.4 定义 Permission Schema

Permission 是权限的最小单元,通常由 资源(resource) 和 操作(action) 两个维度组成。例如 resource="document" + action="read" 表示“读取文档”权限。

// ent/schema/permission.go
package schema

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

type Permission struct {
    ent.Schema
}

func (Permission) Fields() []ent.Field {
    return []ent.Field{
        field.UUID("id", uuid.UUID{}).
            Default(uuid.New),
        field.String("resource").
            MaxLen(100).
            Comment("资源标识,如 document、user、report"),
        field.String("action").
            MaxLen(50).
            Comment("操作标识,如 create、read、update、delete"),
        field.String("description").
            MaxLen(255).
            Optional().
            Comment("权限描述"),
        field.Time("created_at").
            Default(time.Now).
            Immutable(),
    }
}

func (Permission) Edges() []ent.Edge {
    return []ent.Edge{
        edge.From("roles", Role.Type).
            Ref("permissions").
            Through("role_permissions", RolePermission.Type),
    }
}

3.5 字段类型与注解对照表

Ent 提供了丰富的字段类型和注解,用于控制数据库列类型、校验规则、序列化行为等。下表列出本课程中常用的字段类型与注解:

字段类型 / 注解 用途 示例
field.UUID 分布式主键,避免冲突 field.UUID("id", uuid.UUID{}).Default(uuid.New)
field.String 字符串字段,支持长度限制 field.String("username").MaxLen(50)
field.Bool 布尔字段,用于状态标记 field.Bool("is_active").Default(true)
field.Time 时间字段,支持默认值和自动更新 field.Time("updated_at").UpdateDefault(time.Now)
Unique() 唯一约束,防止重复值 field.String("name").Unique()
Sensitive() 标记为敏感字段,日志脱敏 field.String("password_hash").Sensitive()
Immutable() 字段创建后不可修改 field.Time("created_at").Immutable()
Optional() 字段允许为空(NULL) field.String("description").Optional()
Comment() 添加字段注释,生成文档和 DDL 注释 field.String("resource").Comment("资源标识")

3.6 中间表 Schema:UserRole 与 RolePermission

为了实现多对多关系并携带额外属性,我们定义两个中间表 Schema。中间表不仅存储关联关系,还可以记录分配时间、操作人等审计信息。

// ent/schema/user_role.go
package schema

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

type UserRole struct {
    ent.Schema
}

func (UserRole) Fields() []ent.Field {
    return []ent.Field{
        field.UUID("id", uuid.UUID{}).Default(uuid.New),
        field.Time("assigned_at").Default(time.Now).Comment("角色分配时间"),
    }
}

func (UserRole) Edges() []ent.Edge {
    return []ent.Edge{
        edge.To("user", User.Type).Unique(),
        edge.To("role", Role.Type).Unique(),
    }
}

// ent/schema/role_permission.go
package schema

type RolePermission struct {
    ent.Schema
}

func (RolePermission) Fields() []ent.Field {
    return []ent.Field{
        field.UUID("id", uuid.UUID{}).Default(uuid.New),
        field.Time("granted_at").Default(time.Now).Comment("权限授予时间"),
    }
}

func (RolePermission) Edges() []ent.Edge {
    return []ent.Edge{
        edge.To("role", Role.Type).Unique(),
        edge.To("permission", Permission.Type).Unique(),
    }
}
为什么使用中间表而非直接 M2M? 直接使用 edge.To("roles", Role.Type) 虽然简洁,但无法记录关联的附加属性(如分配时间、过期时间)。通过 Through() 指定中间表,可以在未来扩展审计、时间戳、软删除等能力,而无需重构 Schema。

3.7 Schema 生成与验证

定义完所有 Schema 后,执行以下命令生成 Ent 代码:

go run -mod=mod entgo.io/ent/cmd/ent generate ./ent/schema

# 验证生成结果
go build ./...

生成成功后,Ent 会自动创建以下文件结构:

  • ent/user.go — User 实体的客户端方法
  • ent/role.go — Role 实体的客户端方法
  • ent/permission.go — Permission 实体的客户端方法
  • ent/userrole.go — UserRole 中间表实体
  • ent/rolepermission.go — RolePermission 中间表实体
  • ent/migrate/ — 数据库迁移文件