In Django, how can I prevent a "Save with update_fields did not affect any rows." error?
Asked Answered
T

4

19

I'm using Django and Python 3.7. I have this code

article = get_article(id)
...
article.label = label
article.save(update_fields=["label"])

Sometimes I get the following error on my "save" line ...

    raise DatabaseError("Save with update_fields did not affect any rows.")
django.db.utils.DatabaseError: Save with update_fields did not affect any rows.

Evidently, in the "..." another thread may be deleting my article. Is there another way to rewrite my "article.save(...)" statement such that if the object no longer exists I can ignore any error being thrown?

Total answered 22/5, 2019 at 14:22 Comment(3)
Have a look docs.djangoproject.com/en/2.1/ref/models/querysets/…Calenture
You can catch the DatabaseError and check whether article.refresh_from_db() throws Article.DoesNotExist or not to verify the object has been deleted.Croatian
Are you shore that article.label != label ? seems that before and after save data is sameOpenwork
M
18

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.

Mcgriff answered 25/5, 2019 at 19:23 Comment(0)
A
2

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.

Atonement answered 22/5, 2019 at 14:30 Comment(2)
Hey I tried putting in yoru function but discovered I'm still getting the same error.Total
I don't think there's a bug in it. Did you try debugging what's running to see what the values are?Atonement
D
2

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')
Drupe answered 30/5, 2019 at 22:23 Comment(0)
B
0

In my case, I use a Primary instance and a Read Replica for Postgres. When I removed the read replica, I was able to log in successfully.

from django.conf import settings

class PrimaryReplicaRouter:
    def db_for_read(self, model, **hints):
        return 'read_replica' if 'read_replica' in settings.DATABASES else 'default'

    def db_for_write(self, model, **hints):
        return 'default'

    def allow_relation(self, obj1, obj2, **hints):
        return True

    def allow_migrate(self, db, app_label, model_name=None, **hints):
        return db == 'default'

Boric answered 13/7 at 15:32 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.