prefetch_relatedはDjango 1.4で追加された機能です。
親子関係を表すモデル(多対多になってるものなど)をツリー状に表示する場合、ループ内でクエリを実行しってしまうと、クエリ数が多くて極端に遅くなります(特に2段目とか3段目)。
prefetch_relatedを使うと、事前にリレーション先のデータを取得しておき、ループ内で新たにクエリが実行されないようにできます。
試したバージョンは、Python2.7、Django1.6.5です。
ソースコード
完全なコードはgithubに置いてます。
sample_nullpobug/django/market at main · tokibito/sample_nullpobug · GitHub
shop/models.py
# coding: utf-8 from django.db import models class Category(models.Model): "カテゴリ" name = models.CharField(max_length=40) def __unicode__(self): return self.name class Meta: db_table = 'category' class Item(models.Model): "商品" name = models.CharField(max_length=40) code = models.CharField(max_length=10, unique=True) price = models.IntegerField() category = models.ForeignKey('Category') def __unicode__(self): return self.name class Meta: db_table = 'item' class Bundle(models.Model): "まとめ売り" name = models.CharField(max_length=40) price = models.IntegerField() items = models.ManyToManyField('Item', through='BundleItem') def __unicode__(self): return self.name class Meta: db_table = 'bundle' class BundleItem(models.Model): "まとめ売り商品" bundle = models.ForeignKey('Bundle') item = models.ForeignKey('Item') class Meta: db_table = 'bundle_item'
django-extensionsのgraph_modelsコマンドで出力すると、こういう構造になっています。
shop/templates/index.html
テンプレートファイルは、どちらのビュー関数でも同じものを使います。
<html> <body> <ul> {% for bundle in bundles %} <li>{{ bundle.name }} <ul> {% for item in bundle.items.all %} <li>{{ item }} - {{ item.category }}</li> {% endfor %} </ul> </li> {% endfor %} </ul> </body> </html>
bundle、itemのところで2重のforループになっています。また2つ目のほうは、bundleからitemを、itemからcategoryを取得して使うようなテンプレートになっています。
shop/views.py
prefetch_relatedを使わない場合と使う場合のビュー関数をそれぞれ用意して、urls.pyに設定しておきます。
# coding: utf-8 from django.shortcuts import render from shop.models import Category, Item, Bundle def no_prefetch_view(request): # prefetch_relatedを使わない場合 bundles = Bundle.objects.all() return render(request, 'index.html', {'bundles': bundles}) def prefetch_view(request): # prefetch_relatedを使う場合 bundles = Bundle.objects.prefetch_related('items__category') return render(request, 'index.html', {'bundles': bundles})
prefetch_relatedを使う場合、今回はテンプレート中でbundle→item→categoryの順でツリー状に表示するため、「items__category」までをプリフェッチ対象に指定しています。
prefetch_relatedを使わない場合
debug-toolbarでSQLをみてみると、クエリ数は9回になっています。
テンプレート中の「bundle.items.all」の部分と、「item.category」の部分でクエリが実行されているためです。
prefetch_relatedを使う場合
debug-toolbarでSQLをみてみると、クエリ数は4回になっています。
bundle_idをINで指定してitemテーブル、category_idをINで指定してcategoryテーブルのデータをまとめて取得しています。
このようにprefetch_relatedを使うことで、いくらかクエリ数を減らせます。