Lets say you want to make a page to create or update a model that has a foreign key to 10k other records. If you are not careful, this can have detrimental consequences. For example, without caching, your app will need to pull 10k records from the db to make the drop-down. This post is my notes for dealing with this problem.
Cache the Values in a Fat Model
The goal is to create a form for creating and updating MyModel2 (see below). This model has a foreign key to MyModel1. Make a classmethod in Model1 for getting the drop down query set from cache. Over-ride the save method to keep the cache up to date.
from django.db import models from django.core.cache import cache class MyModel1(models): """Model with 50k records, 10k active.""" afield = models.CharField() is_active = models.BooleanField() def __unicode__(self): return self.afield @classmethod def get_active(cls): cache_key = 'model1_cache' qs = cache.get(cache_key) if not qs: qs = cls.objects.filter(is_active=True) cache.set(cache_key, qs) return qs def save(self, **kwargs): cache.delete('model1_cache') return super(MyModel1, self).save(**kwargs) class MyModel2(models): fk = models.ForeignKey(MyModel1) field2 = models.CharField() @property def fk_is_readonly(self): if self.field2 == 'xyz': return True else: return False
Use JQuery Chosen in Your Template
A standard HTML select sucks when you have lots of items. You will need an enhanced widget. Recently I tried using jquery autocomplete, but it required too many hacks to get it to work. The worst problem was limiting the user to the provided choices, while at the same time triggering an AJAX call once a valid value was entered. It was doable, but hacky and fragile.
I settled on Jquery Chosen. It’s easy to install and use. However some hacks were required when I needed it to be read-only. The hacks are in the Django form.
Make the ModelForm
If you use the standard methods for making the model form, Django will pull all the values from the database. You don’t want to do that. Below is what you want. The second line make sure the query set is pulled from cache and only has active items.
What happens if you are updating a MyModel2 record and the associated MyModel1 became inactive? We handle that by adding that instance to the query set in the __init__ method.
Finally, I often run into the case where fk needs to be “read-only”. I tried using the chosen “disable” function, but it removes the field from the POST data. I handle this by making a custom widget that’s a HiddenInput with text.
class MyModel2Form(forms.ModelForm): fk = forms.ModelChoiceField(required=True, queryset=MyModel1.get_active()) class Meta: model = MyModel2 fields = ('fk', 'field2') def __init__(self, *args, **kwargs): super(MyModel2Form, self).__init__(*args, **kwargs) if self.instance.id: # Make sure the current value is in the queryset. If that value became inactive since the record was # created, then it will not be in the query set. Without it, if the user saves without changing the # value they will get a form error saying its an invalid choice. new_qs = self.fields['fk'].queryset.filter(id=self.instance.fk.id) if not new_qs: self.fields['fk'].queryset = MyModel1.objects.filter(id=self.instance.fk.id) | self.fields['fk'].queryset self.fields['fk'].empty_label = None # remove the empty label if self.instance.fk_is_readonly: self.fields['fk'].widget = HiddenInputWText(unicode(self.instance.fk))
Hidden Input with Text
Here is one way to make a field “read-only”. The field value will still be in the POST. Use CSS to alter the appearance to make it clear this is readonly.