Django QuerySet Heisenberg gotcha
We have a task management system in Django that I was modifying to email assignees when they are added to or removed from a task. The changes worked fine when testing on the development server, but refused to notice any changes to the assignee list in the live server.
See if you can tell why:
[toggle code]
-
1.
class projectBaseForm(forms.ModelForm):
-
2.
def save(self, manager=None):
-
3.
if self.instance.id:
- 4. previousAssignees = self.instance.assignees.all()
-
5.
else:
- 6. previousAssignees = []
- 7. #print "Previous assignees:", previousAssignees
- 8. newVersion = super(projectBaseForm, self).save()
- 9. self.assigneeNotifications(previousAssignees, newVersion)
- 10. return newVersion
-
3.
if self.instance.id:
-
11.
def assigneeNotifications(self, previousAssignees, task):
- 12. assignees = task.assignees.all()
- 13. #construct new assignee list
- 14. newAssignees = []
-
15.
for assignee in assignees:
-
16.
if assignee not in previousAssignees:
- 17. newAssignees.append(assignee)
-
16.
if assignee not in previousAssignees:
- 18. #construct removed assignee list
- 19. removedAssignees = []
-
20.
for assignee in previousAssignees:
-
21.
if assignee not in assignees:
- 22. removedAssignees.append(assignee)
-
21.
if assignee not in assignees:
-
23.
if newAssignees:
- 24. task.taskMail(newAssignees, mailTemplate="projects/mails/assigned.mail")
-
25.
if removedAssignees:
- 26. task.taskMail(removedAssignees, mailTemplate="projects/mails/deassigned.mail")
-
2.
def save(self, manager=None):
If the assignees before making a change are George, Bill, and Ted, and the assignees after making the change are George, Bob, and Jay, what are the values of previousAssignees and assignees in the two for loops in the assigneeNotifications method at lines 15 and 20?
The answer depends on whether or not the “print "Previous assignees:", previousAssignees” line is commented or uncommented. One of the key features of Django’s QuerySets is that they aren’t evaluated until they “need” to be evaluated. But that also lends a somewhat non-sequential Heisenbergian nature to the code. In the version as it’s listed above, previousAssignees isn’t evaluated until it is used in the assigneeNotifications method. By this time, the previous assignees have been lost—the save has already happened putting the new assignees in place!
Observing a system changes the system. During the initial testing, I used that print line to see what was happening. And the act of printing previousAssignees caused the QuerySet to evaluate, producing the result I wanted. When I moved it to the production server, I commented that line out. The fix is simple:
[toggle code]
-
-
def save(self, manager=None):
-
if self.instance.id:
- #use list to force it to perform the query immediately
- #otherwise, it waits until after the changes have been made to perform the query
- previousAssignees = list(self.instance.assignees.all())
- else:
-
if self.instance.id:
-
def save(self, manager=None):
As described on the Django QuerySet API page, I need to force the query to evaluate if I want it to evaluate immediately, using something like the list() method.
- Django QuerySet API
- “This document describes the details of the QuerySet API. Internally, a QuerySet can be constructed, filter, sliced, and generally passed around without actually hitting the database. No database activity actually occurs until you do something to evaluate the queryset.”
More Django
- Converting an existing Django model to Django-MPTT
- Using a SQL database to mimic a filesystem will, eventually, create bottlenecks when it comes to traversing the filesystem. One solution is modified preordered tree traversal, which saves the tree structure in an easily-used manner inside the model.
- Two search bookmarklets for Django
- Bookmarklets—JavaScript code in a bookmark—can make working with big Django databases much easier.
- Fixing Django’s feed generator without hacking Django
- It looks like it’s going to be a while before the RSS feed generator in Django is going to get fixed, so I looked into subclassing as a way of getting a working guid in my Django RSS feeds.
- ModelForms and FormViews
- This is just a notice because when I did a search, nothing came up. Don’t use ModelForm with FormView, use UpdateView instead.
- Django: fix_ampersands and abbreviations
- The fix_ampersands filter will miss some cases where ampersands need to be replaced.
- 29 more pages with the topic Django, and other related pages
More Duh
- ModelForms and FormViews
- This is just a notice because when I did a search, nothing came up. Don’t use ModelForm with FormView, use UpdateView instead.
- minidom self-closes empty SCRIPT tags
- Python’s minidom will self-close empty script tags—as it should. But it turns out that Firefox 3.6 and IE 8 don’t support empty script tags.
- Django using too much memory? Turn off DEBUG=True!
- DEBUG=False can save hundreds of megabytes in Django command-line scripts, and probably in Django web processes.
- Add nodes to SimpleXMLElement
- If you want to add child nodes in PHP’s SimpleXML, the correct way to do it is to add the node first, then create it.
- No distutils? Install Xcode
- If Distutils is not available on Mac OS X Leopard, install the Xcode developer tools. Also, the upgrade process I followed for upgrading from Mailman 2.1.9 to 2.1.12.