You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Adonis.js最佳实践:用户角色关联建模——User hasOne Role还是Role hasMany Users?

在Adonis.js中设计用户-角色关联的最佳实践

基础场景:单角色体系(Role hasMany Users + User belongsTo Role)

这是最常见的权限模型,适合用户只能拥有一个角色的场景(比如普通用户、管理员二选一)。

数据库结构

  • roles表:存储角色信息

    字段名类型说明
    idint/unsigned主键
    namevarchar角色标识(如admin、user)
    descriptiontext角色描述(可选)
    created_atdatetime创建时间
    updated_atdatetime更新时间
  • users表:添加外键关联角色
    在原有用户字段基础上,新增role_id字段(外键关联roles.id),允许为null(比如未分配角色的新用户)。

Adonis.js 模型实现

Role 模型

import { BaseModel, column, hasMany, HasMany } from '@ioc:Adonis/Lucid/Orm'
import User from 'App/Models/User'
import { DateTime } from 'luxon'

export default class Role extends BaseModel {
  @column({ isPrimary: true })
  public id: number

  @column()
  public name: string

  @column()
  public description: string | null

  @column.dateTime({ autoCreate: true })
  public createdAt: DateTime

  @column.dateTime({ autoCreate: true, autoUpdate: true })
  public updatedAt: DateTime

  // 一个角色对应多个用户
  @hasMany(() => User)
  public users: HasMany<typeof User>
}

User 模型

import { BaseModel, column, belongsTo, BelongsTo } from '@ioc:Adonis/Lucid/Orm'
import Role from 'App/Models/Role'
import { DateTime } from 'luxon'

export default class User extends BaseModel {
  @column({ isPrimary: true })
  public id: number

  @column()
  public name: string

  @column()
  public email: string

  @column()
  public roleId: number | null // 驼峰命名自动映射到数据库的role_id

  @column.dateTime({ autoCreate: true })
  public createdAt: DateTime

  @column.dateTime({ autoCreate: true, autoUpdate: true })
  public updatedAt: DateTime

  // 用户属于一个角色
  @belongsTo(() => Role)
  public role: BelongsTo<typeof Role>
}

优势

  • 结构简单,查询效率高,维护成本低
  • 符合大多数中小型系统的权限需求

进阶方案:多角色体系(User belongsToMany Role)

如果需要支持用户拥有多个角色(比如一个用户既是内容编辑又是审核员),一对多关联就无法满足,此时应该用多对多关联,通过中间表建立关系。

数据库结构

  • 保留roles和users表(去掉users表的role_id字段)
  • 新增中间表user_roles,存储用户与角色的关联关系:
    字段名类型说明
    user_idint/unsigned外键,关联users.id
    role_idint/unsigned外键,关联roles.id
    created_atdatetime创建时间(可选)
    注意:将user_id和role_id设置为联合主键,避免重复关联。

Adonis.js 模型实现

User 模型新增关联

import { belongsToMany, BelongsToMany } from '@ioc:Adonis/Lucid/Orm'
import Role from 'App/Models/Role'

// ... 原有字段 ...

// 用户拥有多个角色
@belongsToMany(() => Role, {
  pivotTable: 'user_roles', // 指定中间表名称
})
public roles: BelongsToMany<typeof Role>

Role 模型新增关联

import { belongsToMany, BelongsToMany } from '@ioc:Adonis/Lucid/Orm'
import User from 'App/Models/User'

// ... 原有字段 ...

// 角色被多个用户拥有
@belongsToMany(() => User, {
  pivotTable: 'user_roles',
})
public users: BelongsToMany<typeof User>

优势

  • 灵活性极强,支持复杂的权限组合
  • 扩展性好,未来新增角色或调整用户权限无需修改核心表结构

配套权限校验实践

不管用哪种关联方案,都可以通过Adonis.js的中间件实现权限校验,确保只有对应角色的用户能访问特定路由。

示例:角色校验中间件

创建app/Middleware/Role.ts:

import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'

export default class RoleMiddleware {
  public async handle({ auth, response }: HttpContextContract, next: () => Promise<void>, allowedRoles: string[]) {
    const user = auth.user
    if (!user) {
      return response.unauthorized('请先登录')
    }

    // 单角色场景:加载用户关联的角色
    await user.load('role')
    if (!allowedRoles.includes(user.role.name)) {
      return response.forbidden('无权限访问此资源')
    }

    // 多角色场景:替换为以下代码
    // await user.load('roles')
    // const hasPermission = user.roles.some(role => allowedRoles.includes(role.name))
    // if (!hasPermission) {
    //   return response.forbidden('无权限访问此资源')
    // }

    await next()
  }
}

注册与使用中间件

在start/kernel.ts中注册中间件:

export default class Kernel {
  protected namedMiddleware = {
    // ... 其他中间件 ...
    role: () => import('App/Middleware/Role'),
  }
}

在路由中使用:

// 只有admin角色能访问
Route.get('/admin/dashboard', 'AdminController.dashboard').middleware(['auth', 'role:admin'])

// 多角色场景:允许admin和editor访问
Route.get('/content/edit', 'ContentController.edit').middleware(['auth', 'role:admin,editor'])

选型建议

  1. 优先选一对多:如果你的系统是明确的单角色体系,或者短期内不需要多角色功能,用一对多关联足够,代码和数据库都更简洁。
  2. 提前布局多对多:如果不确定未来是否需要多角色,或者已经有相关需求,直接用多对多关联,避免后期重构数据库和模型的麻烦。
  3. 规范角色标识:把角色名定义为常量或枚举(比如app/Constants/Role.ts),避免硬编码带来的拼写错误,提升可维护性:
    export enum RoleName {
      ADMIN = 'admin',
      USER = 'user',
      EDITOR = 'editor',
    }
    

内容的提问来源于stack exchange,提问作者Gilson Garcia

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.08 13:22:14