Djangoのモデルで結合関連(1対多)
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対多対多
これも実現可能。例は省略
Discussion