[Fixed]-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)
    return instance

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)

        print('Done. No exception raised')

Leave a comment