In Django, how can I prevent a "Save with update_fields did not affect any rows." error?
A comment by gachdavit suggested using select_for_update
. You could modify your get_article
function to call select_for_update
prior to fetching the article. By doing this, the database row holding the article will be locked as long as the current transaction does not commit or roll back. If another thread tries to delete the article at the same time, that thread will block until the lock is released. Effectively, the article won't be deleted until after you have called the save
function.
Unless you have special requirements, this is the approach I'd take.
I'm not aware of any special way to handle it other than to check to see if the values have changed.
article = update_model(article, {'label': label})
def update_model(instance, updates):
update_fields = {
field: value
for field, value in updates.items()
if getattr(instance, field) != value
}
if update_fields:
for field, value in update_fields.items():
setattr(instance, field, value)
instance.save(update_fields=update_fields.keys())
return instance
Edit: Another alternative would be to catch and handle the exception.
This is hacky, but you could override _do_update
in your model and simply return True
. Django itself does something kind of hacky on line 893 of _do_update
to suppress the same exception when update_fields
contains column names that do not appear in the model.
The return value from _do_update
triggers the exception you are seeing from this block
I tested the override below and it seemed to work. I feel somewhat dirty for overriding a private-ish method, but I think I will get over it.
def _do_update(self, base_qs, using, pk_val, values, update_fields, forced_update):
updated = super(Article, self)._do_update(base_qs, using, pk_val, values, update_fields, forced_update)
if not updated and Article.objects.filter(id=pk_val).count() == 0:
return True
return updated
This solution could be genericized and moved to a mixin base class if you need to handle this for more than one model.
I used this django management command to test
from django.core.management.base import BaseCommand
from foo.models import Article
class Command(BaseCommand):
def handle(self, *args, **kwargs):
Article.objects.update_or_create(id=1, defaults=dict(label='zulu'))
print('Testing _do_update hack')
article1 = Article.objects.get(id=1)
article1.label = 'yankee'
article2 = Article.objects.get(id=1)
article2.delete()
article1.save(update_fields=['label'])
print('Done. No exception raised')