4

I have a primary key ID CharField in my model Image, I want to create unique IDs for newly created objects. I try to achieve this by overriding the model's save method:

    def save(self, *args, **kwargs):
        if not self.pk: # is created
            self.id = uuid.uuid4().hex
            while Image.objects.filter(id=self.id).exists():
                self.id = uuid.uuid4().hex
        return super().save(*args,**kwargs)

The problem is, save() doesn't seem to be called when I create objects with Image.objects.create(), it is only called when I create the object with image=Image(...) and then call image.save(). As a result, the newly created object has no id assigned unless specified, which causes PostgreSQL to throw a non_unique primary key error.

How can I make sure that unique IDs are created upon calling Image.objects.create()?

Django version: 1.11.3

UPDATE: I realized that the overridden save() method was not called either. Turns out the problem was I was calling model's save method in a migration. As it is pointed out in this post custom model methods are not available in migrations. I will have to copy the model's save method to the migration file.

yam
  • 1,383
  • 3
  • 15
  • 34
  • 2
    The point of UUID (universally unique identifier) is that you can always assume that two UUIDs are distinct. You'd be better off setting your `id` field to have `default=uuid.uuid4` (no parentheses after the `uuid4`) and this will happen automatically on object creation. – Aaron Klein Mar 27 '19 at 14:01
  • I wish :( Database was designed before me and now it is very difficult to change the primary key field without losing data so this is not an option for me. – yam Mar 27 '19 at 14:05
  • by the [docs](https://docs.djangoproject.com/en/2.1/topics/db/queries/#creating-objects) `To create an object, instantiate it using keyword arguments to the model class, then call save() to save it to the database.` so please clarify why you think so? – Brown Bear Mar 27 '19 at 14:06
  • is you model `pk` name `id`? – Brown Bear Mar 27 '19 at 14:09
  • docs also say that `To create and save an object in a single step, use the create() method.` so I would expect this using `create()` cause a problem. Besides, code base is big and there are different ways that create objects (e.g. `get_or_create()`). I don't want to go through the whole code base and hunt down these methods to see whether they use `create()` or `save()`. – yam Mar 27 '19 at 14:26
  • yes, model `pk` name is `id` – yam Mar 27 '19 at 14:27

2 Answers2

3

This can't be done in general. In between your if statement checking that the ID doesn't exist yet and you setting it, something else could add a new row with that ID. Which is why other solutions are used -- an auto-incrementing ID that the database ensures is unique, or a UUID that has a really tiny chance of being unique.

Luckily, you use one those. With UUIDs the custom is to just assume that they are unique.

The way to do it is to set a function returning the unique ID as the field's default:

def uuid_hex():
    return uuid.uuid4().hex

class YourModel(models.Model):
    id = CharField(unique=True, primary_key=True, default=uuid_hex, null=False)
RemcoGerlich
  • 30,470
  • 6
  • 61
  • 79
  • Forgot that the default has to be an actual function, not a lambda. Does this work? – RemcoGerlich Mar 27 '19 at 14:57
  • I think it does. The problem was that I was trying to do this in a migration (see my update). – yam Mar 27 '19 at 15:27
  • 1
    @yam: ah yes, in a migration you don't get the real model classes, you get something model-like that has the same fields as the models had at the time the migrations were made. – RemcoGerlich Mar 27 '19 at 20:28
0

You should call the real save method while overriding the model save method:

def save(self, *args, **kwargs):
    if not self.pk: # is created
        self.id = uuid.uuid4().hex
        while Image.objects.filter(id=self.id).exists():
            self.id = uuid.uuid4().hex
    super(Image, self).save(*args, **kwargs)

Check docs: Overriding predefined model methods

Also it's not a good idea to override the default id. If you need another unique field to be used as something like an ID then i recommend adding another field alongside the default id field.

Navid Zarepak
  • 4,148
  • 1
  • 12
  • 26
  • I'm sorry, I'd forgotten to add that part. I do call the save method. Updating my post. – yam Mar 27 '19 at 14:03
  • Oh, okay then, but still i don't think this is a good idea. Assigning id in django level is not a good idea. let the main id be created by database which is what it does best and add another field. – Navid Zarepak Mar 27 '19 at 14:07
  • I agree. Unfortunately the database was designed this way by someone else and it turns out to be very difficult (if not impossible) to change the ID field into a self-incrementing integer field without breaking M2M relationships with Django migrations. So this is the only other solution I could think of. – yam Mar 27 '19 at 14:13
  • 1
    I see. I would've tried to convert the data to a new structure that fits better with both django and new technologies if the amount of data isn't huge but still i don't have enough information to actually recommend that. So best of lucks ;) – Navid Zarepak Mar 27 '19 at 14:31