关系和联接

在本文档中,我们将介绍 Peewee 如何处理模型之间的关系。

模型定义

我们将在示例中使用以下模型定义

import datetime
from peewee import *


db = SqliteDatabase(':memory:')

class BaseModel(Model):
    class Meta:
        database = db

class User(BaseModel):
    username = TextField()

class Tweet(BaseModel):
    content = TextField()
    timestamp = DateTimeField(default=datetime.datetime.now)
    user = ForeignKeyField(User, backref='tweets')

class Favorite(BaseModel):
    user = ForeignKeyField(User, backref='favorites')
    tweet = ForeignKeyField(Tweet, backref='favorites')

Peewee 使用 ForeignKeyField 定义模型之间的外键关系。每个外键字段都有一个隐含的反向引用,该引用公开为使用提供的 backref 属性进行预过滤的 Select 查询。

创建测试数据

为了遵循示例,让我们使用一些测试数据填充此数据库

def populate_test_data():
    db.create_tables([User, Tweet, Favorite])

    data = (
        ('huey', ('meow', 'hiss', 'purr')),
        ('mickey', ('woof', 'whine')),
        ('zaizee', ()))
    for username, tweets in data:
        user = User.create(username=username)
        for tweet in tweets:
            Tweet.create(user=user, content=tweet)

    # Populate a few favorites for our users, such that:
    favorite_data = (
        ('huey', ['whine']),
        ('mickey', ['purr']),
        ('zaizee', ['meow', 'purr']))
    for username, favorites in favorite_data:
        user = User.get(User.username == username)
        for content in favorites:
            tweet = Tweet.get(Tweet.content == content)
            Favorite.create(user=user, tweet=tweet)

这给了我们以下内容

用户

推文

收藏者

huey

meow

zaizee

huey

hiss

huey

purr

mickey、zaizee

mickey

woof

mickey

whine

huey

注意

在以下示例中,我们将执行许多查询。如果你不确定执行了多少查询,则可以添加以下代码,它会将所有查询记录到控制台

import logging
logger = logging.getLogger('peewee')
logger.addHandler(logging.StreamHandler())
logger.setLevel(logging.DEBUG)

注意

在 SQLite 中,外键默认情况下未启用。大多数内容,包括 Peewee 外键 API,都能正常工作,但即使你在 ForeignKeyField 中明确指定 on_delete,也会忽略 ON DELETE 行为。结合默认 AutoField 行为(已删除记录 ID 可以重复使用),这可能会导致难以察觉的错误。为避免出现问题,我建议你在使用 SQLite 时启用外键约束,方法是在实例化 SqliteDatabase 时设置 pragmas={'foreign_keys': 1}

# Ensure foreign-key constraints are enforced.
db = SqliteDatabase('my_app.db', pragmas={'foreign_keys': 1})

执行简单联接

作为学习如何使用 Peewee 执行联接的练习,我们编写一个查询以打印出“huey”的所有推文。为此,我们将从 Tweet 模型中进行选择,并联接到 User 模型,以便我们随后可以对 User.username 字段进行筛选

>>> query = Tweet.select().join(User).where(User.username == 'huey')
>>> for tweet in query:
...     print(tweet.content)
...
meow
hiss
purr

注意

我们不必明确指定联接谓词(“ON”子句),因为 Peewee 从模型中推断出,当我们从 Tweet 联接到 User 时,我们正在联接到 Tweet.user 外键。

以下代码是等效的,但更明确

query = (Tweet
         .select()
         .join(User, on=(Tweet.user == User.id))
         .where(User.username == 'huey'))

如果我们已经拥有“huey”的 User 对象的引用,我们可以使用 User.tweets 反向引用来列出 huey 的所有推文

>>> huey = User.get(User.username == 'huey')
>>> for tweet in huey.tweets:
...     print(tweet.content)
...
meow
hiss
purr

仔细观察 huey.tweets,我们可以看到它只是一个简单的预先筛选的 SELECT 查询

>>> huey.tweets
<peewee.ModelSelect at 0x7f0483931fd0>

>>> huey.tweets.sql()
('SELECT "t1"."id", "t1"."content", "t1"."timestamp", "t1"."user_id"
  FROM "tweet" AS "t1" WHERE ("t1"."user_id" = ?)', [1])

联接多个表

让我们通过查询用户列表并获取他们创作的推文被收藏的次数来再次查看联接。这需要我们进行两次联接:从用户到推文,从推文到收藏。我们将添加额外的要求,即应包括尚未创建任何推文的用户,以及推文尚未被收藏的用户。以 SQL 表达的查询为

SELECT user.username, COUNT(favorite.id)
FROM user
LEFT OUTER JOIN tweet ON tweet.user_id = user.id
LEFT OUTER JOIN favorite ON favorite.tweet_id = tweet.id
GROUP BY user.username

注意

在上述查询中,两个联接都是 LEFT OUTER,因为用户可能没有任何推文,或者如果他们有推文,则可能没有一个被收藏。

Peewee 有一个联接上下文的概念,这意味着每当我们调用 join() 方法时,我们隐式地联接到先前联接的模型(或者如果这是第一次调用,则我们从中进行选择的模型)。由于我们直接从用户联接到推文,然后从推文联接到收藏,因此我们可以简单地编写

query = (User
         .select(User.username, fn.COUNT(Favorite.id).alias('count'))
         .join(Tweet, JOIN.LEFT_OUTER)  # Joins user -> tweet.
         .join(Favorite, JOIN.LEFT_OUTER)  # Joins tweet -> favorite.
         .group_by(User.username))

迭代结果

>>> for user in query:
...     print(user.username, user.count)
...
huey 3
mickey 1
zaizee 0

对于涉及多个联接和切换联接上下文的更复杂的示例,让我们查找 Huey 的所有推文以及它们被收藏的次数。为此,我们需要执行两个联接,并且我们还将使用聚合函数来计算收藏计数。

以下是我们在 SQL 中编写此查询的方式

SELECT tweet.content, COUNT(favorite.id)
FROM tweet
INNER JOIN user ON tweet.user_id = user.id
LEFT OUTER JOIN favorite ON favorite.tweet_id = tweet.id
WHERE user.username = 'huey'
GROUP BY tweet.content;

注意

我们从推文到收藏使用 LEFT OUTER 联接,因为推文可能没有任何收藏,但我们仍然希望在结果集中显示其内容(以及零计数)。

使用 Peewee,生成的 Python 代码看起来与我们在 SQL 中编写的内容非常相似

query = (Tweet
         .select(Tweet.content, fn.COUNT(Favorite.id).alias('count'))
         .join(User)  # Join from tweet -> user.
         .switch(Tweet)  # Move "join context" back to tweet.
         .join(Favorite, JOIN.LEFT_OUTER)  # Join from tweet -> favorite.
         .where(User.username == 'huey')
         .group_by(Tweet.content))

请注意调用 switch() - 指示 Peewee 将联接上下文切换回 Tweet。如果我们省略显式调用 switch,Peewee 会使用 User(我们联接的最后一个模型)作为联接上下文,并使用 Favorite.user 外键从 User 构建到 Favorite 的联接,这会给我们错误的结果。

如果我们想省略联接上下文切换,我们可以使用 join_from() 方法。以下查询等效于上一个查询

query = (Tweet
         .select(Tweet.content, fn.COUNT(Favorite.id).alias('count'))
         .join_from(Tweet, User)  # Join tweet -> user.
         .join_from(Tweet, Favorite, JOIN.LEFT_OUTER)  # Join tweet -> favorite.
         .where(User.username == 'huey')
         .group_by(Tweet.content))

我们可以迭代上述查询的结果以打印推文内容和收藏计数

>>> for tweet in query:
...     print('%s favorited %d times' % (tweet.content, tweet.count))
...
meow favorited 1 times
hiss favorited 0 times
purr favorited 2 times

从多个来源进行选择

如果我们希望列出数据库中的所有推文以及其作者的用户名,你可以尝试编写以下内容

>>> for tweet in Tweet.select():
...     print(tweet.user.username, '->', tweet.content)
...
huey -> meow
huey -> hiss
huey -> purr
mickey -> woof
mickey -> whine

上述循环存在一个大问题:它为每条推文执行一个附加查询以查找 tweet.user 外键。对于我们的小型表格,性能损失并不明显,但我们会发现随着行数的增加,延迟会增大。

如果你熟悉 SQL,你可能会记得可以从多张表中进行 SELECT,这使我们能够在一次查询中获取推文内容用户名

SELECT tweet.content, user.username
FROM tweet
INNER JOIN user ON tweet.user_id = user.id;

Peewee 使这变得非常容易。事实上,我们只需要稍微修改一下查询。我们告诉 Peewee 我们希望选择 Tweet.content 以及 User.username 字段,然后我们从推文到用户包含一个联接。为了让它更明显地做正确的事情,我们可以要求 Peewee 以字典的形式返回行。

>>> for row in Tweet.select(Tweet.content, User.username).join(User).dicts():
...     print(row)
...
{'content': 'meow', 'username': 'huey'}
{'content': 'hiss', 'username': 'huey'}
{'content': 'purr', 'username': 'huey'}
{'content': 'woof', 'username': 'mickey'}
{'content': 'whine', 'username': 'mickey'}

现在,我们将省略对 “.dicts()” 的调用,并将行作为 Tweet 对象返回。请注意,Peewee 将 username 值分配给 tweet.user.username - 而不是 tweet.username!因为从推文到用户有一个外键,并且我们从这两个模型中选择了字段,Peewee 将为我们重建模型图

>>> for tweet in Tweet.select(Tweet.content, User.username).join(User):
...     print(tweet.user.username, '->', tweet.content)
...
huey -> meow
huey -> hiss
huey -> purr
mickey -> woof
mickey -> whine

如果我们希望,可以通过在 join() 方法中指定 attr 来控制 Peewee 在上述查询中放置联接的 User 实例的位置

>>> query = Tweet.select(Tweet.content, User.username).join(User, attr='author')
>>> for tweet in query:
...     print(tweet.author.username, '->', tweet.content)
...
huey -> meow
huey -> hiss
huey -> purr
mickey -> woof
mickey -> whine

相反,如果我们只是希望我们选择所有属性作为 Tweet 实例的属性,我们可以在查询末尾添加对 objects() 的调用(类似于我们调用 dicts() 的方式)

>>> for tweet in query.objects():
...     print(tweet.username, '->', tweet.content)
...
huey -> meow
(etc)

更复杂的示例

作为一个更复杂的示例,在此查询中,我们将编写一个单一查询,以选择所有收藏夹、创建收藏夹的用户、被收藏的推文以及该推文的作者。

在 SQL 中,我们将编写

SELECT owner.username, tweet.content, author.username AS author
FROM favorite
INNER JOIN user AS owner ON (favorite.user_id = owner.id)
INNER JOIN tweet ON (favorite.tweet_id = tweet.id)
INNER JOIN user AS author ON (tweet.user_id = author.id);

请注意,我们从用户表中选择了两次 - 一次是在创建收藏夹的用户上下文中,另一次是在推文作者上下文中。

使用 Peewee,我们使用 Model.alias() 为模型类创建别名,以便在单个查询中引用两次

Owner = User.alias()
query = (Favorite
         .select(Favorite, Tweet.content, User.username, Owner.username)
         .join(Owner)  # Join favorite -> user (owner of favorite).
         .switch(Favorite)
         .join(Tweet)  # Join favorite -> tweet
         .join(User))   # Join tweet -> user

我们可以迭代结果并以下列方式访问联接值。请注意 Peewee 如何解析我们选择的各种模型中的字段并重建模型图

>>> for fav in query:
...     print(fav.user.username, 'liked', fav.tweet.content, 'by', fav.tweet.user.username)
...
huey liked whine by mickey
mickey liked purr by huey
zaizee liked meow by huey
zaizee liked purr by huey

子查询

Peewee 允许您联接任何表状对象,包括子查询或公用表表达式 (CTE)。为了演示如何联接子查询,让我们查询所有用户及其最新推文。

以下是 SQL

SELECT tweet.*, user.*
FROM tweet
INNER JOIN (
    SELECT latest.user_id, MAX(latest.timestamp) AS max_ts
    FROM tweet AS latest
    GROUP BY latest.user_id) AS latest_query
ON ((tweet.user_id = latest_query.user_id) AND (tweet.timestamp = latest_query.max_ts))
INNER JOIN user ON (tweet.user_id = user.id)

我们将通过创建一个子查询来执行此操作,该子查询选择每个用户及其最新推文的 timestamp。然后,我们可以在外部查询中查询推文表,并联接子查询中的用户和 timestamp 组合。

# Define our subquery first. We'll use an alias of the Tweet model, since
# we will be querying from the Tweet model directly in the outer query.
Latest = Tweet.alias()
latest_query = (Latest
                .select(Latest.user, fn.MAX(Latest.timestamp).alias('max_ts'))
                .group_by(Latest.user)
                .alias('latest_query'))

# Our join predicate will ensure that we match tweets based on their
# timestamp *and* user_id.
predicate = ((Tweet.user == latest_query.c.user_id) &
             (Tweet.timestamp == latest_query.c.max_ts))

# We put it all together, querying from tweet and joining on the subquery
# using the above predicate.
query = (Tweet
         .select(Tweet, User)  # Select all columns from tweet and user.
         .join(latest_query, on=predicate)  # Join tweet -> subquery.
         .join_from(Tweet, User))  # Join from tweet -> user.

迭代查询,我们可以看到每个用户及其最新推文。

>>> for tweet in query:
...     print(tweet.user.username, '->', tweet.content)
...
huey -> purr
mickey -> whine

在本节中,您可能在用于创建查询的代码中看到了一些您以前可能没有见过的内容

  • 我们使用 join_from() 明确指定联接上下文。我们编写了 .join_from(Tweet, User),它等效于 .switch(Tweet).join(User)

  • 我们使用神奇的 .c 属性引用子查询中的列,例如 latest_query.c.max_ts.c 属性用于动态创建列引用。

  • 我们没有将各个字段传递给 Tweet.select(),而是传递了 TweetUser 模型。这是选择给定模型上所有字段的简写。

公用表表达式

在上一节中,我们联接了一个子查询,但我们也可以轻松地使用 公用表表达式 (CTE)。我们将重复与之前相同的查询,列出用户及其最新推文,但这一次我们将使用 CTE 来执行此操作。

以下是 SQL

WITH latest AS (
    SELECT user_id, MAX(timestamp) AS max_ts
    FROM tweet
    GROUP BY user_id)
SELECT tweet.*, user.*
FROM tweet
INNER JOIN latest
    ON ((latest.user_id = tweet.user_id) AND (latest.max_ts = tweet.timestamp))
INNER JOIN user
    ON (tweet.user_id = user.id)

此示例与带有子查询的先前示例非常相似

# Define our CTE first. We'll use an alias of the Tweet model, since
# we will be querying from the Tweet model directly in the main query.
Latest = Tweet.alias()
cte = (Latest
       .select(Latest.user, fn.MAX(Latest.timestamp).alias('max_ts'))
       .group_by(Latest.user)
       .cte('latest'))

# Our join predicate will ensure that we match tweets based on their
# timestamp *and* user_id.
predicate = ((Tweet.user == cte.c.user_id) &
             (Tweet.timestamp == cte.c.max_ts))

# We put it all together, querying from tweet and joining on the CTE
# using the above predicate.
query = (Tweet
         .select(Tweet, User)  # Select all columns from tweet and user.
         .join(cte, on=predicate)  # Join tweet -> CTE.
         .join_from(Tweet, User)  # Join from tweet -> user.
         .with_cte(cte))

我们可以迭代结果集,其中包含每个用户的最新推文

>>> for tweet in query:
...     print(tweet.user.username, '->', tweet.content)
...
huey -> purr
mickey -> whine

注意

有关使用 CTE 的更多信息,包括有关编写递归 CTE 的信息,请参阅“查询”文档的 公用表表达式 部分。

到同一模型的多个外键

当有多个外键指向同一模型时,最好明确指定您要联接的字段。

回到示例应用程序的模型,考虑关系模型,它用于表示一个用户关注另一个用户。以下是模型定义

class Relationship(BaseModel):
    from_user = ForeignKeyField(User, backref='relationships')
    to_user = ForeignKeyField(User, backref='related_to')

    class Meta:
        indexes = (
            # Specify a unique multi-column index on from/to-user.
            (('from_user', 'to_user'), True),
        )

由于有两个外键指向用户,我们应该始终指定在联接中使用哪个字段。

例如,要确定我关注了哪些用户,我会写

(User
 .select()
 .join(Relationship, on=Relationship.to_user)
 .where(Relationship.from_user == charlie))

另一方面,如果我想确定哪些用户关注了我,我将改为联接from_user列并过滤关系的to_user

(User
 .select()
 .join(Relationship, on=Relationship.from_user)
 .where(Relationship.to_user == charlie))

联接任意字段

如果两个表之间不存在外键,你仍然可以执行联接,但你必须手动指定联接谓词。

在以下示例中,用户活动日志之间没有明确的外键,但在活动日志.object_id字段和用户.id之间有一个隐含的关系。我们不会联接特定字段,而是使用表达式联接。

user_log = (User
            .select(User, ActivityLog)
            .join(ActivityLog, on=(User.id == ActivityLog.object_id), attr='log')
            .where(
                (ActivityLog.activity_type == 'user_activity') &
                (User.username == 'charlie')))

for user in user_log:
    print(user.username, user.log.description)

#### Print something like ####
charlie logged in
charlie posted a tweet
charlie retweeted
charlie posted a tweet
charlie logged out

注意

回想一下,我们可以通过在join()方法中指定attr参数来控制 Peewee 将联接实例分配给哪个属性。在前面的示例中,我们使用了以下联接

join(ActivityLog, on=(User.id == ActivityLog.object_id), attr='log')

然后在迭代查询时,我们能够直接访问联接的活动日志,而无需进行额外的查询

for user in user_log:
    print(user.username, user.log.description)

自联接

Peewee 支持构建包含自联接的查询。

使用模型别名

要在同一模型(表)上联接两次,有必要创建一个模型别名来表示查询中表的第二个实例。考虑以下模型

class Category(Model):
    name = CharField()
    parent = ForeignKeyField('self', backref='children')

如果我们想查询所有父类别为电子产品的类别。一种方法是执行自联接

Parent = Category.alias()
query = (Category
         .select()
         .join(Parent, on=(Category.parent == Parent.id))
         .where(Parent.name == 'Electronics'))

在执行使用模型别名的联接时,有必要使用on关键字参数指定联接条件。在这种情况下,我们将类别与其父类别联接起来。

使用子查询

另一种不太常见的方法涉及使用子查询。以下是我们使用子查询构建查询以获取所有父类别为电子产品的类别的另一种方法

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")

实现多对多

Peewee 提供了一个用于表示多对多关系的字段,很像 Django 所做的那样。此功能的添加是由于许多用户的请求,但我强烈反对使用它,因为它将字段的概念与连接表和隐藏连接混为一谈。它只是一个提供方便访问器的讨厌的 hack。

要使用 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 子句中从 StudentCourse 中选择了所有字段,因此这些外键遍历是“免费”的,我们只需 1 个查询就完成了整个迭代。

ManyToManyField

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 实例的模型进行子类化。

ManyToManyField,尽管其名称,但它不是通常意义上的字段。多对多字段不是表上的列,而是涵盖了幕后实际上有一个带有两个外键指针(通过表)的单独表的事实。

因此,当创建继承多对多字段的子类时,实际上需要继承的是通过表。由于存在细微错误的可能性,Peewee 不会尝试自动对通过模型进行子类化并修改其外键指针。因此,多对多字段通常不适用于继承。

有关更多示例,请参阅

避免 N+1 问题

N+1 问题是指应用程序执行查询,然后针对结果集的每一行,应用程序至少执行一次其他查询(另一种概念化方式是将其视为嵌套循环)。在许多情况下,可以通过使用 SQL 联接或子查询来避免这些n个查询。数据库本身可能会执行嵌套循环,但通常比在应用程序代码中执行n个查询的性能更高,其中涉及与数据库通信的延迟,并且可能无法利用数据库在联接或执行子查询时采用的索引或其他优化。

Peewee 提供了多个 API 来缓解N+1查询行为。回顾本文档中使用的模型UserTweet,本节将尝试概述一些常见的N+1场景,以及 peewee 如何帮助你避免它们。

注意

在某些情况下,N+1 查询不会导致显着或可衡量的性能下降。这完全取决于你查询的数据、你使用的数据库以及执行查询和检索结果所涉及的延迟。与进行优化时一样,始终在优化前后进行分析,以确保更改符合你的预期。

列出最近的推文

Twitter 时间线显示来自多个用户的推文列表。除了推文的内容外,还显示推文作者的用户名。此处的 N+1 场景为

  1. 获取 10 条最近的推文。

  2. 对于每条推文,选择作者(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 并检索关联的用户。但由于我们已经选择并联接了 User,peewee 将自动为我们解析外键。

注意

此技术在 从多个来源选择 中进行了更详细的讨论。

列出用户及其所有推文

假设你想要构建一个显示多个用户及其所有推文的页面。N+1 场景为

  1. 获取一些用户。

  2. 对于每个用户,获取他们的推文。

此情况与前面的示例类似,但有一个重要的区别:当我们选择推文时,它们只有一个关联的用户,因此我们可以直接分配外键。然而,反过来却不是这样,因为一个用户可能有多条推文(或根本没有)。

Peewee 提供了一种方法来避免这种情况下的O(n)查询。首先获取用户,然后获取与这些用户关联的所有推文。一旦 peewee 获得了大量的推文,它将分配它们,并将其与适当的用户匹配。此方法通常更快,但会涉及对每个要选择的表进行查询。

使用预取

peewee 支持使用子查询预取相关数据。此方法需要使用特殊 API prefetch()。顾名思义,预取将使用子查询急切加载给定用户的相应推文。这意味着对于 n 行,我们执行 O(k) 查询(针对 k 个表),而不是 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() 时,无需指定联接。

prefetch() 可用于查询任意数量的表。查看 API 文档以获取更多示例。

使用 prefetch() 时需要考虑的一些事项

  • 预取的模型之间必须存在外键。

  • LIMIT 在最外层查询中按预期工作,但如果尝试限制子查询的大小,则可能难以正确实现。* 当 LIMIT 不受支持时,可以使用参数 prefetch_type

    使用默认查询构造(例如,使用 MySQL)。