I'm having issues with ManytoMany Relationships that are not updating
in a model when I save it (via the admin) and try to use the new value within a
function attached to the post_save
signal or within the save_model
of
the associated AdminModel
.
I've tried to reload the object within those functions by using the
get function with the id.. but it still has the old values.
Is this a transaction issue? Is there a signal thrown when the transaction ends?
Thanks,
When you save a model via admin forms it's not an atomic transaction. The main object gets saved first (to make sure it has a PK), then the M2M is cleared and the new values set to whatever came out of the form. So if you are in the save() of the main object you are in a window of opportunity where the M2M hasn't been updated yet. In fact, if you try to do something to the M2M, the change will get wiped out by the clear(). I ran into this about a year ago.
The code has changed somewhat from the pre-ORM refactor days, but it boils down to code in
django.db.models.fields.ManyRelatedObjectsDescriptor
andReverseManyRelatedObjectsDescriptor
. Look at their __set__() methods and you'll seemanager.clear(); manager.add(*value)
That clear() complete cleans out any M2M references for the current main object in that table. The add() then sets the new values.So to answer your question: yes, this is a transaction issue.
Is there a signal thrown when the transaction ends? Nothing official, but read on:
There was a related thread a few months ago and MonkeyPatching was one method proposed. Grégoire posted a MonkeyPatch for this. I haven't tried it, but it looks like it should work.
You can find more informations in this thread : Django manytomany signals?
I have a general solution to this that seems a bit cleaner than monkey-patching the core or even using celery (although I'm sure someone could find areas where it fails). Basically I add a clean() method in the admin for the form that has the m2m relationships, and set the instance relations to the cleaned_data version. This make the correct data available to the instance's save method, even though it's not "on the books" yet. Try it and see how it goes:
See http://gterzian.github.io/Django-Cookbook/signals/2013/09/07/manipulating-m2m-with-signals.html
problem: When you manipulate the m2m of a model within a post or pre_save signal receiver, your changes get wiped out in the subsequent 'clearing' of the m2m by Django.
solution: In you post or pre_save signal handler, register another handler to the m2m_changed signal on the m2m intermediary model of the model whose m2m you want to update.
Please note that this second handler will receive several m2m_changed signals, and it is key to test for the value of the 'action' arguments passed along with them.
Within this second handler, check for the 'post_clear' action. When you receive a signal with the post_clear action, the m2m has been cleared by Django and you have a chance to successfully manipulate it.
an example:
see https://docs.djangoproject.com/en/1.5/ref/signals/#m2m-changed
When you are trying to access the ManyToMany fields in the post_save signal of the model, the related objects have already been removed and will not be added again until after the signal is finished.
To access this data you have to tie into the save_related method in your ModelAdmin. Unfortunately you'll also have to include the code in the post_save signal for non-admin requests that require customization.
see: https://docs.djangoproject.com/en/1.7/ref/contrib/admin/#django.contrib.admin.ModelAdmin.save_related
Example:
Then in your signals you can make the same changes that you want to execute on a save:
One of the solutions to update m2m, along with updating one of your models.
Django 1.11 and higher
First of all, all requests via admin panel are atomic. You can look at ModelAdmin:
The behavior which you can observe during updating, when changes which you made with m2m records were not saved, even after you made them in a save method one of your models or in a signal, happens only because m2m form rewrites all records after the main object is updated.
This is why, step by step:
The main object is updated.
Your code(in a save method or in a signal) made changes (you can look at them, just put a breakpoint in ModelAdmin):
Unfortunately, the downside of this solution - your changes with m2m will be executed in a separate transaction.