Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Generating a non-sequential ID/PK for a Django Model

I'm on the cusp of starting work on a new webapp. Part of this will give users pages that they can customise in a one to many relationship. These pages naturally need to have unique URLs.

Left to its own devices, Django would normally assign a standard AUTOINCREMENT ID to a model. While this works fantastically, it doesn't look great and it also makes pages very predictable (something that isn't desired in this case).

Rather than 1, 2, 3, 4 I would like set-length, randomly generated alphanumeric strings (eg h2esj4). 6 spots of a possible set of 36 characters should give me over two billion combinations which should be more than enough at this stage. Of course if I could expand this at a later time, that would be good too.

But there are two issues:

  1. Random strings occasionally spell out bad words or other offensive phrases. Is there a decent way to sidestep that? To be fair I could probably settle for a numeric string but it does have a hefty hit on the likelihood of clashes.

  2. How do I get Django (or the database) to do the heavy lifting on insert? I'd rather not insert and then work out the key (as that wouldn't be much of a key). I assume there are concurrency issues to be aware of too though if two new pages were generated at the same time and the second (against all odds) magically got the same key as the first before the first was committed.

I don't see this being a million miles different from how URL shorteners generate their IDs. If there's a decent Django implementation of one, I could piggyback off that.

like image 891
Oli Avatar asked Sep 21 '10 09:09

Oli


People also ask

What is the difference between ID and PK in Django?

pk is the attribute that contains the value of the primary key for the model. id is the name of the field created as a primary key by default if none is explicitly specified.

Does Django automatically create ID?

Django will create or use an autoincrement column named id by default, which is the same as your legacy column.

What is primary key in Django models?

Primary Keys By default, Django adds an id field to each model, which is used as the primary key for that model. You can create your own primary key field by adding the keyword arg primary_key=True to a field. If you add your own primary key field, the automatic one will not be added.

What is UUID Django?

UUIDField is a special field to store universally unique identifiers. It uses Python's UUID class. UUID, Universal Unique Identifier, is a python library that helps in generating random objects of 128 bits as ids.


3 Answers

There is built-in Django way to achieve what you want. Add a field to the model of "custom page" with primary_key=True and default= name of key generation function, like this:

class CustomPage(models.Model):
    ...
    mykey = models.CharField(max_length=6, primary_key=True, default=pkgen)
    ...

Now, for every model instance page, page.pk becomes an alias for page.mykey, which is being auto-assigned with the string returned by your function pkgen() at the moment of creation of that instance.
Fast&dirty implementation:

def pkgen():
    from base64 import b32encode
    from hashlib import sha1
    from random import random
    rude = ('lol',)
    bad_pk = True
    while bad_pk:
        pk = b32encode(sha1(str(random())).digest()).lower()[:6]
        bad_pk = False
        for rw in rude:
            if pk.find(rw) >= 0: bad_pk = True
    return pk

The probability of two pages getting identical primary keys is very low (assuming random() is random enough), and there are no concurrency issues. And, of couse, this method is easilly extensible by slicing more chars from encoded string.

like image 81
atomizer Avatar answered Oct 16 '22 18:10

atomizer


Here's what I ended up doing. I made an abstract model. My use-case for this is needing several models that generate their own, random slugs.

A slug looks like AA##AA so that's 52x52x10x10x52x52 = 731,161,600 combinations. Probably a thousand times more than I'll need and if that's ever an issue, I can add a letter for 52 times more combinations.

Use of the default argument wouldn't cut it as the abstract model needs to check for slug collisions on the child. Inheritance was the easiest, possibly only way of doing that.

from django.db import models
from django.contrib.auth.models import User

import string, random

class SluggedModel(models.Model):
    slug = models.SlugField(primary_key=True, unique=True, editable=False, blank=True)

    def save(self, *args, **kwargs):
        while not self.slug:
            newslug = ''.join([
                random.sample(string.letters, 2),
                random.sample(string.digits, 2),
                random.sample(string.letters, 2),
            ])

            if not self.objects.filter(pk=newslug).exists():
                self.slug = newslug

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

    class Meta:
        abstract = True
like image 10
Oli Avatar answered Oct 16 '22 18:10

Oli


Django now includes an UUIDField type, so you don't need any custom code or the external package Srikanth Chundi suggested. This implementation uses HEX strings with dashes, so the text is pretty child-safe, other than 1337 expressions like abad1d3a :)

You would use it like this to alias pk to the uuid field as a primary key:

import uuid
from django.db import models

class MyModel(models.Model):
    uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    # other fields

Note, however, that when you're routing to this view in urls.py, you need a different regex as mentioned here, e.g.:

urlpatterns = [
    url(r'mymodel/(?P<pk>[^/]+)/$', MyModelDetailView.as_view(),
        name='mymodel'),
]
like image 8
metakermit Avatar answered Oct 16 '22 19:10

metakermit