Mojimi
Mojimi

Reputation: 3171

Django non primary_key AutoField

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?

Upvotes: 13

Views: 17772

Answers (5)

Quantum-script
Quantum-script

Reputation: 142

First I recommend you for performance reasons don't use uuid as primary key if you are using SQL base database second you can use BigAutofield() instead of normal auto field:

import uuid
from django.db import models


class MyModel(models.Model):
    pkid = models.BigAutoField(primary_key=True, editable=False)
    id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True)

Upvotes: 0

Tushar patel
Tushar patel

Reputation: 1

You can make a new field with serial as serial makes sequence automatically in postgres.

from django.db import connection
from django.db.models import BigIntegerField


class AutoIncrementalField(BigIntegerField):
    MAX_BIGINT = 9223372036854775807

    def __init__(self, *args, **kwargs):
        if kwargs.get("null"):
            raise ValueError("AutoIncrementalField cannot be null")
        if kwargs.get("default"):
            raise ValueError("AutoIncrementalField cannot have a default value")
        kwargs["editable"] = False
        super().__init__(*args, **kwargs)

    def formfield(self, **kwargs):
        return super().formfield(
            **{
                "min_value": 0,
                "max_value": BigIntegerField.MAX_BIGINT,
                **kwargs,
            }
        )

    def db_type(self, connection):
        return "bigserial"

    def pre_save(self, model_instance, add):
        if getattr(model_instance, self.attname) is None:
            app_name = model_instance._meta.app_label
            model_name = model_instance._meta.model_name
            attname = self.attname
            sequence_name = (
                f"{connection.schema_name}.{app_name}_{model_name}_{attname}_seq"
            )
            with connection.cursor() as cursor:
                cursor.execute(f"SELECT nextval('{sequence_name}')")
                sequence_value = cursor.fetchone()[0]
                setattr(model_instance, self.attname, sequence_value)
                return sequence_value
        return super().pre_save(model_instance, add)

    def deconstruct(self):
        name, path, args, kwargs = super().deconstruct()
        if "null" in kwargs:
            del kwargs["null"]
        if "blank" in kwargs:
            del kwargs["blank"]
        if "default" in kwargs:
            del kwargs["default"]
        return name, path, args, kwargs

Now use this field in your model.

from django.db import models
class Book(models.Model):
    name = models.CharField(max_length=100, null=False, blank=False)
    serial_number = AutoIncrementalField()

This will auto increment with uniqueness, until and unless you make changes to sequence in db manually.

Upvotes: 0

Yuri Shatrov
Yuri Shatrov

Reputation: 648

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.

Upvotes: 3

Kyaw Zaw Tun
Kyaw Zaw Tun

Reputation: 43

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)

Upvotes: -4

Maxwell
Maxwell

Reputation: 916

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.

Upvotes: 15

Related Questions