Implement initial version of Block.bulk_to_python (with jaroel)

This prevents n+ queries for n blocks of a specific type.
This commit is contained in:
Michael van Tellingen 2016-06-16 14:13:32 +02:00 committed by Matt Westcott
parent 74d13822d5
commit 7d7509aee5
5 changed files with 62 additions and 1 deletions

View file

@ -12,6 +12,7 @@ Changelog
* Form builder now validates against multiple fields with the same name (Richard McMillan)
* The 'choices' field on the form builder no longer has a maximum length (Johannes Spielmann)
* The wagtailimages.Filter model has been removed, and converted to a Python class instead (Gagaro)
* Multiple ChooserBlocks inside a StreamField are now prefetched in bulk, for improved performance (Michael van Tellingen, Roel Bruggink, Matt Westcott)
* Fix: Email templates and document uploader now support custom `STATICFILES_STORAGE` (Jonny Scholes)
* Fix: Removed alignment options (deprecated in HTML and not rendered by Wagtail) from `TableBlock` context menu (Moritz Pfeiffer)
* Fix: Fixed incorrect CSS path on ModelAdmin's "choose a parent page" view

View file

@ -22,6 +22,7 @@ Minor features
* Form builder now validates against multiple fields with the same name (Richard McMillan)
* The 'choices' field on the form builder no longer has a maximum length (Johannes Spielmann)
* The wagtailimages.Filter model has been removed, and converted to a Python class instead (Gagaro)
* Multiple ChooserBlocks inside a StreamField are now prefetched in bulk, for improved performance (Michael van Tellingen, Roel Bruggink, Matt Westcott)
Bug fixes

View file

@ -399,6 +399,16 @@ class ChooserBlock(FieldBlock):
except self.target_model.DoesNotExist:
return None
def bulk_to_python(self, values):
"""Return the model instances for the given list of primary keys.
The instances must be returned in the same order as the values and keep None values.
"""
initial = {key: None for key in values}
objects = self.target_model.objects.in_bulk(values)
initial.update(objects)
return [initial[id] for id in values] # Keeps the ordering the same as in values.
def get_prep_value(self, value):
# the native value (a model instance or None) should serialise to a PK or None
if value is None:

View file

@ -308,7 +308,11 @@ class StreamValue(collections.Sequence):
raw_value = self.stream_data[i]
type_name = raw_value['type']
child_block = self.stream_block.child_blocks[type_name]
value = child_block.to_python(raw_value['value'])
if hasattr(child_block, 'bulk_to_python'):
self._prefetch_blocks(type_name, child_block)
return self._bound_blocks[i]
else:
value = child_block.to_python(raw_value['value'])
else:
type_name, value = self.stream_data[i]
child_block = self.stream_block.child_blocks[type_name]
@ -317,6 +321,21 @@ class StreamValue(collections.Sequence):
return self._bound_blocks[i]
def _prefetch_blocks(self, type_name, child_block):
"""Prefetch all child blocks for the given `type_name` using the
given `child_blocks`.
This prevents n queries for n blocks of a specific type.
"""
raw_values = collections.OrderedDict(
(i, item['value']) for i, item in enumerate(self.stream_data)
if item['type'] == type_name
)
converted_values = child_block.bulk_to_python(raw_values.values())
for i, value in zip(raw_values.keys(), converted_values):
self._bound_blocks[i] = StreamValue.StreamChild(child_block, value)
def __len__(self):
return len(self.stream_data)

View file

@ -84,6 +84,36 @@ class TestLazyStreamField(TestCase):
with self.assertNumQueries(0):
instances_lookup[self.no_image.pk].body[0]
def test_lazy_load_queryset_bulk(self):
"""
Ensure that lazy loading StreamField works when gotten as part of a
queryset list
"""
file_obj = get_test_image_file()
image_1 = Image.objects.create(title='Test image 1', file=file_obj)
image_3 = Image.objects.create(title='Test image 3', file=file_obj)
with_image = StreamModel.objects.create(body=json.dumps([
{'type': 'image', 'value': image_1.pk},
{'type': 'image', 'value': None},
{'type': 'image', 'value': image_3.pk},
{'type': 'text', 'value': 'foo'}]))
with self.assertNumQueries(1):
instance = StreamModel.objects.get(pk=with_image.pk)
# Prefetch all image blocks
with self.assertNumQueries(1):
instance.body[0]
# 1. Further image block access should not execute any db lookups
# 2. The blank block '1' should be None.
# 3. The values should be in the original order.
with self.assertNumQueries(0):
assert instance.body[0].value.title == 'Test image 1'
assert instance.body[1].value is None
assert instance.body[2].value.title == 'Test image 3'
class TestSystemCheck(TestCase):
def tearDown(self):