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 三者之间的多对多关联以及中间表的位置:

User ↔ Role ↔ Permission 多对多关系模型 User UserRole (中间表) Role RolePermission (中间表) Permission N N N N 说明: 通过 Through() 自定义中间表,可在关联上附加 created_at、granted_at 等审计字段。

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 的完整路径:

Edge 链式查询路径:User → Role → Permission User .QueryRoles() Role .QueryPermissions() Permission 查询结果: 获取用户所有权限 无需手动 JOIN 中间表

4.8 常见问题与避坑

  • 中间表命名冲突: 使用 Through 时,中间表 Schema 名称不能与已有实体重名,建议使用 UserRole、RolePermission 等组合命名。
  • 双向引用必须配对: To 和 From 必须通过 Ref 正确配对,否则 Ent 代码生成会报错。
  • 中间表字段不可滥用: 只在中间表存放与关联直接相关的字段(如时间戳),不要将业务主数据放在中间表中。
  • 迁移顺序: 先执行 go generate,再执行迁移命令,确保 Edge 定义被正确编译。
注意: 如果未使用 Through 自定义中间表,Ent 会自动生成隐式表,但无法附加额外字段。对于需要审计追踪的鉴权系统,强烈建议始终使用自定义中间表。