#22841: ModelChoiceField does not make it easy to reuse querysets ---------------------------------------+------------------------ Reporter: mjtamlyn | Owner: nobody Type: New feature | Status: new Component: Forms | Version: master Severity: Normal | Keywords: Triage Stage: Unreviewed | Has patch: 0 Needs documentation: 0 | Needs tests: 0 Patch needs improvement: 0 | Easy pickings: 0 UI/UX: 0 | ---------------------------------------+------------------------ `ModelChoiceField` is designed to aggressively ensure that the queryset is always copied and executed anew each time. This is generally a good idea, but it has performance implications, especially where formsets of modelforms are concerned. The situation is not restricted to formsets however, there are other use cases where you may already have the executed queryset you need for the form within that request/response cycle.
Here's a simple example of a view and form with the API I might like to see. {{{ # views.py def book_create(request): categories = Category.objects.all() if request.method == 'POST': form = BookForm(data=request.POST, categories=categories) if form.is_valid(): form.save() return HttpResponseRedirect(reverse('book_list')) else: form = BookForm(categories=categories) context = { 'categories': categories, 'form': form, } return render('book_form.html', context) # forms.py class BookForm(forms.ModelForm): class Meta: model = Book fields = ['name', 'category'] def __init__(self, categories, **kwargs): super(BookForm, self).__init__(**kwargs) self.fields['category'].evaluated_queryset = categories }}} So we have a view to create a book, but that view has the list of categories in the context as it also includes a by-category navigation in a sidebar. As a result, in order to render the view we currently have to execute `Category.objects.all()` twice - once to render the navigation and once for the form. I have introduced a new proposed API to the `ModelChoiceField` (`form.fields['category']` in the example), currently called `evaluated_queryset`. This will be used by the `ModelChoiceIterator` *without* calling `.all()` on it, allowing the same queryset cache to be used twice within the view. ---- The current "best approach" for doing this that I've found looks as follows: {{{ class BookForm(forms.ModelForm): # ... def __init__(self, categories, **kwargs): super(BookForm, self).__init__(**kwargs) iterator = ModelChoiceIterator(self.fields['category']) choices = [iterator.choice(obj) for obj in categories] self.fields['category'].choices = choices }}} Whilst this is functional, it is not a particularly nice API. If we are happy with it as the correct pattern, we should document it, but at present `ModelChoiceIterator` is not documented, and it probably shouldn't be. ---- Possible more general improvements which become possible with a feature like this: - Automatic sharing of querysets between identical forms in a formset - Similarly, if the queryset has been executed then we can check inside it instead of doing the additional `.get()` query on data validation. This has a small performance gain on write in certain circumstances - in particular where you have a formset with 10+ fields, loading the full queryset once will be more efficient than doing 10 `.get()` queries. - Inlines and list editable in the admin could use this feature for significant performance improvements -- Ticket URL: <https://code.djangoproject.com/ticket/22841> Django <https://code.djangoproject.com/> The Web framework for perfectionists with deadlines. -- You received this message because you are subscribed to the Google Groups "Django updates" group. To unsubscribe from this group and stop receiving emails from it, send an email to django-updates+unsubscr...@googlegroups.com. To post to this group, send email to django-updates@googlegroups.com. To view this discussion on the web visit https://groups.google.com/d/msgid/django-updates/051.d8afc6c5d291bd517efe40b0c98662e0%40djangoproject.com. For more options, visit https://groups.google.com/d/optout.