Django prefetch_related()を使ってDBアクセスを高速化する方法 後編

この記事はDjango prefetch_related()を使ってDBアクセスを高速化する方法 前編の続きです。前編ではprefetch_related()の基本的な使い方を解説しました。

後編では、prefetch_related()とPrefetchオブジェクトを組み合わせて使う方法を解説します。

サンプルコードのRestaurantクラスやPizzaクラスなどは前回の記事を参照してください。

Prefetchオブジェクト

Prefetchオブジェクトを使うと、prefetch_related()による事前呼び出しをより細かく操作できます。

Prefetchの一番シンプルな使い方は、文字列ベースのルックアップです。Prefetchオブジェクトを使わなくても同等に書けます。

from django.db.models import Prefetch
Restaurant.objects.prefetch_related(Prefetch('pizzas__toppings'))

# Prefetchオブジェクトを使った時と同等の書き方
Restaurant.objects.prefetch_related('pizzas__toppings')

オプションの queryset引数で独自のクエリセットを指定できます。この引数を使う事で、クエリセット並び順を変更できます。

Restaurant.objects.prefetch_related(
    Prefetch('pizzas__toppings', queryset=Toppings.objects.order_by('name'))
)

さらにクエリ数を減らすために、可能なときは select_related() を使いましょう。

Pizza.objects.prefetch_related(
    Prefetch(
        'restaurants',
        queryset=Restaurant.objects.select_related('best_pizza'),
    )
)

オプションの to_attr引数 で、事前読み込みの結果を独自の属性に割り当てることもできます。結果はリストに直接格納されます。

これにより、異なる QuerySet で同じリレーションを複数回事前読み込みできるようになります。

vegetarian_pizzas = Pizza.objects.filter(vegetarian=True)
Restaurant.objects.prefetch_related(
    Prefetch('pizzas', to_attr='menu'),
    Prefetch('pizzas', queryset=vegetarian_pizzas, to_attr='vegetarian_menu'),
)

独自の to_attr で生成したルックアップは他のルックアップでも使用できます。

vegetarian_pizzas = Pizza.objects.filter(vegetarian=True)

# Prefetchクラスのto_attrで設定した'vegetarian_menu'を使って、
# toppingsの事前読み込みも行っています。
Restaurant.objects.prefetch_related(
     Prefetch('pizzas', queryset=vegetarian_pizzas, to_attr='vegetarian_menu'),
     'vegetarian_menu__toppings',
)

to_attr引数は、事前読み込みした結果をフィルタする際に推奨されます。これによりコードの可読性が上がります。

queryset = Pizza.objects.filter(vegetarian=True)

# 推奨 各レストランのvegetarian_pizzasを取得していることが明確です。
restaurants = Restaurant.objects.prefetch_related(
    Prefetch('pizzas', queryset=queryset, to_attr='vegetarian_pizzas'))
vegetarian_pizzas = restaurants[0].vegetarian_pizzas

# 非推奨 all()を使っていますが、各レストランの全てのピザではありません。読み違える恐れがあります。よって、上記の方法より可読性が下がります。
restaurants = Restaurant.objects.prefetch_related(
    Prefetch('pizzas', queryset=queryset))
vegetarian_pizzas = restaurants[0].pizzas.all()

独自の事前読み込みは、ForeignKey や OneToOneField のような単一のリレーションでも機能します。通常は select_related() を使うかも知れませんが、以下の様にQuerySet が役立つ場面も多くあります。

  • 関連モデルに対してさらに事前読み込みを行う QuerySet を使用したい
  • 関連オブジェクトのサブセットのみを事前読み込みしたい
  • deferred fieldsの様なパフォーマンスを最適化する方法を使いたい

まとめ

prefetch_related()の機能を前編・後編に分けて解説しました。

後編はPrefetchオブジェクトを使うことで、さらに細かい事前読み込みが可能になります。DBアクセスの最適化もできます。

機能が豊富ですぐに理解できないかも知れませんが、DBアクセスの最適化に関わる機能なのでselect_related()と合わせて是非使いこなして欲しいです。

スポンサーリンク
スポンサーリンク