SQLAlchemy:高效加载三元多对多关系
我想用 async SQLALchemy 2.0 创建一个简单的用户管理数据库,具体想法如下:
- 一个用户在一个项目中只能有 一个 角色
- 一个用户可以在不同的项目中拥有不同的角色
- 一个用户可以参与多个项目(这些项目可能有成千上万的文档)
这是我目前的代码:
class User(db.Model):
__tablename__ = 'user'
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(60), index=True, unique=True)
user_project_roles = relationship('UserProjectRoleLink', back_populates='user')
class Project(db.Model):
__tablename__ = 'device'
id = db.Column(db.Integer, primary_key=True)
project_name = db.Column(db.String(60), unique=True)
user_project_roles = relationship('UserProjectRoleLink', back_populates='project')
documents = relationship('Document', back_populates='project') # thousands of documents
class Document(db.Model)
__tablename__ = 'device'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(60), unique=True)
text =db.Column(db.String(60))
project = relationship('Project', back_populates="documents")
class Role(db.Model):
__tablename__ = "role"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(60), unique=True)
user_project_roles = relationship('UserProjectRoleLink', back_populates='role')
class UserProjectRoleLink(db.Model):
user_id = Column(Integer, ForeignKey('user.id'), primary_key=True)
project_id = Column(Integer, ForeignKey('project.id'), primary_key=True)
role_id = Column(Integer, ForeignKey('role.id'), primary_key=True)
user = relationship('User', back_populates='user_project_roles')
role = relationship('Role', back_populates='user_project_roles')
project = relationship('Project', back_populates='user_project_roles')
每当我选择一个用户(使用 select(User).where(User.id == id)),UserProjectRoleLink 是一个表,其中包含 user_id
、role_id
和 project_id
的信息,而用户之间的关系(包括循环关系)并没有被加载。
我该如何在同一次选择中加载角色和项目的详细信息,但又不加载成千上万的文档呢?
在我的 REST API 中,我希望能返回一个用户的响应,例如:
{
"id": 234,
"username": "JohnDoe",
"project_roles":[
{
"project_name":"Project 1",
"project_id": 1,
"role:"Admin",
"role_id": 1
},
{
"project_name":"Project 2",
"project_id": 2,
"role:"User",
"role_id": 2
}
]
}
我该如何预加载所有项目和角色的关系(而不是逐个遍历并动态加载,这样会很耗费资源),但又不加载用户之间的关系,因为那样会导致递归错误?
另外,从项目的角度,我想查询哪些用户可以访问特定项目,以及他们的角色是什么。项目结果如下:
{
"id":2,
"project_name":"Project 1",
"users":[
{
"user_id": 234,
"user_name": JohnDoe,
"role":"Admin",
"role_id: "1",
}
]
}
我尝试过 joinedLoading
选项,但不知道如何有效地加载每个 user_project_roles
,因为当从用户那边获取时,我得到的是一个 user_project_roles
的列表。
我也考虑过 secondary joins
,但没有成功。
1 个回答
你可以通过链式调用来指定急切加载,也可以一起使用多个选项。
在这里,我们通过外连接从用户(User)加载到用户项目角色链接(UserProjectRoleLink),再到项目(Project)。然后在这个查询加载完成后,我们根据第一次查询中获取的角色ID来查找角色。所以这应该只会产生两个 SELECT
语句。
q = select(
User
).options(
joinedload(
User.user_project_roles
).options(
joinedload(UserProjectRoleLink.project),
selectinload(UserProjectRoleLink.role)
).where(User.id == user_id)
这里有一个示例,使用了这些子选项,具体可以查看这个链接 指定子选项与加载选项
只要你不引用 project.documents
,那么文档就不会被急切加载。根据你的序列化方式,比如说使用 jsonify 或其他方式,你需要排除这个属性。
然后你可以这样输出你的数据:
return [{
"id": user.id,
"username": user.username,
"project_roles": [{
"project_name": upr.project.project_name,
"project_id": upr.project.id,
"role": upr.role.name,
"role_id": upr.role.id,
} for upr in user.user_project_roles],
} for user in session.scalars(q)]
异步(async)我认为是一样的,只是加了一个 await
。
[{} for user in (await session.scalars(q))]