Hi Djangonauts, I'd like to call attention to a patch[0] I submitted last week for #6870[1]. From one perspective, this ticket is a version of the "My models with foreign keys are deleted when their related model deletes because of Django's ON CASCADE DELETE behavior." But from another perspective this ticket is a (the only?) ticket that correctly identifies a bug with the usage of the pre_delete signal, and other tickets are distinguished as feature requests: e.g., adding ON DELETE behaviors, or "Django should set nullable foreign key fields to NULL".
The Bug django.db.models.Model.delete first gathers self._collect_sub_objects to cache all related objects that will be deleted by this model's deletion. (This cache includes the model itself.) Model.delete then calls django.db.models.query.delete_objects, which calls the pre_delete signal for each of the objects before deleting them. Performing the caching before sending the signal breaks the signal's promise with receivers that they will learn about the deletion "pre" any steps taken towards deletion. Describing this as a bug depends upon the characterization of pre_delete, because hypothetically the signal could offer either a weak or a strong contract with receivers. A strong promise to receivers would imply that they will get first crack at a model when Django learns that the model will be deleted. This is what I assumed pre_delete was for when I learned about Django's signals. This strong version of the promise would require that Django take no steps towards deleting the object before calling the signal (especially not permanent steps like caching related object for deletion.) This strong promise would result in the most versatile framework, because it allows for inversion of control. Inversion of control is particularly important in a plug-and-play framework like Django where many different applications come together and must coexist in a single project. On the contrary, pre_delete could also offer a weak promise of just letting receivers know that deletion is occurring, but restricting what receivers can do to modify the results of the deletion. This is what the signal currently does with respect to related models. (Although I don't think this restriction is intentional...maybe just an artifact of how it was originally coded up.) I hope that consensus among the developers will be that the strong version of the promise is the more robust development choice. This Bug is Distinct from Feature Requests The bug I've described is a design issue. When you combine it with Django's documented behavior of ON DELETE CASCADE, though, you get the common complaint that related models are deleted and there's nothing a pre_delete receiver can do about it. At least two features have been requested to address this issue. One is that Django should automatically set nullable foreign key fields to NULL whenever the keyed object is deleted. (#9308[2]. It says this is fixed but I'm not experiencing it.) Another is to implement several ON DELETE/ UPDATE behaviors that are set at field creation (#7539[3]). Even if both of these features were implemented, it would not change the fact that right now the pre_delete signal as-used only offers a weak promise to receivers, and that there may arise in the future another usage where this weak promise causes frustration to programmers. My Patch Modifies _collect_sub_objects to Notify Signal Receivers In order to notify receivers of the pre_delete signal (or any signal for that matter), we need to 1) be at a point where we have identified the model that needs to send the signal, and 2) be at a point in the code before we have taken steps to complete the deletion (or whatever action the signal is relevant to.) The best place for this is at the beginning of Model's _collect_sub_objects method. At the beginning of the method, self's related models have not been inspected to add them to seen_objs, but we have identified which model (namely self) will be the target of the action. Since _collect_sub_objects recurses along relations, it will have the opportunity to send the signal for each related model as soon as it recurses upon the related model. The only thing I don't like about my solution is that it piggybacks sending signals along with collecting related models rather than sending the signals just before the action is applied. Another alternative might be to traverse all of the related models first to send the signals, and then traverse them all again (those that remain related, that is.) But it would be inefficient to perform the same traversal twice. So I think sending the signal from within _collect_sub_objects is a good compromise. Another consideration is what alternatives there are to this solution. I found two. One alternative (#8168[4]) suggests adding another signal, e.g. pre_pre_delete. Personally I think one pre_delete signal is enough if only it were sent in the right place. If a pre_pre_delete signal were implemented, I don't know why anyone would use the current pre_delete signal. Another alternative (#6108[5]) suggests sending a list of the objects to-be-deleted along with the pre_delete signal and allowing the signal receiver to modify the list. There are some problems with that alternative, though. 1) one model may send the pre_delete signal to its receivers, but then another model in the same batch of to-be-deleted models sends the pre_delete signal to another receiver which removes the first model from the list and now even the weak promise of notification of deletion has been broken for the first model's receivers. This doesn't happen in my solution because the signal is sent before any related models are added to the cache of seen_objs, and so if a receiver removes related models, they won't send the signal. 2) Django seems to use the less-is-more approach to signals by just sending the basic information needed with a signal. #6108 seems to depart from that norm. Besides, it's possible for the receiver to reconstruct any information that hypothetically might be passed with the signal, so I say keep the protocol simple and let the receivers figure out what they need. Specifics on the Patch The patch adds an argument called "pre_signal" to _collect_sub_objects. The argument should be one of Django's signals from django.db.models.signals, and it will be called at the beginning of the method (if the model was not autocreated.) The argument is called pre_signal because although _collect_sub_objects is presently only used for finding related objects for deletion, it could possibly be used for some other purpose for which a different pre_xx signal would be sent. An alternative to sending a signal as an argument could be sending a callback. This could easily be used to send the signal (callback=lambda model: pre_delete.send(model.__class__, model)), but it might be overkill because maybe the only thing it would ever be used for would be to send a pre_delete signal. Anyhoo, I love using Django, I'm deeply indebted to all of you who have worked on it. Thank you. I'm sorry my introduction to the mailing list is a short novel, but I hope that this patch results in a useful addition to the framework. -Carl [0] http://code.djangoproject.com/attachment/ticket/6870/6870.pre_delete.r13248.diff [1] http://code.djangoproject.com/ticket/6870 [2] http://code.djangoproject.com/ticket/9308 [3] http://code.djangoproject.com/ticket/7539 [4] http://code.djangoproject.com/ticket/8168 [5] http://code.djangoproject.com/ticket/6108 -- You received this message because you are subscribed to the Google Groups "Django developers" group. To post to this group, send email to django-develop...@googlegroups.com. To unsubscribe from this group, send email to django-developers+unsubscr...@googlegroups.com. For more options, visit this group at http://groups.google.com/group/django-developers?hl=en.