Przeglądaj źródła

Added "defer" keyword argument to PageQuerySet.specific()

Setting this to True will tell Wagtail to defer all the specific fields
for each page. Each page will still be loaded in their correct class,
but only the fields defined on Page will be loaded in the initial query.

This gives a performance improvement when dealing with pages that have large
amounts of body text which we don't need to display in the admin.

Values of specific pages will continue to work, but will add extra
queries at the point of accessing them.
Karl Hobley 8 lat temu
rodzic
commit
c24fabe1d5
2 zmienionych plików z 58 dodań i 3 usunięć
  1. 23 3
      wagtail/core/query.py
  2. 35 0
      wagtail/core/tests/test_page_queryset.py

+ 23 - 3
wagtail/core/query.py

@@ -336,13 +336,21 @@ class PageQuerySet(SearchableQuerySetMixin, TreeQuerySet):
         for page in self.live():
             page.unpublish()
 
-    def specific(self):
+    def specific(self, defer=False):
         """
         This efficiently gets all the specific pages for the queryset, using
         the minimum number of queries.
+
+        When the "defer" keyword argument is set to True, only the basic page
+        fields will be loaded and all specific fields will be deferred. It
+        will still generate a query for each page type though (this may be
+        improved to generate only a single query in a future release).
         """
         clone = self._clone()
-        clone._iterable_class = SpecificIterable
+        if defer:
+            clone._iterable_class = DeferredSpecificIterable
+        else:
+            clone._iterable_class = SpecificIterable
         return clone
 
     def in_site(self, site):
@@ -352,7 +360,7 @@ class PageQuerySet(SearchableQuerySetMixin, TreeQuerySet):
         return self.descendant_of(site.root_page, inclusive=True)
 
 
-def specific_iterator(qs):
+def specific_iterator(qs, defer=False):
     """
     This efficiently iterates all the specific pages in a queryset, using
     the minimum number of queries.
@@ -375,6 +383,13 @@ def specific_iterator(qs):
         # model (i.e. Page) if the more specific one is missing
         model = content_types[content_type].model_class() or qs.model
         pages = model.objects.filter(pk__in=pks)
+
+        if defer:
+            # Defer all specific fields
+            from wagtail.core.models import Page
+            fields = [field.attname for field in Page._meta.get_fields() if field.concrete]
+            pages = pages.only(*fields)
+
         pages_by_type[content_type] = {page.pk: page for page in pages}
 
     # Yield all of the pages, in the order they occurred in the original query.
@@ -385,3 +400,8 @@ def specific_iterator(qs):
 class SpecificIterable(BaseIterable):
     def __iter__(self):
         return specific_iterator(self.queryset)
+
+
+class DeferredSpecificIterable(BaseIterable):
+    def __iter__(self):
+        return specific_iterator(self.queryset, defer=True)

+ 35 - 0
wagtail/core/tests/test_page_queryset.py

@@ -619,6 +619,41 @@ class TestSpecificQuery(TestCase):
             Page.objects.get(url_path='/home/other/').specific,
         ])
 
+    def test_deferred_specific_query(self):
+        # Tests the "defer" keyword argument, which defers all specific fields
+        root = Page.objects.get(url_path='/home/')
+
+        with self.assertNumQueries(0):
+            # The query should be lazy.
+            qs = root.get_descendants().specific(defer=True)
+
+        with self.assertNumQueries(4):
+            # This still performs 4 queries (one for each specific class)
+            # even though we're only pulling in fields from the base Page
+            # model.
+            # TODO: Find a way to make this perform a single query
+            pages = list(qs)
+
+        self.assertIsInstance(pages, list)
+        self.assertEqual(len(pages), 7)
+
+        for page in pages:
+            # An instance of the specific page type should be returned,
+            # not wagtailcore.Page.
+            content_type = page.content_type
+            model = content_type.model_class()
+            self.assertIsInstance(page, model)
+
+            # The page should already be the specific type, so this should not
+            # need another database query.
+            with self.assertNumQueries(0):
+                self.assertIs(page, page.specific)
+
+        # Unlike before, the content fields should be now deferred. This means
+        # that accessing them will generate a new query.
+        with self.assertNumQueries(1):
+            pages[1].body
+
 
 class TestFirstCommonAncestor(TestCase):
     """