'Django tries to INSERT instead of UPDATE on save()

I have a model

class DeviceAdmin(models.Model):
    dev_id = models.AutoField(db_column='dev_id', primary_key=True)
...

A view locates an object:

device = DeviceAdmin.objects.get(uuid=uuid)

then it makes some changes (or maybe not)

if (...):
    device.os_type = 'windows'
...

then it tries to save any changes:

device.save()

Here's where Django attempts to insert anther row instead of updating, causing a DB error

django.db.utils.IntegrityError: duplicate key value violates unique constraint 

There's a kind of similar question with solution:

Once I set the primary_key field to an AutoField, the issue went away.

However, my primary key is already an AutoField.

So I stepped through django code with debugger, and fund this in file ...site-packages\django\models\base.py:

    # If possible, try an UPDATE. If that doesn't update anything, do an INSERT.
    if pk_set and not force_insert:
        base_qs = cls._base_manager.using(using)
        values = [(f, None, (getattr(self, f.attname) if raw else f.pre_save(self, False)))
                  for f in non_pks]
        forced_update = update_fields or force_update
        updated = self._do_update(base_qs, using, pk_val, values, update_fields,
                                  forced_update)
        if force_update and not updated:
            raise DatabaseError("Forced update did not affect any rows.")
        if update_fields and not updated:
            raise DatabaseError("Save with update_fields did not affect any rows.")
    if not updated:
        if meta.order_with_respect_to:
            # If this is a model with an order_with_respect_to
            # autopopulate the _order field
            field = meta.order_with_respect_to
            filter_args = field.get_filter_kwargs_for_object(self)
            order_value = cls._base_manager.using(using).filter(**filter_args).count()
            self._order = order_value

        fields = meta.local_concrete_fields
        if not pk_set:
            fields = [f for f in fields if f is not meta.auto_field]

        update_pk = meta.auto_field and not pk_set
        result = self._do_insert(cls._base_manager, using, fields, update_pk, raw)
        if update_pk:
            setattr(self, meta.pk.attname, result)
    return updated

It seems that if there are no changes (and thus update doesn't do anything), then it will try to insert a duplicate record.

I tested this with adding a save right after get:

device = DeviceAdmin.objects.get(uuid=uuid)
device.save()

and it's trying to insert a duplicate.

Does this mean, my code needs to keep track if an object has to be saved to DB or not?

What's the simplest way to get around this problem?


UPDATE:

As a test, I put a save immediately after finding an object with get, without any changes:

device = DeviceAdmin.objects.get(uuid=uuid)
device.save()

Django should know that the object is an existing one. And yet, it tries to insert a duplicate.

Similarly, creating a new object and calling save twice:

device = DeviceAdmin(...) # create a new object (it has null `dev_id`)
device.save()             # insert the new object into DB; get 'dev_id'
                          #   for the newly inserted record
device.save()             # This shouldn't do anything!

This second save attempts to insert a duplicate - as I can see if I step through the django code with debugger.



Solution 1:[1]

You certainly have some issues with fields definition (maybe for some reason you should add unique=True). But if you still can't determine why UPDATE fails (INSERT happens if UPDATE returns "no rows updated"), then you can use .save(force_update=True) as a last resort.

Also you have some inconsistency in your code:

  • field is named as dev_id but you are using uuid in filtering (.get(uuid=uuid)). Are you sure that you have properly set a name for primary_key field?
  • AutoField is assuming the use of Integer field, so during get_prep_value it does int(self.pk) which works without throwing any errors on instances of uuid.UUID, But if internally db uses varchar field, then it will be converted back to str with a wrong value (e.g. str(int(uuid.UUID('579aea21-49a4-4819-90ec-a192014b4a44'))) == '116447198070669671892400511866020514372'). So that's what might be causing the problems. Instead of AutoField try using UUIDField or CharField:
import uuid


class SomeModel(models.Model): 

    dev_id = models.UUIDField(primary_key=True, blank=True, default=uuid.uuid4)

# Or if your column is a varchar:

class SomeModel(models.Model): 

    def dev_id_default():
        return uuid.uuid4().hex  # or str(uuid.uuid4()) if you need values with `-`

    dev_id = models.CharField(primary_key=True, blank=True, default=dev_id_default, max_length=33_OR_36)

Looks like you are using pre-existing tables in DB: try using code for models generated by python manage.py inspectdb.

Solution 2:[2]

In my case, I had a unique CharField at the top of my class. It seemed to me that Django would sometimes see it as the primary key, sometimes not -- with the consequence of sometimes not realizing it should be updating instead of inserting.

Adding an explicit id=AutoField(primary_key=True) fixed the problem for me.

Solution 3:[3]

One reason why this might occur is if your primary key field is defined with a default value. I don't see a default in your example, but I also don't see that the datatype is a uuid, so I thought I would provide this answer just in case.

If your model's pkey looks like this: models.UUIDField(primary_key=True, default=uuid.uuid4), calling save will always result in an INSERT rather than an UPDATE. I believe this is true even if you pass force_update=True to the save method. This was at one point submitted as a bug, but is actually the intended behavior of Django 3 according to the docs.

There are a couple of ways you can handle this issue if you want to use a uuid as your primary key. The best way is probably to follow the recommendation of the docs and use either the update or update_or_create methods to update your model instances.

>>> MyModel.objects.update_or_create(pk=existing_pk, defaults={'name': 'new name'})
>>> MyModel.objects.filter(pk=existing_pk).update(name='new name')

Alternatively, you could also remove the default argument from your primary key definition and ensure that each instance has a uuid assigned on save by overriding your model's save method - for example:

def save(self, *args, **kwargs):
    if not self.id:
        self.id = uuid.uuid1()
        super(DeviceAdmin, self).save(*args, **kwargs)

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2 Jacopo
Solution 3 Cyrus