この記事は以下のターゲットを対象としています。
★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
id | name |
1 | A-1 |
table_b
id | name | table_a |
1 | B-1 | 1 |
2 | B-2 | 1 |
3 | B-3 | 1 |
table_c
id | name | table_b |
1 | C-1 | 1 |
2 | C-2 | 1 |
3 | C-3 | 1 |
まず、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のノウハウを紹介します。