Django's ORM and auto incrementing non-primary key fields

I'm writing a to-do list, which will be ultimately become draggable and droppable. I'll need some way to maintain the ordering resulting, and an autoincrementing integer would be the simplest way of creating this. Space the increments far enough apart, and then you can reorder items a reasonable amount of times before having to update more than one row to do so.

(There's probably some algorithm to determine the optimal spacing for this kinda thing for N items being reordered X times... wish I knew enough about algorithms to figure out which one, I imagine it operates along the same lines as algorithms minimizing hash collisions).

Django's AutoField

Django has an AutoField, which seems like the simple approach. Of course, if it was, I wouldn't be writing this. AutoField itself presents two problems. The first is this:

class AutoField(Field):
    empty_strings_allowed = False
    def __init__(self, *args, **kwargs):
        assert kwargs.get('primary_key', False) is True, "%ss must have primary_key=True." % self.__class__.__name__
        kwargs['blank'] = True
        Field.__init__(self, *args, **kwargs)

So firstly, AutoField's constructor requires that it be passed the argument to create it as the primary key. Then there's this, another method of AutoField:

def contribute_to_class(self, cls, name):
        assert not cls._meta.has_auto_field, "A model can't have more than one AutoField."
        super(AutoField, self).contribute_to_class(cls, name)
        cls._meta.has_auto_field = True
        cls._meta.auto_field = self

Which means that a model is only allowed one AutoField. These restrictions seem to relate to keeping Django working on all RDBMS backends - has_auto_field and auto_field seem to be used mainly in the lower levels of the ORM, in functions like sql_model_create (in django.core.management.sql).

Custom models.Field

This is another option - I think... I envisage writing an autoincrementing column with a specifiable increment for SQLite would involve a hefty amount of triggers. But at this stage, I honestly don't have the knowledge of Django's ORM to do this, or the time to develop that knowledge.

Amending the table schema directly

This would be the simplest way to fix it, create a model.IntegerField in Django and then fire up the MySQL client and alter the column to autoincrement, but it's not portable. For starters, I develop on SQLite locally, so it wouldn't (easily) work on my dev machine, and secondly, every time I reset the app, I'd have to change the schema again - I tend to reset the app quite often when changing the model while playing, as it's simpler than doing the alter tables, as the data being stored is usually gibberish.

Do it in the model itself

So ultimately, I decided on overriding the save method of the model that needed the ordering field, and handling it there to keep my model portable, and the simplest way to do this was to use one of Django's new abstract base classes:

class OrderedEntity(models.Model):
    orderingField = models.IntegerField()
    orderingIncrement = 5
    def save(self):
        #Only need to populate orderingField if entity hasn't been saved yet
        #If it's been saved, it'll have a primary key.
        if not self.pk:
            try:
                max = self.__class__.objects.order_by("-orderingField")[0].orderingField
                self.orderingField = max + self.orderingIncrement
            except IndexError:
                #No objects yet exist
                self.orderingField = 0 
        #Call actual save method to finish populating DB
        super(OrderedEntity, self).save()

class Meta: abstract = True

Which can be used as follows - note that I'm overriding the ordering increment:

class ListItem(OrderedEntity):
    description = models.CharField(max_length = 100)
    completionDate = models.DateField(null = True, blank = True)
    todoList = models.ForeignKey(TodoList)
orderingIncrement = 20

Of course, this is just the model representation, the interesting logic will come when it's time to reorder the items, but it'll be useful any other time I need an autoincrementing field in Django that isn't a primary key.

Published: 4th July, 2008