【Tortoise-ORM】 基础与数据库操作
文章目录
- 1. 基础概念与安装
- 什么是 ORM(对象关系映射)?
- ORM 的应用场景:
- 安装 Tortoise-ORM
- 安装步骤:
- 2. 数据库模型定义
- 定义模型类
- 基本模型定义
- Tortoise-ORM 的字段类型
- 常见字段类型
- 字段约束
- 使用 `Meta` 类定义模型的额外设置
- 常见的 `Meta` 配置项
- 3. 异步与同步操作
- 学习 Tortoise-ORM 的异步操作
- 异步查询示例
- 异步插入数据
- 异步更新与删除
- 异步操作的优势
- 使用 `Tortoise.init()` 初始化数据库连接
- 初始化数据库连接
- 使用 `Tortoise.close_connections()` 关闭连接
- 异步执行顺序
- 同步操作
- 同步查询示例
- 4. 查询与操作
- 查询操作
- 常用查询方法
- 链式查询
- 条件查询
- 排序与分页
- 数据操作
- 增:`create()` 和 `save()`
- 删:`delete()`
- 改:`update()`
- 批量操作
- 注意事项
- 代码练习:创建用户表,插入数据后成功查询
- 5. 关联查询
- 外键关联
- 设置外键关联
- 使用外键字段
- `prefetch_related` 和 `select_related` 优化查询
- `select_related`
- `prefetch_related`
- `select_related` 与 `prefetch_related` 的区别
- 避免 N+1 查询问题
1. 基础概念与安装
什么是 ORM(对象关系映射)?
ORM(Object-Relational Mapping)是一种用于将对象模型与关系型数据库表格之间进行映射的技术。简单来说,ORM 允许开发者以面向对象的方式操作数据库,而不必直接编写 SQL 查询语句。
传统的关系型数据库(如 MySQL、PostgreSQL)使用表格来存储数据,而对象导向编程语言(如 Python)使用对象来表示数据。ORM 通过创建数据库表与类之间的映射关系,简化了数据存取的操作,并在底层自动生成 SQL 语句,从而提高开发效率,减少了开发者与数据库的直接交互。
ORM 的应用场景:
- 简化代码:开发者可以通过操作 Python 类的对象,而不是编写冗长的 SQL 查询语句来实现数据的插入、删除、更新和查询等操作。
- 提高可维护性:通过对象来表示数据库表,可以更好地遵循面向对象的设计原则,使得代码更加模块化、易于维护。
- 数据库独立性:ORM 框架通常支持多种数据库,使得应用程序可以在不同的数据库之间进行迁移而不需要修改大量的代码。
安装 Tortoise-ORM
Tortoise-ORM 是一个轻量级且支持异步操作的 Python ORM 框架,旨在帮助开发者高效地与数据库进行交互。与传统的 ORM 不同,Tortoise-ORM 使用了 Python 的 asyncio
库,支持异步编程,适用于高并发的 Web 应用开发。
安装步骤:
-
安装 Tortoise-ORM
可以通过pip
安装 Tortoise-ORM:pip install tortoise-orm
-
配置数据库连接
Tortoise-ORM 支持多种数据库,包括 SQLite、MySQL 和 PostgreSQL。我们可以根据项目需求选择合适的数据库。- SQLite:SQLite 是一个轻量级的嵌入式数据库,适合小型应用或开发阶段使用。
- MySQL:MySQL 是一种流行的关系型数据库,适合中大型项目。
- PostgreSQL:PostgreSQL 是一个功能强大的开源关系型数据库,适合高并发、复杂查询的应用。
配置数据库连接时,需要在应用程序中定义数据库的连接信息。例如,假设使用 SQLite 数据库,可以通过以下方式进行配置:
from tortoise import Tortoise async def init(): await Tortoise.init( db_url='sqlite://db.sqlite3', # SQLite 数据库 modules={'models': ['your_app.models']} # 配置模型模块 ) await Tortoise.generate_schemas() # 自动生成数据库表
如果使用的是 MySQL 或 PostgreSQL,只需将
db_url
替换为相应的数据库连接字符串,例如:- MySQL:
mysql://user:password@localhost:3306/dbname
- PostgreSQL:
postgres://user:password@localhost:5432/dbname
通过上述步骤,我们就可以顺利安装并配置 Tortoise-ORM,准备好与数据库进行交互了。
2. 数据库模型定义
定义模型类
在 Tortoise-ORM 中,数据库模型是通过 Python 类来定义的。每个类代表数据库中的一张表,而类的属性则代表表中的每一列。Tortoise-ORM 会根据模型的定义自动生成数据库表,并提供增、删、改、查等操作接口。
基本模型定义
定义模型类的基本步骤如下:
- 继承
Model
类:模型类需要继承 Tortoise-ORM 提供的tortoise.models.Model
基类,这样它才能成为一个数据库模型。 - 定义字段:使用 Tortoise-ORM 提供的字段类来定义模型的字段,如
CharField
、IntField
、DatetimeField
等。 - 字段的类型:每个字段都需要指定其数据类型,例如
CharField
用于存储字符串,IntField
用于存储整数等。
例如,假设我们有一个 User
模型类,用于存储用户的基本信息:
from tortoise import fields
from tortoise.models import Model
class User(Model):
id = fields.IntField(pk=True) # 主键字段,表示每一行数据的唯一标识
username = fields.CharField(max_length=50, unique=True) # 用户名,最大长度50,且唯一
email = fields.CharField(max_length=100, unique=True) # 电子邮件,最大长度100,且唯一
age = fields.IntField() # 年龄,存储整数
created_at = fields.DatetimeField(auto_now_add=True) # 自动记录用户创建时间
class Meta:
table = "users" # 指定数据库表名
indexes = ["username"] # 在 username 列上创建索引,提升查询效率
在上面的代码中:
id
是主键字段,pk=True
表示该字段为主键。username
和email
字段都设置了unique=True
,表示这两个字段的值在数据库中必须是唯一的。created_at
字段使用了auto_now_add=True
,它会自动记录每个User
对象被创建的时间。
Tortoise-ORM 的字段类型
Tortoise-ORM 提供了多种字段类型,来适应不同的数据存储需求。每种字段类型对应数据库中的一种数据类型。
常见字段类型
-
CharField:用于存储字符串,需要指定
max_length
参数,表示该字段最大允许的字符长度。例如,CharField(max_length=50)
表示最多存储 50 个字符。 -
IntField:用于存储整数数据。例如,
IntField()
可以存储整数类型的数据,常用于表示年龄、数量等字段。 -
FloatField:用于存储浮动点数数据。适合表示小数值,例如价格、评分等。
-
BooleanField:用于存储布尔值(True 或 False)。例如,
is_active = fields.BooleanField(default=True)
可以表示一个用户是否激活。 -
TextField:用于存储长文本数据,没有字符长度限制。适用于存储文章内容、评论等大量文本数据。
-
DateField 和 DatetimeField:分别用于存储日期和日期时间。
DateField()
只存储日期,而DatetimeField()
存储日期和时间。例如,created_at = fields.DatetimeField(auto_now_add=True)
用于自动记录创建时间。 -
ForeignKeyField:用于定义模型间的一对多关系。例如,一个用户可以有多个订单,这时可以在
Order
模型中通过ForeignKeyField
引用User
模型。class Order(Model): user = fields.ForeignKeyField("models.User", related_name="orders") # 外键关联到 User 模型
-
ManyToManyField:用于定义多对多关系。例如,一个学生可以选修多门课程,而一门课程也可以有多个学生选修。
-
JSONField:用于存储 JSON 数据,适用于存储不确定结构的复杂数据。此字段类型通常用于支持 PostgreSQL 数据库。
字段约束
字段约束是指对字段数据的限制或规则。通过设置字段约束,可以确保数据库中的数据符合业务要求和数据完整性。
-
unique=True
:表示字段的值在整个表中必须是唯一的。如果为某个字段设置了unique=True
,那么在插入数据时,数据库会检查该字段值是否已存在,若已存在则会抛出错误。例如,email = fields.CharField(max_length=100, unique=True)
表示每个用户的电子邮件必须唯一。 -
null=True
:表示字段允许为空(NULL)。如果一个字段没有数据,可以将null
设置为True
,表示该字段可以存储NULL
值。例如,middle_name = fields.CharField(max_length=50, null=True)
表示中间名可以为空。 -
default=value
:用于指定字段的默认值。如果在创建对象时没有给该字段赋值,Tortoise-ORM 会自动使用默认值。例如,is_active = fields.BooleanField(default=True)
表示用户默认是激活状态。 -
max_length=value
:用于设置字段的最大长度。例如,name = fields.CharField(max_length=100)
表示name
字段的最大长度为 100 个字符。
使用 Meta
类定义模型的额外设置
在 Tortoise-ORM 中,Meta
类用于定义模型的一些元信息。它可以控制模型的表名、索引、约束等属性。Meta
类是一个内部类,在模型类中定义,并不会直接影响到模型本身的字段。
常见的 Meta
配置项
-
table
:指定数据库表的名称。Tortoise-ORM 默认会使用类名的小写形式作为表名,但可以通过Meta
类中的table
属性显式指定表名。例如:class User(Model): # 字段定义... class Meta: table = "users" # 明确指定表名
-
indexes
:定义数据库表的索引。索引通常用于加速查询操作,尤其是在查询频繁的字段上创建索引能够显著提升性能。例如,以下代码将在username
字段上创建索引:class User(Model): # 字段定义... class Meta: indexes = ["username"] # 在 username 列上创建索引
-
unique_together
:用于定义多个字段的联合唯一约束。它确保多个字段的组合值在整个表中是唯一的。例如,在User
模型中,我们可以要求username
和email
的组合唯一:class User(Model): # 字段定义... class Meta: unique_together = (("username", "email"),) # 联合唯一约束
-
ordering
:定义模型的默认排序规则。例如,以下代码会让查询结果默认按照created_at
字段降序排列:class User(Model): # 字段定义... class Meta: ordering = ["-created_at"] # 按创建时间降序排列
3. 异步与同步操作
Tortoise-ORM 是一个异步支持的 ORM 框架,这意味着它能够利用 Python 的 asyncio
库来执行异步数据库操作,从而提高高并发环境下应用的性能。通过异步操作,数据库查询、插入、更新和删除等操作不会阻塞主线程,允许程序同时处理其他任务。
学习 Tortoise-ORM 的异步操作
在 Tortoise-ORM 中,所有与数据库交互的操作都可以通过 async/await
语法进行异步处理。通过这种方式,程序可以在执行数据库操作时不中断其他任务,尤其适用于 Web 应用或 API 后端,能够在处理请求时高效地执行多个数据库操作。
异步查询示例
在 Tortoise-ORM 中,查询数据库通常使用异步方法。例如,假设我们有一个 User
模型,并希望查询所有用户的列表。我们可以使用 await
关键字来执行异步查询:
from tortoise import Tortoise
from models import User # 假设 User 模型已经定义
async def get_all_users():
users = await User.all() # 异步查询所有用户
return users
# 使用异步查询并处理结果
async def main():
await Tortoise.init(
db_url='sqlite://db.sqlite3', # 配置数据库
modules={'models': ['your_app.models']}
)
users = await get_all_users()
for user in users:
print(user.username)
await Tortoise.close_connections() # 关闭数据库连接
在上面的代码中,User.all()
是一个异步查询方法,await
关键字会等待查询结果返回。在异步函数中,我们使用 Tortoise.init()
初始化数据库连接,完成操作后调用 Tortoise.close_connections()
关闭连接。
异步插入数据
也可以使用异步方法插入数据。例如,插入一个新用户:
async def create_user(username, email):
user = await User.create(username=username, email=email)
print(f"User {user.username} created!")
在这个例子中,User.create()
方法是异步的,它会在数据库中插入一条新的记录,并返回插入的对象。
异步更新与删除
Tortoise-ORM 还支持异步更新和删除操作。假设我们要更新某个用户的信息,可以使用 update()
方法:
async def update_user(user_id, new_email):
user = await User.get(id=user_id) # 获取指定用户
user.email = new_email # 更新电子邮件地址
await user.save() # 异步保存更新后的数据
print(f"User {user.username}'s email updated to {new_email}")
同样地,删除操作也可以通过 delete()
方法来实现:
async def delete_user(user_id):
user = await User.get(id=user_id)
await user.delete() # 删除指定用户
print(f"User {user.username} deleted")
异步操作的优势
- 非阻塞性:通过异步操作,数据库查询、插入、更新和删除等操作不会阻塞其他任务,可以同时处理多个请求。
- 提高并发性能:在高并发的 Web 应用或 API 中,异步操作能够显著提高性能,减少响应时间。
- 轻量级:异步操作相比于多线程或多进程的方式更加轻量,避免了线程切换带来的开销。
使用 Tortoise.init()
初始化数据库连接
在 Tortoise-ORM 中,Tortoise.init()
用于初始化与数据库的连接。通常在应用程序的启动时调用 Tortoise.init()
,并指定数据库的连接 URL 和模型模块。
初始化数据库连接
from tortoise import Tortoise
async def init_db():
await Tortoise.init(
db_url='sqlite://db.sqlite3', # 数据库连接 URL(可以是 SQLite、MySQL 或 PostgreSQL)
modules={'models': ['your_app.models']} # 指定模型模块路径
)
await Tortoise.generate_schemas() # 自动生成数据库表
在上述代码中,Tortoise.init()
配置了数据库连接的 URL 和包含模型类的模块路径。Tortoise.generate_schemas()
会根据定义的模型自动创建数据库表。
-
db_url
:数据库连接字符串,具体格式根据所使用的数据库类型不同而有所变化。常见的数据库连接 URL 格式如下:- SQLite:
sqlite://db.sqlite3
- MySQL:
mysql://user:password@localhost:3306/dbname
- PostgreSQL:
postgres://user:password@localhost:5432/dbname
- SQLite:
-
modules
:指定包含模型类的 Python 模块路径。
使用 Tortoise.close_connections()
关闭连接
一旦数据库操作完成,我们需要关闭与数据库的连接。这是一个好习惯,能够确保释放资源并保持数据库连接池的健康。Tortoise.close_connections()
用于关闭与数据库的连接。
async def close_db():
await Tortoise.close_connections()
异步执行顺序
由于数据库操作是异步执行的,执行顺序对于处理结果至关重要。确保在异步函数中,所有的数据库操作都在 await
关键字后执行,且连接在所有操作完成后关闭。例如:
async def main():
await init_db() # 初始化数据库连接
users = await get_all_users() # 异步获取所有用户
for user in users:
print(user.username) # 打印用户信息
await close_db() # 完成所有操作后关闭数据库连接
同步操作
尽管 Tortoise-ORM 主要是为异步操作设计的,但在某些情况下,可能需要执行同步操作。Tortoise-ORM 也支持同步方法,特别是在某些不支持异步的环境中可以使用同步版本的方法。
同步查询示例
同步查询使用 await
去掉,直接通过同步函数调用:
from tortoise import Tortoise
def get_all_users_sync():
return User.all() # 同步查询
请注意,使用同步方法时,需要确保数据库操作不会阻塞程序的主线程,因此推荐尽可能使用异步方式,特别是在高并发的 Web 应用中。
4. 查询与操作
Tortoise-ORM 提供了丰富的查询和操作方法,使得在与数据库交互时能够更直观地进行数据的查询、插入、更新和删除等操作。以下是一些常用的查询方法和数据操作方法,帮助更加高效地操作数据库。
查询操作
Tortoise-ORM 提供了多种查询方法,支持常见的数据库查询需求。通过这些方法,可以方便地执行数据的筛选、过滤、分页等操作。
常用查询方法
-
all()
用于查询所有记录,返回一个QuerySet
对象,表示所有符合条件的记录。users = await User.all() # 获取所有用户
-
filter()
用于根据指定条件进行过滤,返回满足条件的记录。filter()
方法允许指定多个条件。active_users = await User.filter(is_active=True) # 获取所有活跃用户
-
exclude()
用于排除符合指定条件的记录,返回不符合条件的记录。它与filter()
正好相反。inactive_users = await User.exclude(is_active=True) # 获取所有不活跃用户
-
get()
用于查询符合条件的唯一记录。如果没有符合条件的记录,或者有多条记录符合条件,会抛出异常DoesNotExist
或MultipleObjectsReturned
。user = await User.get(id=1) # 获取 ID 为 1 的用户
-
first()
用于获取查询结果中的第一条记录,如果没有找到记录,则返回None
。这是一个安全的查询方法,避免了DoesNotExist
异常。user = await User.filter(is_active=True).first() # 获取第一个活跃用户
链式查询
Tortoise-ORM 支持链式查询,通过连续调用查询方法来实现复杂的查询。每个查询方法都会返回一个新的 QuerySet
,可以继续调用其他查询方法。
users = await User.filter(is_active=True).exclude(username="admin").all() # 获取所有活跃用户,排除用户名为 admin 的用户
在这个例子中,我们先使用 filter()
过滤出所有活跃的用户,然后用 exclude()
排除用户名为 “admin” 的用户,最后使用 all()
获取所有符合条件的用户。
条件查询
Tortoise-ORM 支持多种条件查询,可以在查询时根据字段进行条件判断。常见的条件操作符包括:
exact
:精确匹配gte
:大于等于lte
:小于等于in
:匹配指定列表中的任意值isnull
:检查字段是否为NULL
# 查询所有年龄大于等于 18 岁的用户
adult_users = await User.filter(age__gte=18).all()
# 查询用户名为 "admin" 或 "guest" 的用户
users = await User.filter(username__in=["admin", "guest"]).all()
# 查询电子邮件为空的用户
users_with_no_email = await User.filter(email__isnull=True).all()
排序与分页
Tortoise-ORM 支持通过 order_by()
方法进行排序,并且可以通过 limit()
和 offset()
实现分页功能。
# 按照年龄升序排序
users = await User.all().order_by("age")
# 获取前 10 个用户
users = await User.all().limit(10)
# 跳过前 5 个用户,获取接下来的 10 个用户(分页)
users = await User.all().offset(5).limit(10)
数据操作
Tortoise-ORM 提供了方便的方法来执行数据操作,如增、删、改等操作。以下是常用的数据操作方法:
增:create()
和 save()
-
create()
create()
方法用于创建一条新的记录,并将其保存到数据库中。它是一个快捷的操作方法,适用于直接创建和保存对象的情况。user = await User.create(username="john_doe", email="john@example.com", age=30) print(f"User {user.username} created successfully!")
在上面的代码中,我们使用
create()
方法创建了一个新用户,并立即将其保存到数据库中。 -
save()
save()
方法用于保存或更新已经存在的对象。如果对象已经存在,调用save()
会执行更新操作;如果对象是新创建的,则会执行插入操作。user = await User.get(id=1) # 获取 ID 为 1 的用户 user.age = 31 # 更新用户年龄 await user.save() # 保存更新后的用户信息
save()
方法常用于更新已有的记录。如果需要更新的字段已经存在于对象中,只需修改对应字段后调用save()
即可。
删:delete()
-
delete()
delete()
方法用于删除对象。可以通过查询获取一个对象并删除它,或者使用filter()
获取一组对象并删除它们。user = await User.get(id=1) # 获取 ID 为 1 的用户 await user.delete() # 删除该用户 print(f"User {user.username} deleted successfully!")
delete()
方法会删除对象本身对应的数据库记录。如果要删除符合特定条件的多个对象,可以使用filter()
查询并进行批量删除:await User.filter(age__lte=18).delete() # 删除所有年龄小于等于 18 岁的用户
改:update()
-
update()
update()
方法用于批量更新记录。它允许在一次操作中更新多个记录的某些字段。# 批量更新所有活跃用户的年龄为 35 await User.filter(is_active=True).update(age=35)
在这个例子中,我们使用
filter()
方法选中了所有活跃的用户,并通过update()
方法将他们的年龄批量更新为 35。
批量操作
Tortoise-ORM 还支持批量插入、更新和删除操作。在高并发或大数据量的场景中,批量操作可以显著提高效率。
# 批量创建多个用户
await User.bulk_create([
User(username="user1", email="user1@example.com", age=25),
User(username="user2", email="user2@example.com", age=28),
])
注意事项
save()
与create()
:create()
用于快速创建新记录并保存,而save()
则适用于更新已有对象。- 删除操作:
delete()
会删除对象本身,而filter().delete()
可以批量删除符合条件的记录。 update()
:update()
方法允许在不加载对象的情况下批量更新数据,适用于批量更新场景。
代码练习:创建用户表,插入数据后成功查询
我们首先将文件组织到一个名为 User
的包中,以便更好地管理和组织代码。
下面依次介绍每一步的步骤:
步骤 1:创建 User
包
- 在项目根目录下创建一个名为
User
的文件夹。 - 在
User
文件夹中创建以下文件:__init__.py
(包初始化文件)models.py
(包含用户模型的文件)db_operations.py
(包含数据库操作的文件)
步骤 2:修改 models.py
文件
models.py
文件用于定义数据库模型,内容与之前相同:
# User/models.py
from tortoise import fields
from tortoise.models import Model
class User(Model):
id = fields.IntField(pk=True)
username = fields.CharField(max_length=50, unique=True)
email = fields.CharField(max_length=100, unique=True)
age = fields.IntField()
created_at = fields.DatetimeField(auto_now_add=True)
class Meta:
table = "users"
indexes = ["username"]
步骤 3:修改 db_operations.py
文件
db_operations.py
文件将包含与数据库交互的操作(连接数据库、插入数据、查询数据等):
# User/db_operations.py
from tortoise import Tortoise
from tortoise.transactions import in_transaction
from User.models import User # 导入 User 模型
async def create_users():
await User.create(username="alice", email="alice@example.com", age=25)
await User.create(username="bob", email="bob@example.com", age=30)
await User.create(username="charlie", email="charlie@example.com", age=28)
async def query_users():
users = await User.all() # 获取所有用户
for user in users:
print(f"Username: {user.username}, Email: {user.email}, Age: {user.age}, Created At: {user.created_at}")
async def init():
# 初始化数据库连接
await Tortoise.init(
db_url='sqlite://db.sqlite3', # 使用 SQLite 数据库
modules={'models': ['User.models']} # 引用 User 包下的 models.py 文件
)
# 生成数据库表
await Tortoise.generate_schemas()
# 插入数据
await create_users()
# 查询数据
await query_users()
# 关闭数据库连接
await Tortoise.close_connections()
步骤 4:创建主程序文件
然后,可以在项目根目录下创建一个 main.py
文件,它将调用 User
包中的数据库操作:
# main.py
from tortoise import run_async
from User.db_operations import init # 从 User 包中导入 init 函数
# 使用 run_async 来运行异步代码
run_async(init())
步骤 5:结构组织
最终的项目目录结构应该如下所示:
your_project/
│
├── User/ # User 包
│ ├── __init__.py # 包初始化文件
│ ├── models.py # 用户模型
│ └── db_operations.py # 数据库操作
│
├── db.sqlite3 # SQLite 数据库文件(运行时自动生成)
└── main.py # 主程序入口文件
步骤 6:运行代码
现在,使用 PyCharm 运行 main.py
文件,它会自动引导执行数据库操作,包括初始化数据库、创建表、插入数据和查询数据。
5. 关联查询
在实际的应用中,我们通常会涉及到多个表之间的关联查询。Tortoise-ORM 提供了强大的关联查询支持,包括外键关联、预加载以及优化查询等功能。掌握这些内容可以更加高效地进行复杂查询操作,避免 N+1 查询问题。
外键关联
外键关联是数据库中最常见的一种关联关系。Tortoise-ORM 通过定义外键字段来表示模型之间的关系。通过外键,可以在模型之间建立一对多或多对多的关系。
设置外键关联
在 Tortoise-ORM 中,可以使用 ForeignKeyField
来设置外键关系。外键字段表示模型之间的一对多关系,指向另一个模型的主键。
假设有两个模型:Author
和 Book
,其中一个作者(Author
)可以有多本书(Book
)。这时,Book
模型中需要有一个外键字段指向 Author
。
from tortoise import fields
from tortoise.models import Model
class Author(Model):
id = fields.IntField(pk=True)
name = fields.CharField(max_length=100)
# 关系:一个作者可以有多本书
books = fields.ReverseRelation["Book"]
class Book(Model):
id = fields.IntField(pk=True)
title = fields.CharField(max_length=200)
publication_year = fields.IntField()
# 外键:每本书有一个作者
author = fields.ForeignKeyField("models.Author", related_name="books")
在这个例子中,Book
模型中有一个 ForeignKeyField
字段指向 Author
模型,表示每本书都有一个对应的作者。related_name
参数指定了反向关系,使得可以通过 Author
模型的 books
字段访问到所有与该作者关联的书籍。
使用外键字段
在查询时,可以通过外键字段方便地获取关联的数据。例如,查询一本书的作者:
book = await Book.get(id=1) # 获取 ID 为 1 的书
author = await book.author # 获取与该书关联的作者
print(f"The author of '{book.title}' is {author.name}.")
如果想获取一个作者的所有书籍,可以通过 books
字段进行访问:
author = await Author.get(id=1) # 获取 ID 为 1 的作者
books = await author.books.all() # 获取该作者的所有书籍
prefetch_related
和 select_related
优化查询
在进行关联查询时,如果没有正确地优化查询,可能会导致 N+1 查询问题。这种问题通常出现在通过外键字段多次查询数据库,导致对数据库发出多个查询请求,效率低下。
Tortoise-ORM 提供了两种方法来优化关联查询:prefetch_related
和 select_related
。
select_related
select_related
用于优化一对一或外键关联查询。它通过 SQL 的 JOIN
语句将关联对象和主查询结果合并到一个查询中,从而避免多次查询数据库。
例如,如果查询一本书,并且同时获取它的作者,可以使用 select_related
来将书籍和作者的查询合并:
book = await Book.all().select_related("author").get(id=1)
print(f"The author of '{book.title}' is {book.author.name}.")
在这个查询中,select_related("author")
表示在查询书籍的同时,关联查询作者。这样,Tortoise-ORM 会自动生成一个 SQL 查询,通过 JOIN
将 Book
和 Author
表联合查询,避免了多次查询数据库。
prefetch_related
prefetch_related
用于优化多对多和反向外键查询。与 select_related
不同,prefetch_related
不通过 JOIN
语句,而是生成一个额外的查询,然后将结果进行合并。
假设有多个作者,并且希望获取每个作者的所有书籍,可以使用 prefetch_related
来优化查询:
authors = await Author.all().prefetch_related("books").limit(10)
for author in authors:
print(f"Author: {author.name}")
for book in author.books:
print(f"- {book.title}")
在这个例子中,prefetch_related("books")
会首先查询所有作者,然后再单独查询所有与这些作者关联的书籍,并将结果合并到一起。这样,避免了为每个作者单独发起查询,从而减少了数据库的查询次数。
select_related
与 prefetch_related
的区别
select_related
:用于一对一或外键关系,通过 SQL 的JOIN
合并查询,适用于关联对象较少的情况。prefetch_related
:用于多对多或反向外键关系,通过额外查询和结果合并的方式优化查询,适用于关联对象较多的情况。
避免 N+1 查询问题
N+1 查询问题是指在查询多个对象时,对于每个对象都执行一次查询,导致查询次数大大增加,严重影响性能。通过使用 select_related
和 prefetch_related
,可以避免这一问题。
- 示例: 假设要查询多个作者及其所有书籍。如果不使用优化方法,可能会先查询所有作者,然后对每个作者单独查询他们的书籍,导致大量查询。
# 错误做法:会导致 N+1 查询问题
authors = await Author.all()
for author in authors:
books = await author.books.all() # 对每个作者执行一个单独的查询
- 正确做法: 使用
prefetch_related
来优化查询,避免对每个作者单独发起查询。
# 正确做法:通过 prefetch_related 优化查询
authors = await Author.all().prefetch_related("books")
for author in authors:
for book in author.books:
print(f"{author.name}: {book.title}")
通过以上优化,Tortoise-ORM 会在后台自动优化 SQL 查询,减少了多次查询带来的性能问题。