13

We're migrating and making necessary changes to our Oracle database, one major change is that we're adding an UUIDField as primary_key to all models(hidden to the client), and(trying to add) a regular AutoField.

We found that displaying the primary_key directly to our clients wasn't good design, but they also requested an ID field displayed to reference objects more easily, but Django limits this by not allowing AutoField to NOT be the primary_key

Is there a workaround for this issue?

Mojimi
  • 2,561
  • 9
  • 52
  • 116
  • Though there are two good answers here, here is another potential solution here:https://stackoverflow.com/questions/37709163/how-can-i-support-autofieldprimary-key-false-in-django – Anshuman Kumar Jul 30 '20 at 04:23

3 Answers3

14

What I think could work is using an IntegerField (pretty much what an AutoField uses under the hood), and increment that on the model's first save (before it's ever put into the database).

I wrote an example model to show this below.

from django.db import models

class MyModel(models.Model):

    # This is what you would increment on save
    # Default this to one as a starting point
    display_id = models.IntegerField(default=1)

    # Rest of your model data

    def save(self, *args, **kwargs):
        # This means that the model isn't saved to the database yet
        if self._state.adding:
            # Get the maximum display_id value from the database
            last_id = self.objects.all().aggregate(largest=models.Max('display_id'))['largest']

            # aggregate can return None! Check it first.
            # If it isn't none, just use the last ID specified (which should be the greatest) and add one to it
            if last_id is not None:
                self.display_id = last_id + 1

        super(MyModel, self).save(*args, **kwargs)

This, in theory, just replicates what AutoField does, just with a different model field.

tinfoilboy
  • 926
  • 10
  • 17
  • 1
    What makes it trigger only on the model's first save? – Mojimi Dec 20 '16 at 16:43
  • 1
    @Mojimi, I just updated my answer because the other one was buggy in a way. Checking if a primary key doesn't exist yet works on a `AutoField` primary key, but not a `UUIDField`. But in Django itself, it contains a `_state` with the `adding` field. This returns `True` if this object is being created and not updated, which would mean that the model is first being saved/created. Apologies about that :) – tinfoilboy Dec 21 '16 at 00:25
  • 1
    After some time, I was thinking if this could be done in another way, by assigning the default value to a function that pretty much has the same code as yours above, and using lambda to pass the model as parameter, this would remove the need to overwrite the save method of every model – Mojimi Jan 20 '17 at 17:43
  • @Mojimi Yes, that would work too. At that point you'd just have to set the default for that field to as you said, a function with largely the same code. – tinfoilboy Jan 23 '17 at 05:06
  • 8
    @Tinfoilboy this is not the best approach as doesn't work in the following conditions. 1. If you add two items concurrently, and it's done in a transaction, you'll end up with both items seeing the same Max and getting the same number. 2. If you deleted the last item, you'll have the same number for the next item (which might be or might not be the thing you'd need) 3. It's quite wasteful to perform aggregate requests over the whole table. A better solution would be to create a dedicated Counter model, or look at the DBMS's native sequence support, if any. – Yuri Shatrov Jan 11 '19 at 14:41
  • agree with @YuriShatrov , this definitely can cause a race condition. – GodBlessYou Apr 07 '21 at 16:03
  • This will likely cause race conditions, especially with concurrent transactions. – Ryan Oct 20 '21 at 20:35
3

Assuming there is no sequence support in the chosen DBMS, a solution is to create a model:

class Counter(models.Model):
    count = models.PositiveIntegerField(default=0)

    @classmethod
    def get_next(cls):
        with transaction.atomic():
            cls.objects.update(count=models.F('count') + 1)
            return cls.objects.values_list('count', flat=True)[0]

and create one instance of it in a data migration. This could have some implications if you're using transaction management, but it's (if your DBMS supports transactions) guaranteed to always return the next number, regardless of how many objects have been there at the start of a transaction and whether any had been deleted.

phihag
  • 278,196
  • 72
  • 453
  • 469
Yuri Shatrov
  • 628
  • 6
  • 9
  • @phihag If your DBMS doesn't support transactions (like SQLite or MySQL with some engines), you might experience this issue, that's why I said : "it's (**almost**) guaranteed". – Yuri Shatrov Jul 17 '20 at 19:10
  • Oh, my bad. You are correct; updating the database row. My previous statement was nonsense. Is my edit correct? – phihag Jul 18 '20 at 22:04
  • 1
    @phihag Not quite. If you delete the first object in the counter table which is used for counting, you obviously lose the sequence. I don't see any benefit of adding other objects to the table, either. – Yuri Shatrov Jul 20 '20 at 05:46
-4

You can also using count as auto increment. In my project I'm using like this.

def ids():
    no = Employee.objects.count()
    if no == None:
        return 1
    else:
        return no + 1
emp_id = models.IntegerField(('Code'), default=ids, unique=True, editable=False)
  • How would this go with deleting an employee? the new count would be the same as the last employee's id. – Yuri Shatrov Jan 11 '19 at 14:43
  • Ahh!... Yes sorry my bad. if so i got another solution. We can use (aggergate) def ids(): no = Employee.objects.all().aggregate(largest=models.Max('emp_id'))['largest'] print("Object",no) if no == None: return 1 else: return no + 1 emp_id = models.IntegerField(('Code'), default=ids, unique=True, editable=False) – Kyaw Zaw Tun Jan 12 '19 at 18:48
  • Not only that but it would be incredibly slow for any sizable database! – boatcoder Jul 26 '20 at 04:17
  • DO NOT DO THIS. 1) Its not safe. If you have high trafic, you could end up with multiple writes trying to claim that field. 2) It cant handle key recycling, 3) ETC. Your database has built in serial key generation for a reason. Dont reimplement, it wont work. – Shayne Aug 27 '22 at 03:36