「DjangoのORMに触れてみよう」親テーブルから子テーブルに逆参照をする編

この記事は以下のターゲットを対象としています。

★5 Django の開発経験が 3 年以上。
★4 Django の開発経験が 1 年以上。
★3 WEB サイト開発経験あり。これから Django を学習します。
★2 Python 初級者。簡単なプログラムコードが書けます。
★1 プログラミング未経験。

こんにちは。グローバルウェイの清家です。

前回の記事ではORMを使って、子テーブルから外部キーで親テーブルを参照し、実際にどのようなクエリが発行されるか、無駄にクエリが発行されないようにするにはどうするか(N+1問題)について取り上げました。

今回は親テーブルから子テーブルを参照する、逆参照について取り上げます。

select_relatedによる参照とは違い、prefetch_related、Prefetchを使いますが、少しプログラムが複雑になります。どういったケースで使用し、どのようなクエリが発行されるか確認しながら進めます。

目次

〔related_nameによる逆参照〕

以下に3つのテーブルを用意します。

table_aからtable_cまで外部キーで接続し、親・子・孫の関係が成り立つようにします。

table_a

idname
1A-1

table_b

idnametable_a
1B-11
2B-21
3B-31

table_c

idnametable_b
1C-11
2C-21
3C-31

まず、models.pyのrelated_nameのみでORMを実行します。

それぞれのテーブルにアクセスするタイミングでQuerySetが評価され、多くのクエリが発行されていることがわかります。

〔# Python models.py〕 class TableA(BaseModel):
    name = models.TextField(db_comment=”名前”)

    class Meta:
        db_table = “table_a”
        db_table_comment = “TableA”


class TableB(BaseModel):
    name = models.TextField(db_comment=”名前”)
    table_a = models.ForeignKey(
        “TableA”,
        models.DO_NOTHING,
        db_column=”table_a”,
        related_name=”table_b_table_a”,
        db_comment=”TableA”,
    )

    class Meta:
        db_table = “table_b”
        db_table_comment = “TableB”


class TableC(BaseModel):
    name = models.TextField(db_comment=”名前”)
    table_b = models.ForeignKey(
        “TableB”,
        models.DO_NOTHING,
        db_column=”table_b”,
        related_name=”table_c_table_b”,
        db_comment=”TableB”,
    )

    class Meta:
        db_table = “table_c”
        db_table_comment = “TableC”  
〔# Python〕 table_a = TableA.objects.filter(id=1).first()
for table_b in table_a.table_b_table_a.all():
    print(table_b.name)
    for table_c in table_b.table_c_table_b.all():
        print(table_c.name)  
〔実行結果〕
SELECT “table_a”.”id”, “table_a”.”name” FROM “table_a” WHERE “table_a”.”id” = 1 ORDER BY “table_a”.”id” ASC LIMIT 1; args=(1,); alias=default
SELECT “table_b”.”id”, “table_b”.”name”, “table_b”.”table_a” FROM “table_b” WHERE “table_b”.”table_a” = 1; args=(1,); alias=default
B-1
SELECT “table_c”.”id”, “table_c”.”name”, “table_c”.”table_b” FROM “table_c” WHERE “table_c”.”table_b” = 1; args=(1,); alias=default
C-1
C-2
C-3
B-2
SELECT “table_c”.”id”, “table_c”.”name”, “table_c”.”table_b” FROM “table_c” WHERE “table_c”.”table_b” = 2; args=(2,); alias=default
B-3
SELECT “table_c”.”id”, “table_c”.”name”, “table_c”.”table_b” FROM “table_c” WHERE “table_c”.”table_b” = 3; args=(3,); alias=default

〔prefetch_relatedを使った逆参照〕

prefetch_relatedを使って実行した場合、table_cへのアクセスにおいて、IN句を使うことでまとめてレコードを取得できます。

related_nameを使った場合と比べて、クエリの発行回数を減らすことが可能になります。

〔# Python〕 table_a = (
    TableA.objects.filter(id=1)
    .prefetch_related(“table_b_table_a”)
    .prefetch_related(“table_b_table_a__table_c_table_b”)
    .first()
)
for table_b in table_a.table_b_table_a.all():
    print(table_b.name)
    for table_c in table_b.table_c_table_b.all():
        print(table_c.name)  
〔実行結果〕
SELECT “table_a”.”id”, “table_a”.”name” FROM “table_a” WHERE “table_a”.”id” = 1 ORDER BY “table_a”.”id” ASC LIMIT 1; args=(1,); alias=default
SELECT “table_b”.”id”, “table_b”.”name”, “table_b”.”table_a” FROM “table_b” WHERE “table_b”.”table_a” IN (1); args=(1,); alias=default
SELECT “table_c”.”id”, “table_c”.”name”, “table_c”.”table_b” FROM “table_c” WHERE “table_c”.”table_b” IN (1, 2, 3); args=(1, 2, 3); alias=default
B-1
C-1
C-2
C-3

〔Prefetchを使った逆参照〕

もう少し掘り下げます。

逆参照先のレコードに対して、Prefetchを使うことで絞り込みをしたり、並び替えをするなど詳細な条件を付け加えることが可能です。

〔# Python〕 table_a = (
    TableA.objects.filter(id=1)
    .prefetch_related(
        Prefetch(
            “table_b_table_a”,
            queryset=TableB.objects.order_by(“id”).prefetch_related(
                Prefetch(
                    “table_c_table_b”,
                    queryset=TableC.objects.order_by(“id”),
                    to_attr=”c”,
                )
            ),
            to_attr=”b”,
        )
    )
    .first()
) for table_b in table_a.b:
    print(table_b.name)
    for table_c in table_b.c:
        print(table_c.name)  
〔実行結果〕
SELECT “table_a”.”id”, “table_a”.”name” FROM “table_a” WHERE “table_a”.”id” = 1 ORDER BY “table_a”.”id” ASC LIMIT 1; args=(1,); alias=default
SELECT “table_b”.”id”, “table_b”.”name”, “table_b”.”table_a” FROM “table_b” WHERE “table_b”.”table_a” IN (1) ORDER BY “table_b”.”id” ASC; args=(1,); alias=default
SELECT “table_c”.”id”, “table_c”.”name”, “table_c”.”table_b” FROM “table_c” WHERE “table_c”.”table_b” IN (1, 2, 3) ORDER BY “table_c”.”id” ASC; args=(1, 2, 3); alias=default
B-1
C-1
C-2
C-3
B-2
B-3

〔まとめ〕

親テーブルから子テーブルを参照する、逆参照について効率のいいプログラムの書き方を紹介しました。select_relatedによる子テーブルから親テーブルの参照に比べ、結合(join)が使用されない分クエリの発行回数は多くなってしまいますが、それでも便利な機能です。

繰り返しになりますが、ORMを使う場合はSQLの確認を怠らないようにしましょう。

次回もまたDjangoのノウハウを紹介します。

この記事が気に入ったら
いいね または フォローしてね!

  • URLをコピーしました!
  • URLをコピーしました!
目次