3. Ent Schema 基础:定义 User、Role、Permission 核心 Schema
在构建分布式鉴权系统时,数据模型是权限验证的基石。Ent 通过 Schema 文件定义数据模型,每个 Schema 对应数据库中的一张表。本章将定义 User(用户)、Role(角色)、Permission(权限) 三个核心 Schema,并深入理解字段类型与注解的使用。
3.1 核心 Schema 关系概览
在 RBAC(基于角色的访问控制)模型中,User 通过 Role 获得 Permission,形成“用户 → 角色 → 权限”的多对多关联关系。下图展示了三个 Schema 之间的核心结构:
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/— 数据库迁移文件