Update BaseSetting to make it easier to utilise QuerySet.select_related() for more complex settings which reference related objects (such as pages)

pull/5934/head
Andy Babic 2020-04-03 21:46:32 +01:00
rodzic e9371f45c7
commit db8ab0875d
5 zmienionych plików z 141 dodań i 2 usunięć

Wyświetl plik

@ -222,3 +222,47 @@ Or, alternately, using the ``set`` tag:
.. code-block:: html+jinja
{% set social_settings=settings("app_label.SocialMediaSettings") %}
Utilising ``select_related`` to improve efficiency
--------------------------------------------------
For models with foreign key relationships to other objects (e.g. pages),
which are very often needed to output values in templates, you can set
the ``select_related`` attribute on your model to have Wagtail utilise
Django's `QuerySet.select_related() <https://docs.djangoproject.com/en/stable/ref/models/querysets/#select-related>`_
method to fetch the settings object and related objects in a single query.
With this, the initial query is more complex, but you will be able to
freely access the foreign key values without any additional queries,
making things more efficient overall.
Building on the ``ImportantPages`` example from the previous section, the
following shows how ``select_related`` can be set to improve efficiency:
.. code-block:: python
:emphasize-lines: 4,5
@register_setting
class ImportantPages(BaseSetting):
# Fetch these pages when looking up ImportantPages for or a site
select_related = ["donate_page", "sign_up_page"]
donate_page = models.ForeignKey(
'wagtailcore.Page', null=True, on_delete=models.SET_NULL, related_name='+')
sign_up_page = models.ForeignKey(
'wagtailcore.Page', null=True, on_delete=models.SET_NULL, related_name='+')
panels = [
PageChooserPanel('donate_page'),
PageChooserPanel('sign_up_page'),
]
With these additions, the following template code will now trigger
a single database query instead of three (one to fetch the settings,
and two more to fetch each page):
.. code-block:: html
{{ settings.app_label.ImportantPages.donate_page.url }}
{{ settings.app_label.ImportantPages.sign_up_page.url }}

Wyświetl plik

@ -13,18 +13,42 @@ class BaseSetting(models.Model):
:func:`~wagtail.contrib.settings.registry.register_setting`
"""
# Override to fetch ForeignKey values in the same query when
# retrieving settings via for_site()
select_related = None
site = models.OneToOneField(
Site, unique=True, db_index=True, editable=False, on_delete=models.CASCADE)
class Meta:
abstract = True
@classmethod
def base_queryset(cls):
"""
Returns a queryset of objects of this type to use as a base
for calling get_or_create() on.
You can use the `select_related` attribute on your class to
specify a list of foreign key field names, which the method
will attempt to select additional related-object data for
when the query is executed.
If your needs are more complex than this, you can override
this method on your custom class.
"""
queryset = cls.objects.all()
if cls.select_related is not None:
queryset = queryset.select_related(*cls.select_related)
return queryset
@classmethod
def for_site(cls, site):
"""
Get or create an instance of this setting for the site.
"""
instance, created = cls.objects.get_or_create(site=site)
queryset = cls.base_queryset()
instance, created = queryset.get_or_create(site=site)
return instance
@classmethod

Wyświetl plik

@ -1,7 +1,7 @@
from django.test import TestCase, override_settings
from wagtail.core.models import Site
from wagtail.tests.testapp.models import TestSetting
from wagtail.tests.testapp.models import ImportantPages, TestSetting
from .base import SettingsTestMixin
@ -48,3 +48,38 @@ class SettingModelTestCase(SettingsTestMixin, TestCase):
with self.assertNumQueries(1):
for i in range(4):
TestSetting.for_request(request)
def _create_importantpages_object(self):
site = self.default_site
ImportantPages.objects.create(
site=site,
sign_up_page=site.root_page,
general_terms_page=site.root_page,
privacy_policy_page=self.other_site.root_page,
)
def test_select_related(self, expected_queries=4):
""" The `select_related` attribute on setting models is `None` by default, so fetching foreign keys values requires additional queries """
request = self.get_request()
self._create_importantpages_object()
# force site query beforehand
Site.find_for_request(request)
# fetch settings and access foreiegn keys
with self.assertNumQueries(expected_queries):
settings = ImportantPages.for_request(request)
settings.sign_up_page
settings.general_terms_page
settings.privacy_policy_page
def test_select_related_use_reduces_total_queries(self):
""" But, `select_related` can be used to reduce the number of queries needed to fetch foreign keys """
try:
# set class attribute temporarily
ImportantPages.select_related = ['sign_up_page', 'general_terms_page', 'privacy_policy_page']
self.test_select_related(expected_queries=1)
finally:
# undo temporary change
ImportantPages.select_related = None

Wyświetl plik

@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('tests', '0047_restaurant_tags'),
]
operations = [
migrations.CreateModel(
name='ImportantPages',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('general_terms_page', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='wagtailcore.Page')),
('privacy_policy_page', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='wagtailcore.Page')),
('sign_up_page', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='wagtailcore.Page')),
('site', models.OneToOneField(editable=False, on_delete=django.db.models.deletion.CASCADE, to='wagtailcore.Site')),
],
options={
'abstract': False,
},
),
]

Wyświetl plik

@ -1020,6 +1020,16 @@ class TestSetting(BaseSetting):
email = models.EmailField(max_length=50)
@register_setting
class ImportantPages(BaseSetting):
sign_up_page = models.ForeignKey(
'wagtailcore.Page', related_name="+", null=True, on_delete=models.SET_NULL)
general_terms_page = models.ForeignKey(
'wagtailcore.Page', related_name="+", null=True, on_delete=models.SET_NULL)
privacy_policy_page = models.ForeignKey(
'wagtailcore.Page', related_name="+", null=True, on_delete=models.SET_NULL)
@register_setting(icon="tag")
class IconSetting(BaseSetting):
pass