0

0

Flask-SQLAlchemy 多对多关系:用户与角色权限管理的正确实践

心靈之曲

心靈之曲

发布时间:2025-11-22 13:16:15

|

142人浏览过

|

来源于php中文网

原创

flask-sqlalchemy 多对多关系:用户与角色权限管理的正确实践

本文详细介绍了如何在 Flask 应用中利用 SQLAlchemy 实现用户与角色之间的多对多关系。我们将通过一个博客应用的示例,展示如何正确定义关联表、用户模型和角色模型,并纠正常见的 `InvalidRequestError`,特别是由于模型类命名不规范或关系属性配置错误导致的问题,确保关系配置的清晰与准确。

理解多对多关系及其应用场景

在Web应用开发中,多对多关系是一种常见的数据库设计模式。例如,一个用户可以拥有多个角色(如“管理员”、“编辑”、“普通用户”),同时一个角色也可以分配给多个用户。在 SQLAlchemy 中,实现这种关系通常需要一个中间表(也称为关联表或连接表)来连接两个主表。

定义关联表

关联表是多对多关系的核心。它只包含两个外键,分别指向两个主表的ID。在 Flask-SQLAlchemy 中,我们可以使用 db.Table 来定义它。

import sqlalchemy as sa
import sqlalchemy.orm as so
from datetime import datetime, timezone
from typing import Optional
from flask_login import UserMixin
from werkzeug.security import generate_password_hash, check_password_hash
from hashlib import md5
from flask_security.models import fsqla_v3 as fsqla
from app import db, login # 假设 db 和 login 已在 app.py 中初始化

# 定义用户与角色之间的关联表
roles_users_table = db.Table(
    "roles_users_table", # 关联表的名称
    db.metadata,
    sa.Column("user_id", sa.Integer, sa.ForeignKey("user.id"), primary_key=True),
    sa.Column("role_id", sa.Integer, sa.ForeignKey("role.id"), primary_key=True) # 注意这里是 "role.id"
)

注意事项:

聚蜂消防BeesFPD
聚蜂消防BeesFPD

关注消防领域的智慧云平台

下载
  • 关联表的名称应清晰反映其连接的两个实体,例如 roles_users_table。
  • 外键 (ForeignKey) 引用的是目标表的小写名称和其主键,例如 user.id 和 role.id。
  • 两个外键共同构成联合主键 (primary_key=True),确保每对用户-角色关系的唯一性。

定义用户模型 (User)

用户模型需要包含其基本信息,并定义与角色模型的多对多关系。

class User(fsqla.FsUserMixin, db.Model):
    id: so.Mapped[int] = so.mapped_column(primary_key=True)
    email: so.Mapped[str] = so.mapped_column(sa.String(120), index=True, unique=True)
    password_hash: so.Mapped[Optional[str]] = so.mapped_column(sa.String(256))
    about_me: so.Mapped[Optional[str]] = so.mapped_column(sa.String(140))
    last_seen: so.Mapped[Optional[datetime]] = so.mapped_column(default=lambda: datetime.now(timezone.utc))

    # 定义与 Post 模型的一对多关系
    posts: so.WriteOnlyMapped['Post'] = so.relationship(back_populates='author')

    # 定义与 Role 模型的多对多关系
    # 'roles' 属性将包含一个 Role 对象的列表
    roles: so.Mapped[list['Role']] = so.relationship(
        secondary=roles_users_table, # 指定关联表
        back_populates="users"       # 指定 Role 模型中对应的反向关系属性
    )

    def set_password(self, password):
        self.password_hash = generate_password_hash(password)

    def check_password(self, password):
        return check_password_hash(self.password_hash, password)

    def avatar(self, size):
        digest = md5(self.email.lower().encode('utf-8')).hexdigest()
        return f'https://www.gravatar.com/avatar/{digest}?d=identicon&s={size}'

    def __repr__(self):
        return ''.format(self.email)

# 假设 Post 模型也已定义
class Post(db.Model):
    id: so.Mapped[int] = so.mapped_column(primary_key=True)
    body: so.Mapped[str] = so.mapped_column(sa.String(140))
    timestamp: so.Mapped[datetime] = so.mapped_column(index=True, default=lambda: datetime.now(timezone.utc))
    user_id: so.Mapped[int] = so.mapped_column(sa.ForeignKey(User.id), index=True)
    author: so.Mapped[User] = so.relationship(back_populates='posts')

    def __repr__(self):
        return ''.format(self.body)

注意事项:

  • roles: so.Mapped[list['Role']] 表示一个 User 对象可以关联多个 Role 对象。
  • secondary=roles_users_table 指明了用于连接 User 和 Role 的关联表。
  • back_populates="users" 建立了双向关系。这意味着在 Role 模型中,需要有一个名为 users 的属性来指向关联的 User 对象列表。

定义角色模型 (Role)

角色模型需要包含角色名称等信息,并定义与用户模型的多对多关系。

class Role(db.Model, fsqla.FsRoleMixin): # 注意:类名应为单数 'Role'
    id: so.Mapped[int] = so.mapped_column(primary_key=True)
    name: so.Mapped[str] = so.mapped_column(sa.String(80), unique=True) # 角色名称,应为字符串类型
    description: so.Mapped[Optional[str]] = so.mapped_column(sa.String(255)) # 角色描述(可选)

    # 定义与 User 模型的多对多关系
    # 'users' 属性将包含一个 User 对象的列表
    users: so.Mapped[list['User']] = so.relationship(
        secondary=roles_users_table, # 指定关联表
        back_populates="roles"       # 指定 User 模型中对应的反向关系属性
    )

    def __repr__(self):
        return ''.format(self.name)

关键修正与注意事项:

  1. 类名应为单数:Role 而非 Roles。 这是导致原始 sqlalchemy.exc.InvalidRequestError: When initializing mapper Mapper[User(user)], expression 'Role' failed to locate a name ('Role'). 错误的主要原因。SQLAlchemy 在解析关系时,通常期望引用的是模型类的精确名称。如果 User 模型中 so.Mapped[list['Role']] 引用的是 Role,但实际类名为 Roles,就会出现找不到映射的错误。
  2. name 属性类型:so.Mapped[str]。 角色名称通常是一个字符串(如 "admin", "editor"),而不是一个 User 对象。原始代码中的 name: so.Mapped[User] 是不正确的。
  3. back_populates 属性一致性: User 模型中的 back_populates="users" 必须与 Role 模型中用于反向关系的属性名 users 保持一致。

完整示例代码

结合上述修正,以下是完整的模型定义代码:

from datetime import datetime, timezone
from typing import Optional
import sqlalchemy as sa
import sqlalchemy.orm as so
from app import db, login # 假设 db 和 login 已在 app.py 中初始化
from werkzeug.security import generate_password_hash, check_password_hash
from flask_login import UserMixin
from flask_security.models import fsqla_v3 as fsqla
from hashlib import md5

# 定义用户与角色之间的关联表
roles_users_table = db.Table(
    "roles_users_table",
    db.metadata,
    sa.Column("user_id", sa.Integer, sa.ForeignKey("user.id"), primary_key=True),
    sa.Column("role_id", sa.Integer, sa.ForeignKey("role.id"), primary_key=True)
)

class User(fsqla.FsUserMixin, db.Model):
    id: so.Mapped[int] = so.mapped_column(primary_key=True)
    email: so.Mapped[str] = so.mapped_column(sa.String(120), index=True, unique=True)
    password_hash: so.Mapped[Optional[str]] = so.mapped_column(sa.String(256))
    posts: so.WriteOnlyMapped['Post'] = so.relationship(back_populates='author')
    about_me: so.Mapped[Optional[str]] = so.mapped_column(sa.String(140))
    last_seen: so.Mapped[Optional[datetime]] = so.mapped_column(default=lambda: datetime.now(timezone.utc))

    # 定义与 Role 模型的多对多关系
    roles: so.Mapped[list['Role']] = so.relationship(
        secondary=roles_users_table,
        back_populates="users"
    )

    def set_password(self, password):
        self.password_hash = generate_password_hash(password)

    def check_password(self, password):
        return check_password_hash(self.password_hash, password)

    def __repr__(self):
        return ''.format(self.email)

    def avatar(self, size):
        digest = md5(self.email.lower().encode('utf-8')).hexdigest()
        return f'https://www.gravatar.com/avatar/{digest}?d=identicon&s={size}'

class Post(db.Model):
    id: so.Mapped[int] = so.mapped_column(primary_key=True)
    body: so.Mapped[str] = so.mapped_column(sa.String(140))
    timestamp: so.Mapped[datetime] = so.mapped_column(index=True, default=lambda: datetime.now(timezone.utc))
    user_id: so.Mapped[int] = so.mapped_column(sa.ForeignKey(User.id), index=True)
    author: so.Mapped[User] = so.relationship(back_populates='posts')

    def __repr__(self):
        return ''.format(self.body)

class Role(db.Model, fsqla.FsRoleMixin): # 正确的类名:Role
    id: so.Mapped[int] = so.mapped_column(primary_key=True)
    name: so.Mapped[str] = so.mapped_column(sa.String(80), unique=True) # 角色名称,字符串类型
    description: so.Mapped[Optional[str]] = so.mapped_column(sa.String(255)) # 角色描述(可选)

    # 定义与 User 模型的多对多关系
    users: so.Mapped[list['User']] = so.relationship(
        secondary=roles_users_table,
        back_populates="roles"
    )

    def __repr__(self):
        return ''.format(self.name)

如何创建和关联数据

在上述模型定义正确后,你可以像这样创建角色和用户,并建立它们之间的关系:

# 假设 db 已经初始化并且应用上下文已激活
# from app import app, db, User, Role
# with app.app_context():

# 创建角色
admin_role = Role(name="admin", description="Administrator role")
editor_role = Role(name="editor", description="Editor role")
user_role = Role(name="user", description="Standard user role")

db.session.add_all([admin_role, editor_role, user_role])
db.session.commit()

# 创建用户
user1 = User(email="test1@example.com", password_hash="hashed_password_1")
user2 = User(email="test2@example.com", password_hash="hashed_password_2")

db.session.add_all([user1, user2])
db.session.commit()

# 将角色分配给用户
user1.roles.append(admin_role)
user1.roles.append(editor_role) # user1 拥有 admin 和 editor 两个角色

user2.roles.append(user_role) # user2 拥有 user 角色

db.session.commit()

# 查询用户及其角色
print(f"User 1 roles: {[r.name for r in user1.roles]}")
# 输出: User 1 roles: ['admin', 'editor']

# 查询角色下的用户
print(f"Admin role users: {[u.email for u in admin_role.users]}")
# 输出: Admin role users: ['test1@example.com']

总结

正确配置 Flask-SQLAlchemy 中的多对多关系是构建复杂应用的关键一步。主要需要注意以下几点:

  1. 关联表定义: 使用 db.Table 定义中间表,包含两个主键外键。
  2. 模型类命名: 确保模型类名与 so.Mapped 或 so.relationship 中引用的名称一致,通常使用单数形式(如 Role 而非 Roles)。这是避免 InvalidRequestError 的常见且重要的一点。
  3. 关系属性类型: so.Mapped[list['ModelName']] 用于表示多对多关系中包含的是另一个模型对象的列表。
  4. secondary 参数: 在 so.relationship 中正确指定关联表。
  5. back_populates 参数: 确保双向关系的属性名在两个模型中保持一致,以便于双向导航。 遵循这些最佳实践,可以有效避免常见的配置错误,并构建出健壮的 Flask 应用数据模型。

相关专题

更多
Python Flask框架
Python Flask框架

本专题专注于 Python 轻量级 Web 框架 Flask 的学习与实战,内容涵盖路由与视图、模板渲染、表单处理、数据库集成、用户认证以及RESTful API 开发。通过博客系统、任务管理工具与微服务接口等项目实战,帮助学员掌握 Flask 在快速构建小型到中型 Web 应用中的核心技能。

85

2025.08.25

Python Flask Web框架与API开发
Python Flask Web框架与API开发

本专题系统介绍 Python Flask Web框架的基础与进阶应用,包括Flask路由、请求与响应、模板渲染、表单处理、安全性加固、数据库集成(SQLAlchemy)、以及使用Flask构建 RESTful API 服务。通过多个实战项目,帮助学习者掌握使用 Flask 开发高效、可扩展的 Web 应用与 API。

71

2025.12.15

js 字符串转数组
js 字符串转数组

js字符串转数组的方法:1、使用“split()”方法;2、使用“Array.from()”方法;3、使用for循环遍历;4、使用“Array.split()”方法。本专题为大家提供js字符串转数组的相关的文章、下载、课程内容,供大家免费下载体验。

254

2023.08.03

js截取字符串的方法
js截取字符串的方法

js截取字符串的方法有substring()方法、substr()方法、slice()方法、split()方法和slice()方法。本专题为大家提供字符串相关的文章、下载、课程内容,供大家免费下载体验。

206

2023.09.04

java基础知识汇总
java基础知识汇总

java基础知识有Java的历史和特点、Java的开发环境、Java的基本数据类型、变量和常量、运算符和表达式、控制语句、数组和字符串等等知识点。想要知道更多关于java基础知识的朋友,请阅读本专题下面的的有关文章,欢迎大家来php中文网学习。

1463

2023.10.24

字符串介绍
字符串介绍

字符串是一种数据类型,它可以是任何文本,包括字母、数字、符号等。字符串可以由不同的字符组成,例如空格、标点符号、数字等。在编程中,字符串通常用引号括起来,如单引号、双引号或反引号。想了解更多字符串的相关内容,可以阅读本专题下面的文章。

617

2023.11.24

java读取文件转成字符串的方法
java读取文件转成字符串的方法

Java8引入了新的文件I/O API,使用java.nio.file.Files类读取文件内容更加方便。对于较旧版本的Java,可以使用java.io.FileReader和java.io.BufferedReader来读取文件。在这些方法中,你需要将文件路径替换为你的实际文件路径,并且可能需要处理可能的IOException异常。想了解更多java的相关内容,可以阅读本专题下面的文章。

548

2024.03.22

php中定义字符串的方式
php中定义字符串的方式

php中定义字符串的方式:单引号;双引号;heredoc语法等等。想了解更多字符串的相关内容,可以阅读本专题下面的文章。

543

2024.04.29

Golang gRPC 服务开发与Protobuf实战
Golang gRPC 服务开发与Protobuf实战

本专题系统讲解 Golang 在 gRPC 服务开发中的完整实践,涵盖 Protobuf 定义与代码生成、gRPC 服务端与客户端实现、流式 RPC(Unary/Server/Client/Bidirectional)、错误处理、拦截器、中间件以及与 HTTP/REST 的对接方案。通过实际案例,帮助学习者掌握 使用 Go 构建高性能、强类型、可扩展的 RPC 服务体系,适用于微服务与内部系统通信场景。

0

2026.01.15

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
Java 教程
Java 教程

共578课时 | 46.1万人学习

国外Web开发全栈课程全集
国外Web开发全栈课程全集

共12课时 | 1.0万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号