9

I need to add an autoinc field that is not the primary key. I am in the process of migrating a very large production database that uses autoincrementing fields to models.UUIDField. I have been doing a piecewise migration, and all of my relationships are now duplicated with both field types. I'm ready to make the primary key swap, but unfortunately I still need to keep the auto incrementing integer field for old clients as it becomes deprecated.

Since django will not allow me to define an autofield with primary_key=False (even though this is fully supported at the db layer), i'm looking for a simple solution. My initial strategy would be to simply change the field to models.BigIntegerField('GUID', db_index=True, null=True, unique=True) and then manually set the default nextval('my_guid_seq'::regclass) using migrations.RunSQL. So far so good, except not. It turns out, because of my null=True declaration, django at the ORM layer is taking over and inserting null which will not allow defaults at the database layer to do it's job.

The core developers are fast to reject this request because of bad design, which I most definetly agree with, but there are very valid use cases such as this. https://code.djangoproject.com/ticket/8576

I am a very weak django developer so I don't want to get in the weeds metaprogramming at the ORM layer. This is by definition a hack, so i'm looking for the least complex, creative solution that gets me around this limitation

Ryan Romanchuk
  • 10,819
  • 6
  • 37
  • 41
  • It looks from 'my_guid_seq'::regclass that you are using postgresql, you can use postgreql sequences by editing the migration. But if you update your question and mention your real objective, you migt get a better answer. – e4c5 Jun 09 '16 at 06:05
  • @e4c5 i was a little afraid to tag postgres, because that part is working perfectly fine and I didn't want to distract the postgres community. Unfortunately my problem lies all the way up the stack with django's ORM. – Ryan Romanchuk Jun 09 '16 at 06:33
  • Right so if you could explain the objective of this excercise we might find a way – e4c5 Jun 09 '16 at 06:41
  • My objective is to demote my autoinc primary keys and to use my UUID fields as primary keys. I am not removing the autoinc fields, just demoting them. Yes, all fks are updated, and yes all relationships are being maintained by UUIDs and autoincs – Ryan Romanchuk Jun 09 '16 at 17:28
  • My objective is in the very first sentence. Are you asking me *why* i'm switching from sequenced big integers to UUIDs? – Ryan Romanchuk Jun 09 '16 at 17:36

3 Answers3

6

You could subclass AutoField and override the _check_primary_key() and the deconstruct() methods.

from django.db.models.fields import AutoField
from django.db.models.fields import checks


class AutoFieldNonPrimary(AutoField):

    def _check_primary_key(self):
        if self.primary_key:
            return [
                checks.Error(
                    "AutoFieldNonPrimary must not set primary_key=True.",
                    obj=self,
                    id="fields.E100",
                )
            ]
        else:
            return []

    def deconstruct(self):
        name, path, args, kwargs = super().deconstruct()
        kwargs["primary_key"] = False
        return name, path, args, kwargs

See AutoField source code here.

hashlash
  • 897
  • 8
  • 19
roob
  • 2,419
  • 3
  • 29
  • 45
  • 4
    That error check is there for a reason, and bypassing it is likely to cause problems. I am not familiar enough with the internals of the Django ORM to predict what the consequences might be, but I would look for another workaround. – evergreen Jun 08 '16 at 19:34
  • 8
    Without an example of what a consequence might be, I don't think that's enough to discredit his answer. There is nothing stopping AUTO INCREMENT functionality being non-primary keyed at the database layer, so I couldn't foresee an issue. – Adam Barnes Jul 24 '18 at 08:10
  • 5
    After overriding, the errors stops for makemigrations, but when you actually try to migrate, we will get a error. When you output using sqlmigrate, you can see even the AutoFieldNonPrimary column would be marked as a PRIMARY, hence creating two primary keys are not allowed at the database level! – user3785966 Jan 22 '20 at 07:03
  • That happened to me once, but I just did a hacky solution of tweaking the query. Terrible fix but it had to be done. – Anshuman Kumar Jul 30 '20 at 05:36
  • Unexpected consequence: migrations does not work anymore on a model with that field. Considering an `AddField` operation with `field=AutoFieldNonPrimary(primary_key=False)`, you will get a `multiple primary keys for table 'foo' are not allowed`. Seems @evergreen concerns were legitimate. – David Dahan Aug 16 '22 at 14:19
  • @user3785966 @David Dahan You'll also need to override the `deconstruct` method, which will be used to generate the migration https://docs.djangoproject.com/en/4.2/howto/custom-model-fields/#field-deconstruction – hashlash Jul 04 '23 at 09:11
3

I know, changing the primary key to UUID is such a pain.Hence the simple and better solution that I think of is to add another integer field that is auto-incrementing in nature.

Here is my solution:

class ModelName(models.Model):
    auto_inc_id = models.IntegerField()

Then override the save model:

def save(self, *args, **kwargs):
    self.object_list = ModelName.objects.order_by('auto_inc_id')
    if len(self.object_list) == 0:  # if there are no objects
        self.auto_inc_id = 1
    else:
        self.auto_inc_id = self.object_list.last().auto_inc_id + 1
    super(ModelName, self).save()
 
Headmaster
  • 2,008
  • 4
  • 24
  • 51
Abhimanyu
  • 725
  • 1
  • 6
  • 20
  • 3
    I think `auto_inc_id + 1` can lead to a race condition if too many calls to save(). – GodBlessYou Apr 07 '21 at 15:48
  • you mean override the save *function*? you could have shown it properly how overriding a model works, instead of posting the inner parts – Conor Aug 15 '22 at 10:31
-2

Couldn't format this as a comment, but modifying @Abhimanyu's answer to make the save method more concise (and issue only one query). Same model property:

class ModelName(models.Model):
    auto_inc_id = models.IntegerField()

And here's the save method on the model:

def save(self, *args, **kwargs):
    self.auto_inc_id = ModelName.objects.all().count() + 1
    super(ModelName, self).save()
Nic
  • 168
  • 4