💬

Djangoのモデルで結合関連(1対多)

2024/08/22に公開

DjangoのモデルでQuerySetの結合関連(1対多)

すぐに忘れてしまうので、モデルを利用したクエリ処理をを備忘録としてまとめておく

1対多、多対多の関係

1対多の場合です。prefetch_relateを利用すると、パフォーマンスが向上します

prefetch_relatedを利用しないで2段階で行う

prefetch_relatedを利用しないで2段階で行う場合、以下のようにします

モデルインスタンス = 1側モデル名.objects.get(id=1)
モデルインスタンス.多側モデル名_set.all()

実際の例は以下のようになります。「多側モデル名_set」で関連している多側のレコードを取得します。多側モデル名に大文字が含まれていても、全て小文字になります。末尾に「_set」をつけます

author = Author.objects.get(id=2)
author.book_set.all()

2段階でSQLを実行します

SELECT
    `author`.`id`,
    `author`.`name`,
    `author`.`age`
FROM
    `author`
WHERE
    `author`.`id` = 2

SELECT
    `book`.`id`,
    `book`.`title`,
    `book`.`price`,
    `book`.`pub_date`,
    `book`.`author_id`,
    `book`.`category_id`
FROM
    `book`
WHERE
    `book`.`author_id` = 2

SQLを2段階で実行しています。この例では、1段階目のSQLの実行結果が1レコードなので、2段階目のSQLを1階だけ実行しています。

複数件になった場合

上記の場合、getなので戻り値はモデルインスタンスです

一方、1段階目のSQLの実行結果が1レコードではない場合、(つまり、QeurySetで返る場合、)2段階のSQLを複数回実行します

authors = Author.objects.filter(age=20)
for author in authors:
    for book in author.book_set.all():
        print(a.id, a.name,a.age,b.title, b.price,b.pub_date)
-- 1段階目のSQL
SELECT
    `author`.`id`, `author`.`name`, `author`.`age`
FROM
    `author`
WHERE
    `author`.`age` = 20

-- 2段階目のSQL
SELECT
    `book`.`id`, `book`.`title`, `book`.`price`,
    `book`.`pub_date`, `book`.`author_id`, `book`.`category_id`
FROM
    `book`
WHERE
    `book`.`author_id` = 1

SELECT
    `book`.`id`, `book`.`title`, `book`.`price`,
    `book`.`pub_date`, `book`.`author_id`, `book`.`category_id`
FROM
    `book`
WHERE
    `book`.`author_id` = 4

SELECT
    `book`.`id`, `book`.`title`, `book`.`price`,
    `book`.`pub_date`, `book`.`author_id`, `book`.`category_id`
FROM
    `book`
WHERE
    `book`.`author_id` = 6

1段階目のSQLの実行結果による件数に基づいて、2段階目のSQLを複数回繰り返して実行します

prefetch_relatedを利用する

prefetch_relatedを利用して実行します。この場合も2段階でSQLを実行します

Author.objects.prefetch_related("book_set").filter(id=1)
SELECT
    `author`.`id`,
    `author`.`name`,
    `author`.`age`
FROM
    `author`
WHERE
    `author`.`id` = 1

SELECT
    `book`.`id`,
    `book`.`title`,
    `book`.`price`,
    `book`.`pub_date`,
    `book`.`author_id`,
    `book`.`category_id`
FROM
    `book`
WHERE
    `book`.`author_id` IN (1)

prefetch_relatedを利用する(複数件)

prefetch_relatedを利用します。複数件の結果が返ってきます。2段階目のSQLを複数回実行しないで、「in」を利用して、1回だけ実行します

Author.objects.prefetch_related("book_set").filter(age=20)
SELECT
    `author`.`id`, `author`.`name`, `author`.`age`
FROM
    `author`
WHERE
    `author`.`age` = 20

SELECT
    `book`.`id`, `book`.`title`, `book`.`price`,
    `book`.`pub_date`, `book`.`author_id`, `book`.`category_id`
FROM
    `book`
WHERE
    `book`.`author_id`
IN
    (1, 4, 6)

1対多の場合、prefetch_relatedの有無に関わらず、2段階でSQLを実行します

しかし、prefetch_relatedを利用する場合、2段階目のSQLを複数回実行しないで、「in」を利用して、1回だけのSQLになります

prefetch_related はリレーションごとに個別のルックアップを行い、Pythonで「結合」を行います

1側モデルのフィールドでfilter

1側モデルのフィールドでfilterを行う。Authorのageが21以下で検索します

Author.objects.prefetch_related("book_set").filter(age__lte=21)
SELECT
    `author`.`id`,
    `author`.`name`,
    `author`.`age`
FROM
    `author`
WHERE
    `author`.`age` <= 21

SELECT
    `book`.`id`,
    `book`.`title`,
    `book`.`price`,
    `book`.`pub_date`,
    `book`.`author_id`,
    `book`.`category_id`
FROM
    `book`
WHERE
    `book`.`author_id`
IN
    (1, 2, 4, 6)

これは問題ない

多側モデルのフィールドでfilter

多側モデルのフィールドでfilterを行う場合、prefetchだけではうまくいかない。prefetch_relatedの場合、2段階目のSQLは常に「all」が実行されるので、多側の関連するレコードをすべて取り出してしまう

そのため、Prefetchクラスを利用します

1側モデル名.prefetch(
    Prefech(
        "多側モデル名_set",
        queryset=多側モデル名.objects.filter(検索条件),
        to_attr="結果を保持する名称"
    )

Authorのageが21以下、Bookのpriceが1200でfilterします

from django.db.models import Prefetch

Author.objects \
    .filter(age__lte=21) \
    .prefetch_related(
        Prefetch(
            "book_set",
            queryset=Book.objects.filter(price__lte=1200),
            to_attr="books"))
SELECT
    `author`.`id`, `author`.`name`, `author`.`age`
FROM
    `author`
WHERE
    `author`.`age` <= 21

SELECT
    `book`.`id`, `book`.`title`, `book`.`price`,
    `book`.`pub_date`,`book`.`author_id`, `book`.`category_id`
FROM
    `book`
WHERE
    (
        `book`.`price` <= 1200
    AND
        `book`.`author_id`
    IN
        (1, 2, 4, 6)
    )

これで、2段階目のSQLでfilterを行うことが可能になります

1対多対1

ここまでは1対多でしたが、1対多からさらに多対1、つまり1対多対1を目指します。prefetch_relatedとPrefetch、select_relatedを併用します

Authorのageが21以下で、BookとCategoryをJOINして取得します。2段階目のSQLでJOINを行います

Author.objects \
        .filter(age__lte=21) \
        .prefetch_related(
            Prefetch(
                "book_set",
                queryset=Book.objects.select_related("category"),
                to_attr="books"))
SELECT
    `author`.`id`, `author`.`name`, `author`.`age`
FROM
    `author`
WHERE
    `author`.`age` <= 21

SELECT
    `book`.`id`,
    `book`.`title`,
    `book`.`price`,
    `book`.`pub_date`,
    `book`.`author_id`,
    `book`.`category_id`,
    `category`.`id`,
    `category`.`name`
FROM
    `book`
INNER JOIN
    `category`
ON
    (`book`.`category_id` = `category`.`id`)
WHERE
    `book`.`author_id`
IN
    (1, 2, 4, 6)

1対多対多

これも実現可能。例は省略

https://docs.djangoproject.com/ja/5.1/ref/models/querysets/#prefetch-related

Discussion