4. Ent Edge 与关系建模:User ↔ Role ↔ Permission 多对多关联
在分布式鉴权系统中,用户、角色与权限三者之间的关联关系是权限模型的核心骨架。本章将基于 Ent ORM 的 Edge 机制,建立 User ↔ Role(多对多) 与 Role ↔ Permission(多对多) 的完整关系模型,为后续的鉴权验证提供数据基础。
4.1 关系模型设计概览
本课程采用经典的 RBAC(基于角色的访问控制)模型变体,其核心关系如下:
- User ↔ Role:一个用户可以拥有多个角色,一个角色也可以被多个用户持有(多对多)。
- Role ↔ Permission:一个角色可以关联多个权限,一个权限也可以被多个角色引用(多对多)。
这种设计将用户与权限解耦,通过角色作为中间层,实现灵活的权限分配与回收。
设计原则: 多对多关系在 Ent 中通过隐式中间表实现,无需手动创建关联实体,Edge 会自动维护连接表(Join Table)。
4.2 Schema 定义与 Edge 声明
在 Ent 项目中,每个实体对应一个 Schema 文件。我们需要在 user.go、role.go 和 permission.go 中分别声明 Edge。
4.2.1 User Schema(用户)
// 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.String("username").Unique(),
field.String("password_hash"),
field.Bool("active").Default(true),
}
}
func (User) Edges() []ent.Edge {
return []ent.Edge{
edge.To("roles", Role.Type).Through("user_roles", UserRole.Type),
}
}
4.2.2 Role Schema(角色)
// 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.String("name").Unique(),
field.String("description").Optional(),
}
}
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),
}
}
4.2.3 Permission Schema(权限)
// 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.String("action").NotEmpty(),
field.String("resource").NotEmpty(),
field.String("description").Optional(),
}
}
func (Permission) Edges() []ent.Edge {
return []ent.Edge{
edge.From("roles", Role.Type).Ref("permissions").Through("role_permissions", RolePermission.Type),
}
}
4.2.4 中间表 Schema(UserRole & RolePermission)
Ent 的 Through 方法允许我们自定义中间表,以便在关联上附加额外字段(如创建时间、过期时间等)。
// 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.Time("created_at").Default(time.Now),
}
}
func (UserRole) Edges() []ent.Edge {
return []ent.Edge{
edge.From("user", User.Type).Ref("user_roles").Unique().Required(),
edge.From("role", Role.Type).Ref("user_roles").Unique().Required(),
}
}
// ent/schema/role_permission.go
package schema
import (
"entgo.io/ent"
"entgo.io/ent/schema/edge"
"entgo.io/ent/schema/field"
)
type RolePermission struct {
ent.Schema
}
func (RolePermission) Fields() []ent.Field {
return []ent.Field{
field.Time("granted_at").Default(time.Now),
}
}
func (RolePermission) Edges() []ent.Edge {
return []ent.Edge{
edge.From("role", Role.Type).Ref("role_permissions").Unique().Required(),
edge.From("permission", Permission.Type).Ref("role_permissions").Unique().Required(),
}
}
4.3 关系模型结构图
下图展示了 User、Role、Permission 三者之间的多对多关联以及中间表的位置:
4.4 Edge 关键参数对比
| 参数 | 说明 | 示例 |
|---|---|---|
To |
定义当前实体指向目标实体的边 | edge.To("roles", Role.Type) |
From |
定义当前实体从目标实体反向引用的边 | edge.From("users", User.Type).Ref("roles") |
Through |
指定自定义中间表,替代默认隐式表 | .Through("user_roles", UserRole.Type) |
Ref |
关联到目标实体的对应边名称 | .Ref("roles") |
Unique |
标记为唯一约束(用于中间表外键) | .Unique().Required() |
4.5 生成代码与数据库迁移
完成 Schema 定义后,执行以下命令生成 Ent 代码并执行迁移:
# 生成代码
go generate ./ent
# 执行数据库迁移(自动创建表及中间表)
go run -mod=mod main.go migrate
迁移完成后,数据库中将自动创建以下表:
users— 用户表roles— 角色表permissions— 权限表user_roles— 用户-角色关联表(含created_at字段)role_permissions— 角色-权限关联表(含granted_at字段)
4.6 关联操作示例
以下代码演示了如何使用 Ent 客户端创建关联数据:
// 创建用户并关联角色
user, _ := client.User.Create().
SetUsername("alice").
SetPasswordHash("hashed_pwd").
Save(ctx)
role, _ := client.Role.Create().
SetName("admin").
SetDescription("Administrator role").
Save(ctx)
// 通过中间表建立关联
client.UserRole.Create().
SetUser(user).
SetRole(role).
SetCreatedAt(time.Now()).
Save(ctx)
// 查询用户的所有角色
roles, _ := user.QueryRoles().All(ctx)
for _, r := range roles {
fmt.Println(r.Name)
}
最佳实践: 使用
Through 自定义中间表时,务必在中间表的 Edge 中设置 Unique().Required(),确保外键组合的唯一性,避免重复关联。
4.7 关系查询路径图
下面的 SVG 展示了从 User 出发,通过 Edge 链式查询到 Permission 的完整路径:
4.8 常见问题与避坑
- 中间表命名冲突: 使用
Through时,中间表 Schema 名称不能与已有实体重名,建议使用UserRole、RolePermission等组合命名。 - 双向引用必须配对:
To和From必须通过Ref正确配对,否则 Ent 代码生成会报错。 - 中间表字段不可滥用: 只在中间表存放与关联直接相关的字段(如时间戳),不要将业务主数据放在中间表中。
- 迁移顺序: 先执行
go generate,再执行迁移命令,确保 Edge 定义被正确编译。
注意: 如果未使用
Through 自定义中间表,Ent 会自动生成隐式表,但无法附加额外字段。对于需要审计追踪的鉴权系统,强烈建议始终使用自定义中间表。