文章目录
十、 关系和连接
在本文档中,我们将介绍 Peewee 如何处理模型之间的关系。
10.8 自联接
Peewee 支持构建包含自联接的查询。
10.8.1 使用模型别名
要在同一个模型(表)上连接两次,需要创建一个模型别名来表示查询中表的第二个实例。考虑以下模型:
class Category(Model):
name = CharField()
parent = ForeignKeyField('self', backref='children')
如果我们想查询其父类别为 Electronics的所有类别怎么办。一种方法是执行自联接:
Parent = Category.alias()
query = (Category
.select()
.join(Parent, on=(Category.parent == Parent.id))
.where(Parent.name == 'Electronics'))
执行使用 a 的连接时,需要使用关键字参数ModelAlias指定连接条件。on在这种情况下,我们将类别与其父类别连接起来。
10.8.2 使用子查询
另一种不太常见的方法涉及使用子查询。这是另一种方式,我们可以使用子查询构造查询以获取其父类别为Electronics的所有类别:
Parent = Category.alias()
join_query = Parent.select().where(Parent.name == 'Electronics')
# Subqueries used as JOINs need to have an alias.
join_query = join_query.alias('jq')
query = (Category
.select()
.join(join_query, on=(Category.parent == join_query.c.id)))
这将生成以下 SQL 查询:
SELECT t1."id", t1."name", t1."parent_id"
FROM "category" AS t1
INNER JOIN (
SELECT t2."id"
FROM "category" AS t2
WHERE (t2."name" = ?)) AS jq ON (t1."parent_id" = "jq"."id")
要从子查询中访问id值,我们使用.c魔术查找,它将生成适当的 SQL 表达式:
Category.parent == join_query.c.id
# Becomes: (t1."parent_id" = "jq"."id")
10.9 实现多对多
Peewee 提供了一个表示多对多关系的字段,就像 Django 一样。由于用户的许多请求而添加了此功能,但我强烈反对使用它,因为它将字段的概念与联结表和隐藏连接混为一谈。提供方便的访问器只是一个讨厌的黑客攻击。
要使用 peewee正确实现多对多,因此您将自己创建中间表并通过它进行查询:
class Student(Model):
name = CharField()
class Course(Model):
name = CharField()
class StudentCourse(Model):
student = ForeignKeyField(Student)
course = ForeignKeyField(Course)
要查询,假设我们要查找参加数学课的学生:
query = (Student
.select()
.join(StudentCourse)
.join(Course)
.where(Course.name == 'math'))
for student in query:
print(student.name)
要查询给定学生注册了哪些课程:
courses = (Course
.select()
.join(StudentCourse)
.join(Student)
.where(Student.name == 'da vinci'))
for course in courses:
print(course.name)
为了有效地迭代多对多关系,即列出所有学生及其各自的课程,我们将查询直通模型 StudentCourse并预先计算学生和课程:
query = (StudentCourse
.select(StudentCourse, Student, Course)
.join(Course)
.switch(StudentCourse)
.join(Student)
.order_by(Student.name))
要打印学生及其课程的列表,您可以执行以下操作:
for student_course in query:
print(student_course.student.name, '->', student_course.course.name)
由于我们从查询的 select 子句中选择了所有字段Student,因此Course这些 外键遍历是“免费的”,我们只用 1 个查询就完成了整个迭代。
10.9.1 多对多字段
在多对多字段上ManyToManyField提供了一个类似于字段的API。对于除了最简单的多对多情况之外的所有情况,您最好使用标准的 peewee API。但是,如果您的模型非常简单并且您的查询需求不是很复杂,ManyToManyField则可能会起作用。
使用以下方法对学生和课程进行建模ManyToManyField:
from peewee import *
db = SqliteDatabase('school.db')
class BaseModel(Model):
class Meta:
database = db
class Student(BaseModel):
name = CharField()
class Course(BaseModel):
name = CharField()
students = ManyToManyField(Student, backref='courses')
StudentCourse = Course.students.get_through_model()
db.create_tables([
Student,
Course,
StudentCourse])
# Get all classes that "huey" is enrolled in:
huey = Student.get(Student.name == 'Huey')
for course in huey.courses.order_by(Course.name):
print(course.name)
# Get all students in "English 101":
engl_101 = Course.get(Course.name == 'English 101')
for student in engl_101.students:
print(student.name)
# When adding objects to a many-to-many relationship, we can pass
# in either a single model instance, a list of models, or even a
# query of models:
huey.courses.add(Course.select().where(Course.name.contains('English')))
engl_101.students.add(Student.get(Student.name == 'Mickey'))
engl_101.students.add([
Student.get(Student.name == 'Charlie'),
Student.get(Student.name == 'Zaizee')])
# The same rules apply for removing items from a many-to-many:
huey.courses.remove(Course.select().where(Course.name.startswith('CS')))
engl_101.students.remove(huey)
# Calling .clear() will remove all associated objects:
cs_150.students.clear()
注意力
在添加多对多关系之前,需要先保存被引用的对象。为了在多对多直通表中创建关系,Peewee 需要知道被引用模型的主键。
警告
强烈建议您不要尝试对包含ManyToManyField实例的模型进行子类化。
A
ManyToManyField,尽管它的名字,不是通常意义上的字段。多对多字段不是表上的列,而是涵盖了这样一个事实,即在幕后实际上有一个带有两个外键指针的单独表(通过表)。因此,当创建一个继承多对多字段的子类时,真正需要继承的是直通表。由于潜在的细微错误,Peewee
不会尝试自动继承模型并修改其外键指针。因此,多对多字段通常不适用于继承。
有关更多示例,请参见:
ManyToManyField.add()
ManyToManyField.remove()
ManyToManyField.clear()
ManyToManyField.get_through_model()
10.10 避免 N+1 问题
N+1 问题是指应用程序执行查询的情况,然后对于结果集的每一行,应用程序至少执行一个其他查询(另一种概念化方法是嵌套循环)。在许多情况下,可以通过使用 SQL 连接或子查询来避免这n 个查询。数据库本身可能会执行嵌套循环,但它通常比在应用程序代码中执行n次查询具有更高的性能,这涉及与数据库通信的延迟,并且在加入或执行时可能无法利用数据库采用的索引或其他优化一个子查询。
Peewee 提供了几个 API 来缓解N+1查询行为。回顾本文档中使用的模型User和Tweet,本节将尝试概述一些常见的N+1场景,以及 peewee 如何帮助您避免它们。
注意力
在某些情况下,N+1
个查询不会导致显着或可衡量的性能损失。这完全取决于您查询的数据、您使用的数据库以及执行查询和检索结果所涉及的延迟。与进行优化时一样,在进行优化之前和之后进行分析,以确保更改符合您的预期。
10.10.1 列出最近的推文
Twitter 时间线显示来自多个用户的推文列表。除了推文的内容外,还会显示推文作者的用户名。这里的 N+1 场景是:
- 获取最近的 10 条推文。
- 对于每条推文,选择作者(10 个查询)。
通过选择两个表并使用连接,peewee 可以在单个查询中完成此操作:
query = (Tweet
.select(Tweet, User) # Note that we are selecting both models.
.join(User) # Use an INNER join because every tweet has an author.
.order_by(Tweet.id.desc()) # Get the most recent tweets.
.limit(10))
for tweet in query:
print(tweet.user.username, '-', tweet.message)
如果没有连接,访问tweet.user.username将触发查询以解析外键tweet.user并检索关联用户。但是由于我们选择并加入了 on User,peewee 会自动为我们解析外键。
笔记
在从多个来源中选择中更详细地讨论了此技术。
10.10.2 列出用户及其所有推文
假设您要构建一个显示多个用户及其所有推文的页面。N+1 情景将是:
- 获取一些用户。
- 对于每个用户,获取他们的推文。
这种情况与前面的例子类似,但有一个重要区别:当我们选择推文时,它们只有一个关联用户,所以我们可以直接分配外键。然而,反之则不然,因为一个用户可能有任意数量的推文(或根本没有推文)。
Peewee 提供了一种在这种情况下避免O(n)查询的方法。首先获取用户,然后获取与这些用户关联的所有推文。一旦 peewee 拥有大量推文,它就会将它们分配出去,将它们与适当的用户进行匹配。此方法通常更快,但会涉及对每个选定表的查询。
10.10.3 使用预取
peewee 支持使用子查询预取相关数据。此方法需要使用特殊的 API prefetch(),. 顾名思义,预取将使用子查询为给定用户急切地加载适当的推文。这意味着我们将对 k个表进行O(k)次查询,而不是对n行进行O(n)次查询。
这是一个示例,说明我们如何获取多个用户以及他们在过去一周内创建的任何推文。
week_ago = datetime.date.today() - datetime.timedelta(days=7)
users = User.select()
tweets = (Tweet
.select()
.where(Tweet.timestamp >= week_ago))
# This will perform two queries.
users_with_tweets = prefetch(users, tweets)
for user in users_with_tweets:
print(user.username)
for tweet in user.tweets:
print(' ', tweet.message)
笔记
请注意,User查询和Tweet查询都不包含 JOIN 子句。使用prefetch()时不需要指定join。
prefetch()可用于查询任意数量的表。查看 API 文档以获取更多示例。
使用时需要考虑的一些事项prefetch():
- 预取的模型之间必须存在外键。
- LIMIT在最外层查询上的工作方式与您所期望的一样,但如果尝试限制子选择的大小,则可能难以正确实现。