Django模型方法get_absolute_url访问ForeignKey关系导致重复的SQL查询。

huangapple go评论60阅读模式
英文:

django duplicate SQL queries stemming from model method get_absolute_url that accesses ForeignKey relationship

问题

问题概要

我有一个基于类的列表视图,只显示每个对象的链接以及一些来自某些对象字段的数据。链接的 href 是使用模型中我编写的 get_absolute_url 方法生成的。问题是每次运行 get_absolute_url 时都会查询数据库。这导致许多重复查询(在将来,随着更多对象,这将成为问题)。

尝试的解决方法

我的 get_absolute_url 方法访问了模型中的一些外键字段,所以我尝试在我的视图中为查询集使用 .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 调试工具栏)

此查询被重复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.
英文:

Problem Summary

I have a class-based list view that just displays links for each object along with some data from some of the object fields. The href for the links is generated using a get_absolute_url method that I've written in the model itself. The problem is that the database is queried each time get_absolute_url is run. This causes many duplicate queries (in the future, with more objects, this will be a problem).

Attempted Solutions

My get_absolute_url method accesses some ForeignKey fields from my model, so I tried to use .select_related() for my queryset in my view. This did not change anything though.

Question

How can I eliminate the duplicate queries from running get_absolute_url?

Code

models.py

class LanguageLocale(models.Model):
    &quot;&quot;&quot;model for representing language locale combinations&quot;&quot;&quot;

    class LANG_CODES(models.TextChoices):
        EN = &#39;en&#39;, _(&#39;English&#39;)
        ES = &#39;es&#39;, _(&#39;espa&#241;ol&#39;)
        QC = &#39;qc&#39;, _(&quot;K&#39;iche&#39;&quot;)

    lang = models.CharField(max_length=2, choices=LANG_CODES.choices, blank=False)

class Scenario(models.Model):
    &quot;&quot;&quot;model for representing interpreting practice scenarios&quot;&quot;&quot;

    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 = &#39;PROD&#39;, _(&#39;Production&#39;)
        STGE = &#39;STGE&#39;, _(&#39;Staged&#39;)
        EXPR = &#39;EXPR&#39;, _(&#39;Experimental&#39;)

    status = models.CharField(
        max_length=4, choices=SCENARIO_STATUSES.choices, default=&#39;EXPR&#39;)

    def get_absolute_url(self):
        &quot;&quot;&quot;Returns the URL to access a detail record for this scenario.&quot;&quot;&quot;
        return reverse(&#39;dialogue-detail&#39;, kwargs={
            &#39;lang_power&#39;: self.lang_power.lang,
            &#39;lang_primary_non_power&#39;: self.lang_primary_non_power.lang,
            &#39;pk&#39;: self.scenario_id
            }
        )

views.py

class ScenarioListView(generic.ListView):
    &quot;&quot;&quot;View class for list of Scenarios&quot;&quot;&quot;

    queryset = Scenario.objects.select_related(
        &#39;domain_subdomain&#39;, &#39;lang_power&#39;, &#39;lang_primary_non_power&#39;
    )
    
    demo = Scenario.objects.get(scenario_id=&#39;demo&#39;)
    prod = Scenario.objects.filter(status=&#39;PROD&#39;)
    staged = Scenario.objects.filter(status=&#39;STGE&#39;)
    experimental = Scenario.objects.filter(status=&#39;EXPR&#39;)
    
    extra_context = {
        &#39;demo&#39;: demo,
        &#39;prod&#39;: prod,
        &#39;staged&#39;: staged,
        &#39;experimental&#39;: experimental,
    }

scenario_list.html

{% extends &quot;home.html&quot; %}

{% block content %}
  &lt;h1&gt;Practice Dialogues&lt;/h1&gt;
  &lt;section&gt;
    {% if prod %}
      &lt;ul&gt;
        {% for scenario in prod %}
          &lt;li&gt;
            &lt;a href=&quot;{{ scenario.get_absolute_url }}&quot;&gt;{{  scenario.title }}&lt;/a&gt; ({{ 
            scenario.domain_subdomain.domain }})
          &lt;/li&gt;
        {% endfor %}
      &lt;/ul&gt;
    {% else %}
      &lt;p&gt;There are no practice scenarios. Something went wrong.&lt;/p&gt;
    {% endif %}
  &lt;/section&gt;
  {% if user.is_staff %}
    &lt;section&gt;
      {% if staged %}
        &lt;article&gt;
          &lt;h2&gt;Staged Dialogues&lt;/h2&gt;
          &lt;ul&gt;
            {% for scenario in staged %}
              &lt;li&gt;
                &lt;a href=&quot;{{ scenario.get_absolute_url }}&quot;&gt;{{  scenario.title }}&lt;/a&gt; ({{ 
                scenario.domain_subdomain.domain }})
              &lt;/li&gt;
            {% endfor %}
          &lt;/ul&gt;
        &lt;/article&gt;
      {% endif %}
      {% if experimental %}
        &lt;article&gt;
          &lt;h2&gt;Experimental Dialogues&lt;/h2&gt;
          &lt;ul&gt;
            {% for scenario in experimental %}
              &lt;li&gt;
                &lt;a href=&quot;{{ scenario.get_absolute_url }}&quot;&gt;{{  scenario.title }}&lt;/a&gt; ({{ 
                scenario.domain_subdomain.domain }})
              &lt;/li&gt;
            {% endfor %}
          &lt;/ul&gt;
        &lt;/article&gt;
      {% endif %}
    &lt;/section&gt;
  {% endif %}
{% endblock %}

duplicate queries readout (from django debug toolbar)

This query is duplicated 3 times:

SELECT &quot;scenario_languagelocale&quot;.&quot;id&quot;,
       &quot;scenario_languagelocale&quot;.&quot;lang_locale&quot;,
       &quot;scenario_languagelocale&quot;.&quot;lang&quot;
  FROM &quot;scenario_languagelocale&quot;
 WHERE &quot;scenario_languagelocale&quot;.&quot;id&quot; = 1
 LIMIT 21 6 similar queries.  Duplicated 3 times.

This query is duplicated 2 times:

SELECT &quot;scenario_languagelocale&quot;.&quot;id&quot;,
       &quot;scenario_languagelocale&quot;.&quot;lang_locale&quot;,
       &quot;scenario_languagelocale&quot;.&quot;lang&quot;
  FROM &quot;scenario_languagelocale&quot;
 WHERE &quot;scenario_languagelocale&quot;.&quot;id&quot; = 3
 LIMIT 21 6 similar queries.  Duplicated 2 times.

答案1

得分: 1

我建议不要使用extra_context。这样做会阻止在两个请求之间重新评估查询,这意味着如果你添加了一个额外的Scenario,并且没有重新启动服务器,它不会在你再次请求场景时进行“更新”。demo查询甚至更为棘手,因为它在启动服务器时立即运行,因此如果你在带有数据库的服务器上运行它,并且没有这样的演示Scenario,可能会引发错误。

你可以使用queryset来处理已经在这里执行必要关系的&lt;code&gt;.select_related(&amp;hellip;)&lt;/code&gt;

from django.shortcuts import get_object_or_404

class ScenarioListView(generic.ListView):
    &amp;quot;&amp;quot;&amp;quot;Scenarios列表的视图类&amp;quot;&amp;quot;&amp;quot;
    queryset = Scenario.objects.select_related(
        &#39;domain_subdomain&#39;, &#39;lang_power&#39;, &#39;lang_primary_non_power&#39;
    )

    def get_context_data(self, *args, **kwargs):
        return super().get_context_data(
            *args,
            **kwargs,
            demo=get_object_or_404(&lt;b&gt;self.queryset&lt;/b&gt;, scenario_id=&#39;demo&#39;),
            prod=&lt;b&gt;self.queryset&lt;/b&gt;.filter(status=&#39;PROD&#39;),
            staged=&lt;b&gt;self.queryset&lt;/b&gt;.filter(status=&#39;STGE&#39;),
            experimental=&lt;b&gt;self.queryset&lt;/b&gt;.filter(status=&#39;EXPR&#39;),
        )&lt;/code&gt;&lt;/pre&gt;
英文:

I would advise not to use extra_context. This will prevent reevaluating the query between two requests, which means that if you thus add an extra Scenario, and you do not restart the server, it will not "update' the list when you ask for the scenario's again. The demo query is even more problematic, since that runs immediately when you start the server, and thus can raise errors if you run this on a server with a database, without such demo Scenario.

You can however work with queryset which already performs a <code>.select_related(&hellip;)</code> on the necessary relations here, so:

<pre><code>from django.shortcuts import get_object_or_404

class ScenarioListView(generic.ListView):
&quot;&quot;&quot;View class for list of Scenarios&quot;&quot;&quot;
queryset = Scenario.objects.select_related(
'domain_subdomain', 'lang_power', 'lang_primary_non_power'
)

def get_context_data(self, *args, **kwargs):
    return super().get_context_data(
        *args,
        **kwargs,
        demo=get_object_or_404(&lt;b&gt;self.queryset&lt;/b&gt;, scenario_id=&#39;demo&#39;),
        prod=&lt;b&gt;self.queryset&lt;/b&gt;.filter(status=&#39;PROD&#39;),
        staged=&lt;b&gt;self.queryset&lt;/b&gt;.filter(status=&#39;STGE&#39;),
        experimental=&lt;b&gt;self.queryset&lt;/b&gt;.filter(status=&#39;EXPR&#39;),
    )&lt;/code&gt;&lt;/pre&gt;

huangapple
  • 本文由 发表于 2023年6月1日 00:44:54
  • 转载请务必保留本文链接:https://go.coder-hub.com/76375702.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定