3

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.

Granny Aching
  • 1,295
  • 12
  • 37
  • Are your sequences messed up at all? [Possible duplicate](https://stackoverflow.com/q/11089850/1324033) – Sayse May 02 '19 at 14:03
  • Have you try this way (although I'm not sure if it'd fix): `device.save(update_fields=['field'])` or instead you could use `device = DeviceAdmin.objects.filter(uuid=uuid).update(field=value)` – Higor Rossato May 02 '19 at 14:06
  • Sequences are fine. I checked. – Granny Aching May 02 '19 at 14:14
  • Your diagnosis is incorrect. The `updated` flag would only be False if no rows were matched, ie the PK does not exist. It wouldn't be False if the PK existed but the values were set to the same as they are now. – Daniel Roseman May 02 '19 at 14:54
  • I don't have a diagnosis. I'm only describing the symptoms. `updated` appeared to be `False` in debugger – Granny Aching May 03 '19 at 18:55
  • 1
    From your other question I know that you already have tables in the db. Can you show what happens if you use code generated by `manage.py inspectdb` for your model? And you can always fall-back to using `.save(force_update=True)` – imposeren May 10 '19 at 23:11

3 Answers3

0

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.

imposeren
  • 4,142
  • 1
  • 19
  • 27
  • for .save(force_update=True), if the DB returns the no change, will it still override it or it will not take any action behind the scene. – Ashish Kumar Oct 10 '20 at 21:29
0

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.

Jacopo
  • 562
  • 6
  • 10
0

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)
Cyrus
  • 166
  • 4