リレーションシップと結合

このドキュメントでは、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 を含むほとんどの機能は問題なく動作しますが、ForeignKeyFieldon_delete を明示的に指定した場合でも、ON DELETE の動作は無視されます。AutoField のデフォルトの動作(削除されたレコードの ID が再利用される)と組み合わせて使用すると、微妙なバグが発生する可能性があります。問題を回避するために、SqliteDatabase をインスタンス化するときに pragmas={'foreign_keys': 1} を設定して、SQLite で外部キー制約を有効にすることをお勧めします。

# 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

注意

Tweet から User への結合時に Tweet.user 外部キーを使用して結合していることがモデルから推測されるため、結合述語(「ON」句)を明示的に指定する必要はありませんでした。

次のコードは同等ですが、より明示的です。

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

複数のテーブルの結合

ユーザーのリストを取得し、お気に入りされたツイートの作成数を取得することで、結合をもう一度見てみましょう。これには、ユーザーからツイートへの結合、ツイートからお気に入りへの結合の 2 回の結合が必要です。さらに、ツイートを作成していないユーザーや、ツイートがお気に入りされていないユーザーも含まれるようにします。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 によるすべてのツイートとそのお気に入り回数を調べましょう。これを行うには、2 つの結合を実行し、集計関数を使用してお気に入りの回数を計算します。

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;

注意

ツイートにお気に入りがない場合でも、そのコンテンツ(0 のカウント付き)を結果セットに表示したいので、ツイートからお気に入りへの 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 では、これは非常に簡単です。実際、クエリを少し変更するだけです。Tweet.contentUser.username フィールドを選択することを Peewee に指示し、次にツイートからユーザーへの結合を含めます。それが正しく動作していることをより明確にするために、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)

より複雑な例

より複雑な例として、このクエリでは、すべてのいいね!とそのいいね!を作成したユーザー、いいね!されたツイート、およびそのツイートの投稿者を1つのクエリで選択します。

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

いいね!を作成したユーザーという文脈と、ツイートの投稿者として、2回ユーザーテーブルから選択していることに注意してください。

Peeweeでは、Model.alias()を使用してモデルクラスにエイリアスを付け、1つのクエリで2回参照できるようにします。

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)

これは、各ユーザーとその最新のツイートのタイムスタンプを選択するサブクエリを作成することで実現します。次に、外部クエリでtweetsテーブルをクエリし、サブクエリのユーザーとタイムスタンプの組み合わせに結合できます。

# 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の使用方法の詳細については、「クエリ」ドキュメントの共通テーブル式セクションを参照してください。

同じモデルへの複数の外部キー

同じモデルへの外部キーが複数ある場合は、結合に使用するフィールドを明示的に指定することをお勧めします。

例アプリのモデルを振り返り、あるユーザーが別のユーザーをフォローする場合を示すRelationshipモデルを考えてみましょう。これがモデルの定義です。

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への外部キーが2つあるため、結合に使用するフィールドを常に指定する必要があります。

たとえば、自分がフォローしているユーザーを特定するには、次のように記述します。

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

任意のフィールドへの結合

2つのテーブル間に外部キーが存在しない場合でも、結合を実行できますが、結合述語を手動で指定する必要があります。

次の例では、UserActivityLogの間に明示的な外部キーはありませんが、ActivityLog.object_idフィールドとUser.idの間に暗黙的な関係があります。特定のFieldに結合するのではなく、Expressionを使用して結合します。

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を使用しました。

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

次に、クエリを反復処理すると、追加のクエリを実行することなく、結合されたActivityLogに直接アクセスできました。

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

自己結合

Peeweeは、自己結合を含むクエリの構築をサポートしています。

モデルエイリアスの使用

同じモデル(テーブル)に2回結合するには、クエリ内のテーブルの2番目のインスタンスを表すモデルエイリアスを作成する必要があります。次のモデルを考えてみましょう。

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

親カテゴリが「Electronics」であるすべてのカテゴリをクエリしたい場合はどうでしょうか。1つの方法は、自己結合を実行することです。

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

ModelAliasを使用する結合を実行する場合は、onキーワード引数を使用して結合条件を指定する必要があります。この場合、カテゴリとその親カテゴリを結合しています。

サブクエリの使用

もう1つのあまり一般的ではないアプローチは、サブクエリを使用することです。親カテゴリが「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値にアクセスするには、適切なSQL式を生成するマジック.cルックアップを使用します。

Category.parent == join_query.c.id
# Becomes: (t1."parent_id" = "jq"."id")

多対多の実装

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)

多対多の関係を効率的に反復処理する(つまり、すべての生徒とそのコースを一覧表示する)には、throughモデルStudentCourseをクエリし、StudentとCourseを事前計算します。

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)

クエリで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()

注意

多対多の関係を追加する前に、参照されるオブジェクトを最初に保存する必要があります。多対多のthroughテーブルで関係を作成するには、Peeweeは参照されるモデルの主キーを知る必要があります。

警告

ManyToManyFieldインスタンスを含むモデルをサブクラス化しようとしないでください。

ManyToManyFieldは、その名前に反して、通常の意味でのフィールドではありません。テーブル上の列である代わりに、多対多フィールドは、実際には2つの外部キーポインター(throughテーブル)を持つ別のテーブルが舞台裏にあるという事実をカバーしています。

したがって、多対多フィールドを継承するサブクラスが作成されると、実際に継承する必要があるのはthroughテーブルです。微妙なバグが発生する可能性があるため、Peeweeはthroughモデルを自動的にサブクラス化して外部キーポインターを変更しようとしますません。その結果、多対多フィールドは通常、継承では機能しません。

詳細な例については、以下を参照してください。

N+1問題の回避

N+1問題とは、アプリケーションがクエリを実行し、その結果セットの各行に対して、アプリケーションが少なくとも1つの他のクエリを実行する状況を指します(別の言い方をすれば、ネストされたループです)。多くの場合、これらのn個のクエリは、SQLのJOINまたはサブクエリを使用して回避できます。データベース自体がネストされたループを実行する場合がありますが、アプリケーションコードでn個のクエリを実行するよりも通常はパフォーマンスが優れています。アプリケーションコードでのn個のクエリの実行には、データベースとの通信におけるレイテンシが含まれ、データベースがJOINまたはサブクエリを実行する際に使用するインデックスやその他の最適化を利用できない可能性があります。

Peeweeは、N+1クエリの動作を軽減するためのいくつかのAPIを提供します。このドキュメント全体で使用されているモデル、UserTweetを改めて考えると、このセクションでは一般的なN+1シナリオとその回避方法について説明します。

注意

場合によっては、N+1クエリは、重大な、または測定可能なパフォーマンス低下をもたらしません。それは、クエリ対象のデータ、使用しているデータベース、クエリの実行と結果の取得にかかるレイテンシにすべて依存します。最適化を行う際には常に、変更が期待通りに機能することを確認するために、変更の前後をプロファイリングしてください。

最近のツイートのリスト

Twitterのタイムラインには、複数のユーザーからのツイートのリストが表示されます。ツイートの内容に加えて、ツイートの投稿者のユーザー名も表示されます。ここでN+1シナリオは次のようになります。

  1. 最新の10件のツイートを取得します。

  2. 各ツイートについて、投稿者を選択します(10個のクエリ)。

両方のテーブルを選択し、JOINを使用することで、Peeweeはこれを1つのクエリで実行できるようにします。

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)

JOINを使用せずにtweet.user.usernameにアクセスすると、外部キーtweet.userを解決し、関連付けられたユーザーを取得するためのクエリがトリガーされます。しかし、Userを選択してJOINしたので、Peeweeは自動的に外部キーを解決します。

注意

この手法については、複数のソースからの選択で詳しく説明しています。

ユーザーとすべてのツイートのリスト

いくつかのユーザーとそれらのユーザーのすべてのツイートを表示するページを作成したいとしましょう。N+1シナリオは次のようになります。

  1. いくつかのユーザーを取得します。

  2. 各ユーザーについて、そのユーザーのツイートを取得します。

この状況は前の例と似ていますが、1つの重要な違いがあります。ツイートを選択したとき、それらは1つの関連付けられたユーザーしか持たないため、外部キーを直接割り当てることができました。しかし、逆は当てはまりません。1人のユーザーは、任意の数のツイート(または全くない場合もあります)。

Peeweeはこの状況でO(n)クエリを回避する方法を提供します。最初にユーザーを取得し、次にそれらのユーザーに関連付けられたすべてのツイートを取得します。Peeweeがツイートの大規模なリストを取得したら、それらを割り当てて、適切なユーザーと一致させます。この方法は通常高速ですが、選択されている各テーブルに対してクエリが必要になります。

prefetchの使用

Peeweeは、サブクエリを使用して関連データのプリフェッチをサポートしています。この方法は、特別なAPIであるprefetch()を使用する必要があります。名前が示すように、prefetchは、サブクエリを使用して指定されたユーザーの適切なツイートを事前に読み込みます。つまり、n行に対してO(n)クエリを実行する代わりに、k個のテーブルに対してO(k)クエリを実行します。

過去1週間以内に作成したいくつかのユーザーとツイートを取得する例を次に示します。

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は最外部のクエリで期待通りに動作しますが、サブセレクトのサイズを制限しようとする場合、正しく実装するのが難しい場合があります。* パラメータprefetch_typeは、LIMITがサポートされていない場合に使用できます。

    デフォルトのクエリ構築(例:MySQL)を使用する場合。