Django中get_absolute_url访问外键引发重复SQL查询问题
问题描述
我使用基于类的列表视图展示对象链接及字段数据,链接的href由模型中的get_absolute_url方法生成。该方法访问模型的ForeignKey字段,每次调用都会触发数据库查询,导致大量重复SQL查询(数据量增大后问题会更严重)。我已尝试在视图的queryset中使用.select_related(),但无效果。请问如何消除get_absolute_url引发的重复查询?
代码示例
models.py
class LanguageLocale(models.Model): """model for representing language locale combinations""" class LANG_CODES(models.TextChoices): EN = 'en', _('English') ES = 'es', _('español') QC = 'qc', _("K'iche'") lang = models.CharField(max_length=2, choices=LANG_CODES.choices, blank=False) class Scenario(models.Model): """model for representing interpreting practice scenarios""" scenario_id = models.CharField(max_length=20, primary_key=True) lang_power = models.ForeignKey(LanguageLocale) lang_primary_non_power = models.ForeignKey(LanguageLocale) class SCENARIO_STATUSES(models.TextChoices): PROD = 'PROD', _('Production') STGE = 'STGE', _('Staged') EXPR = 'EXPR', _('Experimental') status = models.CharField( max_length=4, choices=SCENARIO_STATUSES.choices, default='EXPR') def get_absolute_url(self): """Returns the URL to access a detail record for this scenario.""" return reverse('dialogue-detail', kwargs={ 'lang_power': self.lang_power.lang, 'lang_primary_non_power': self.lang_primary_non_power.lang, 'pk': self.scenario_id } )
views.py
class ScenarioListView(generic.ListView): """View class for list of Scenarios""" queryset = Scenario.objects.select_related( 'domain_subdomain', 'lang_power', 'lang_primary_non_power' ) demo = Scenario.objects.get(scenario_id='demo') prod = Scenario.objects.filter(status='PROD') staged = Scenario.objects.filter(status='STGE') experimental = Scenario.objects.filter(status='EXPR') extra_context = { 'demo': demo, 'prod': prod, 'staged': staged, 'experimental': experimental, }
scenario_list.html
{% extends "home.html" %} {% block content %} <h1>Practice Dialogues</h1> <section> {% if prod %} <ul> {% for scenario in prod %} <li> <a href="{{ scenario.get_absolute_url }}">{{ scenario.title }}</a> ({{ scenario.domain_subdomain.domain }}) </li> {% endfor %} </ul> {% else %} <p>There are no practice scenarios. Something went wrong.</p> {% endif %} </section> {% if user.is_staff %} <section> {% if staged %} <article> <h2>Staged Dialogues</h2> <ul> {% for scenario in staged %} <li> <a href="{{ scenario.get_absolute_url }}">{{ scenario.title }}</a> ({{ scenario.domain_subdomain.domain }}) </li> {% endfor %} </ul> </article> {% endif %} {% if experimental %} <article> <h2>Experimental Dialogues</h2> <ul> {% for scenario in experimental %} <li> <a href="{{ scenario.get_absolute_url }}">{{ scenario.title }}</a> ({{ scenario.domain_subdomain.domain }}) </li> {% endfor %} </ul> </article> {% endif %} </section> {% endif %} {% endblock %}
重复查询信息(来自Django Debug Toolbar)
重复3次的查询:
SELECT "scenario_languagelocale"."id", "scenario_languagelocale"."lang_locale", "scenario_languagelocale"."lang" FROM "scenario_languagelocale" WHERE "scenario_languagelocale"."id" = 1 LIMIT 21 6 similar queries. Duplicated 3 times.
重复2次的查询:
SELECT "scenario_languagelocale"."id", "scenario_languagelocale"."lang_locale", "scenario_languagelocale"."lang" FROM "scenario_languagelocale" WHERE "scenario_languagelocale"."id" = 3 LIMIT 21 6 similar queries. Duplicated 2 times.
解决方案
问题核心是你视图里手动定义的prod、staged、experimental这些查询集,根本没用到配置的select_related——ListView的queryset属性只给默认的object_list用,而你直接创建的这些查询集是独立的,没有预取关联数据,所以遍历它们时,每次调用get_absolute_url都会触发额外的ForeignKey查询。
具体修复步骤:
- 移除视图类中直接定义的
demo、prod、staged、experimental属性,这些属性会在类加载阶段就执行数据库查询,不仅没用到预取逻辑,还会导致不必要的提前查询。 - 重写
get_context_data方法,在这个方法里基于带select_related的基础查询集,创建各状态的场景集合:
修改后的views.py代码:
class ScenarioListView(generic.ListView): """View class for list of Scenarios""" queryset = Scenario.objects.select_related( 'domain_subdomain', 'lang_power', 'lang_primary_non_power' ) def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) # 基于预取后的基础查询集生成各状态数据 base_qs = self.get_queryset() context['demo'] = base_qs.get(scenario_id='demo') context['prod'] = base_qs.filter(status='PROD') context['staged'] = base_qs.filter(status='STGE') context['experimental'] = base_qs.filter(status='EXPR') return context
额外优化方案:
如果lang字段是固定枚举值,可以在Scenario模型中添加lang_power_code和lang_primary_non_power_code两个CharField,通过重写save方法保持与ForeignKey字段的同步。这样get_absolute_url可以直接读取本地字段,彻底避免关联查询:
class Scenario(models.Model): # ... 原有字段 ... lang_power_code = models.CharField(max_length=2, choices=LanguageLocale.LANG_CODES.choices) lang_primary_non_power_code = models.CharField(max_length=2, choices=LanguageLocale.LANG_CODES.choices) def save(self, *args, **kwargs): # 保存前同步code字段 self.lang_power_code = self.lang_power.lang self.lang_primary_non_power_code = self.lang_primary_non_power.lang super().save(*args, **kwargs) def get_absolute_url(self): return reverse('dialogue-detail', kwargs={ 'lang_power': self.lang_power_code, 'lang_primary_non_power': self.lang_primary_non_power_code, 'pk': self.scenario_id })
修改完成后,打开Django Debug Toolbar验证,重复的LanguageLocale查询应该会消失。
内容的提问来源于stack exchange,提问作者DevinG
相关产品推荐
相关产品推荐

