在 Tortoise-ORM 中,关联关系(Relationships)是构建复杂数据模型的核心功能。它支持三种经典关系:
- 一对一(One-to-One)
- 一对多(One-to-Many)
- 多对多(Many-to-Many)
下面我将通过 清晰的代码示例 + 查询用法 + 注意事项,逐一讲解这三种关系在 Tortoise-ORM 中的实现方式。
🧩 前提:基础模型结构
我们以一个常见的博客系统为例:
- User:用户
- Profile:用户资料(一对一)
- Post:文章(一对多:一个用户有多篇文章)
- Tag:标签(多对多:一篇文章有多个标签,一个标签可用于多篇文章)
✅ 一、一对一(One-to-One)
场景:每个用户有且仅有一个个人资料(Profile)
🔹 模型定义
from tortoise.models import Model
from tortoise import fields
class User(Model):
id = fields.IntField(pk=True)
username = fields.CharField(max_length=50, unique=True)
class Profile(Model):
id = fields.IntField(pk=True)
bio = fields.TextField()
# 关键:OneToOneField
user = fields.OneToOneField('models.User', on_delete=fields.CASCADE)'models.User':指向模型的路径(模块名.类名)on_delete=fields.CASCADE:当用户被删除时,资料也自动删除
💡 谁持有外键?
在一对一中,“附属”模型(Profile)持有外键,指向主模型(User)。
🔹 使用示例
创建
user = await User.create(username="alice")
profile = await Profile.create(bio="Python developer", user=user)查询(正向)
profile = await Profile.get(id=1).prefetch_related('user')
print(profile.user.username) # "alice"查询(反向)
user = await User.get(id=1).prefetch_related('profile') # ❌ 默认没有反向属性!⚠️ 问题:默认情况下,User 没有 .profile 属性!
✅ 解决方案:使用 related_name 显式定义反向关系:
class Profile(Model):
user = fields.OneToOneField(
'models.User',
related_name='profile', # ← 关键!
on_delete=fields.CASCADE
)现在就可以:
user = await User.get(username="alice")
await user.fetch_related('profile') # 或 prefetch_related
print(user.profile.bio) # "Python developer"✅ 最佳实践:始终为 OneToOneField 设置 related_name✅ 二、一对多(One-to-Many)
场景:一个用户可以写多篇文章,一篇文章只属于一个用户
🔹 模型定义
class User(Model):
id = fields.IntField(pk=True)
username = fields.CharField(max_length=50)
class Post(Model):
id = fields.IntField(pk=True)
title = fields.CharField(max_length=200)
# 外键指向 User
author = fields.ForeignKeyField(
'models.User',
related_name='posts' # ← 反向关系名
)ForeignKeyField表示“多”的一方(Post)持有外键related_name='posts':在User上生成.posts属性
🔹 使用示例
创建
user = await User.create(username="bob")
post1 = await Post.create(title="FastAPI Guide", author=user)
post2 = await Post.create(title="Tortoise ORM", author=user)查询:用户的所有文章(反向)
user = await User.get(username="bob").prefetch_related('posts')
for post in user.posts:
print(post.title)
# 输出:
# FastAPI Guide
# Tortoise ORM查询:文章的作者(正向)
post = await Post.get(id=1).prefetch_related('author')
print(post.author.username) # "bob"过滤:查找某用户的文章
posts = await Post.filter(author__username="bob").all()✅ 注意:
author是字段名author__username是跨表查询语法(双下划线)
✅ 三、多对多(Many-to-Many)
场景:一篇文章可以有多个标签,一个标签可用于多篇文章
🔹 模型定义
class Tag(Model):
id = fields.IntField(pk=True)
name = fields.CharField(max_length=30, unique=True)
class Post(Model):
id = fields.IntField(pk=True)
title = fields.CharField(max_length=200)
# 多对多关系
tags = fields.ManyToManyField(
'models.Tag',
related_name='posts'
)- Tortoise 会自动创建中间表(如
post_tag) - 无需手动定义中间模型(除非需要额外字段)
🔹 使用示例
创建并关联
post = await Post.create(title="Async Python")
tag1 = await Tag.create(name="python")
tag2 = await Tag.create(name="async")
# 关联
await post.tags.add(tag1, tag2)
# 或
await tag1.posts.add(post)查询:文章的所有标签
post = await Post.get(id=1).prefetch_related('tags')
for tag in post.tags:
print(tag.name)
# python
# async查询:带某个标签的所有文章
posts = await Post.filter(tags__name="python").prefetch_related('tags')移除关联
await post.tags.clear() # 清空所有标签
await post.tags.remove(tag1) # 移除单个标签🔸 高级:自定义中间表(带额外字段)
如果需要在关联中存储额外信息(如“打标签时间”),需手动定义中间模型:
class PostTag(Model):
post = fields.ForeignKeyField('models.Post')
tag = fields.ForeignKeyField('models.Tag')
tagged_at = fields.DatetimeField(auto_now_add=True)
class Meta:
unique_together = ("post", "tag") # 防止重复关联
class Post(Model):
title = fields.CharField(max_length=200)
# 通过中间模型定义多对多
tags = fields.ManyToManyField(
'models.Tag',
through='models.PostTag',
related_name='posts'
)⚠️ 使用through后,.add()等方法仍可用,但无法直接设置中间表字段。
如需设置tagged_at,需手动创建PostTag对象。
📊 四、三种关系对比总结
| 关系类型 | 字段类型 | 谁持外键 | 反向访问 | 典型场景 |
|---|---|---|---|---|
| 一对一 | OneToOneField | 附属模型(Profile) | 需 related_name | 用户 ↔ 资料、订单 ↔ 发票 |
| 一对多 | ForeignKeyField | “多”方(Post) | 自动通过 related_name | 用户 ↔ 文章、分类 ↔ 商品 |
| 多对多 | ManyToManyField | 自动生成中间表 | 双向通过 related_name | 文章 ↔ 标签、学生 ↔ 课程 |
⚠️ 五、重要注意事项
1. 必须使用 prefetch_related 或 fetch_related
否则反向/多对多关系不会加载,访问时报错或为空。
# ❌ 错误:未预加载
user = await User.get(id=1)
print(len(user.posts)) # 可能报错或返回 0
# ✅ 正确
user = await User.get(id=1).prefetch_related('posts')2. related_name 是反向查询的关键
- 如果不设,Tortoise 会自动生成(如
post_set),但不直观 - 强烈建议显式指定
3. 删除行为(on_delete)
CASCADE:级联删除SET_NULL:设为 NULL(字段需null=True)RESTRICT(默认):阻止删除(如果有子记录)
4. 查询性能
- 多层关联时,尽量用
prefetch_related减少数据库查询次数 - 避免在循环中做异步查询(N+1 问题)
✅ 六、完整可运行示例(FastAPI + Tortoise)
from fastapi import FastAPI
from tortoise.models import Model
from tortoise import fields
from tortoise.contrib.fastapi import register_tortoise
app = FastAPI()
class User(Model):
id = fields.IntField(pk=True)
username = fields.CharField(max_length=50)
class Post(Model):
id = fields.IntField(pk=True)
title = fields.CharField(max_length=200)
author = fields.ForeignKeyField('models.User', related_name='posts')
@app.get("/users/{user_id}/posts")
async def get_user_posts(user_id: int):
user = await User.get(id=user_id).prefetch_related('posts')
return {
"user": user.username,
"posts": [{"id": p.id, "title": p.title} for p in user.posts]
}
register_tortoise(
app,
db_url="sqlite://db.sqlite3",
modules={"models": ["__main__"]},
generate_schemas=True,
)
评论已关闭