kopia lustrzana https://github.com/wagtail/wagtail
Update BaseSetting to make it easier to utilise QuerySet.select_related() for more complex settings which reference related objects (such as pages)
rodzic
e9371f45c7
commit
db8ab0875d
|
@ -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 }}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
),
|
||||
]
|
|
@ -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
|
||||
|
|
Ładowanie…
Reference in New Issue