Generate a random alphanumeric string as a primary key for a model
Question:
I would like a model to generate automatically a random alphanumeric string as its primary key when I create a new instance of it.
example:
from django.db import models
class MyTemporaryObject(models.Model):
id = AutoGenStringField(lenght=16, primary_key=True)
some_filed = ...
some_other_field = ...
in my mind the key should look something like this “Ay3kJaBdGfcadZdao03293”. It’s for very temporary use. In case of collision I would like it Django to try a new key.
I was wondering if there was already something out there, or a very simple solution I am not seeing (I am fairly new to python and Django). Otherwise I was thinking to do my own version of models.AutoField
, would that be the right approach?
I have already found how to generate the key here, so it’s not about the string generation. I would just like to have it work seamlessly with a simple Django service without adding too much complexity to the code.
EDIT:
Possible solution? What do you think?
id = models.CharField(unique=True, primary_key=True, default=StringKeyGenerator(), editable=False)
with
class StringKeyGenerator(object):
def __init__(self, len=16):
self.lenght = len
def __call__(self):
return ''.join(random.choice(string.letters + string.digits) for x in range(self.lenght))
I came up with it after going through the Django documentation one more time.
Answers:
One of the simplest way to generate unique strings in python is to use uuid
module. If you want to get alphanumeric output, you can simply use base64 encoding as well:
import uuid
import base64
uuid = base64.b64encode(uuid.uuid4().bytes).replace('=', '')
# sample value: 1Ctu77qhTaSSh5soJBJifg
You can then put this code in the model’s save
method or define a custom model field using it.
Here’s how I would do it without making the field a primary key:
from django.db import IntegrityError
class MyTemporaryObject(models.Model):
auto_pseudoid = models.CharField(max_length=16, blank=True, editable=False, unique=True)
# add index=True if you plan to look objects up by it
# blank=True is so you can validate objects before saving - the save method will ensure that it gets a value
# other fields as desired
def save(self, *args, **kwargs):
if not self.auto_pseudoid:
self.auto_pseudoid = generate_random_alphanumeric(16)
# using your function as above or anything else
success = False
failures = 0
while not success:
try:
super(MyTemporaryObject, self).save(*args, **kwargs)
except IntegrityError:
failures += 1
if failures > 5: # or some other arbitrary cutoff point at which things are clearly wrong
raise
else:
# looks like a collision, try another random value
self.auto_pseudoid = generate_random_alphanumeric(16)
else:
success = True
Two problems that this avoids, compared to using the field as the primary key are that:
1) Django’s built in relationship fields require integer keys
2) Django uses the presence of the primary key in the database as a sign that save
should update an existing record rather than insert a new one. This means if you do get a collision in your primary key field, it’ll silently overwrite whatever else used to be in the row.
Try this:
The if statement below is to make sure that the model is update able.
Without the if statement you’ll update the id field everytime you resave the model, hence creating a new model everytime
from uuid import uuid4
from django.db import IntegrityError
class Book(models.Model):
id = models.CharField(primary_key=True, max_length=32)
def save(self, *args, **kwargs):
if self.id:
super(Book, self).save(*args, **kwargs)
return
unique = False
while not unique:
try:
self.id = uuid4().hex
super(Book, self).save(*args, **kwargs)
except IntegrityError:
self.id = uuid4().hex
else:
unique = True
The code snippet below uses the secrets
library that comes with Python, handles id collisions, and continues to pass integrity errors when there isn’t an id collision.
example of the ids 0TCKybG1qgAhRuEN
, yJariA4QN42E9aLf
, AZOMrzlkJ-RKh4dp
import secrets
from django.db import models, IntegrityError
class Test(models.Model):
pk = models.CharField(primary_key=True, max_length=32)
def save(self, *args, **kwargs):
unique = False
while not unique:
try:
self.pk = secrets.token_urlsafe(12)
super(Test, self).save(*args, **kwargs)
except IntegrityError as e :
# keep raising the exception if it's not id collision error
if not 'pk' in e.args[0]:
unique = True
raise e
else:
unique = True
I would like a model to generate automatically a random alphanumeric string as its primary key when I create a new instance of it.
example:
from django.db import models
class MyTemporaryObject(models.Model):
id = AutoGenStringField(lenght=16, primary_key=True)
some_filed = ...
some_other_field = ...
in my mind the key should look something like this “Ay3kJaBdGfcadZdao03293”. It’s for very temporary use. In case of collision I would like it Django to try a new key.
I was wondering if there was already something out there, or a very simple solution I am not seeing (I am fairly new to python and Django). Otherwise I was thinking to do my own version of models.AutoField
, would that be the right approach?
I have already found how to generate the key here, so it’s not about the string generation. I would just like to have it work seamlessly with a simple Django service without adding too much complexity to the code.
EDIT:
Possible solution? What do you think?
id = models.CharField(unique=True, primary_key=True, default=StringKeyGenerator(), editable=False)
with
class StringKeyGenerator(object):
def __init__(self, len=16):
self.lenght = len
def __call__(self):
return ''.join(random.choice(string.letters + string.digits) for x in range(self.lenght))
I came up with it after going through the Django documentation one more time.
One of the simplest way to generate unique strings in python is to use uuid
module. If you want to get alphanumeric output, you can simply use base64 encoding as well:
import uuid
import base64
uuid = base64.b64encode(uuid.uuid4().bytes).replace('=', '')
# sample value: 1Ctu77qhTaSSh5soJBJifg
You can then put this code in the model’s save
method or define a custom model field using it.
Here’s how I would do it without making the field a primary key:
from django.db import IntegrityError
class MyTemporaryObject(models.Model):
auto_pseudoid = models.CharField(max_length=16, blank=True, editable=False, unique=True)
# add index=True if you plan to look objects up by it
# blank=True is so you can validate objects before saving - the save method will ensure that it gets a value
# other fields as desired
def save(self, *args, **kwargs):
if not self.auto_pseudoid:
self.auto_pseudoid = generate_random_alphanumeric(16)
# using your function as above or anything else
success = False
failures = 0
while not success:
try:
super(MyTemporaryObject, self).save(*args, **kwargs)
except IntegrityError:
failures += 1
if failures > 5: # or some other arbitrary cutoff point at which things are clearly wrong
raise
else:
# looks like a collision, try another random value
self.auto_pseudoid = generate_random_alphanumeric(16)
else:
success = True
Two problems that this avoids, compared to using the field as the primary key are that:
1) Django’s built in relationship fields require integer keys
2) Django uses the presence of the primary key in the database as a sign that save
should update an existing record rather than insert a new one. This means if you do get a collision in your primary key field, it’ll silently overwrite whatever else used to be in the row.
Try this:
The if statement below is to make sure that the model is update able.
Without the if statement you’ll update the id field everytime you resave the model, hence creating a new model everytime
from uuid import uuid4
from django.db import IntegrityError
class Book(models.Model):
id = models.CharField(primary_key=True, max_length=32)
def save(self, *args, **kwargs):
if self.id:
super(Book, self).save(*args, **kwargs)
return
unique = False
while not unique:
try:
self.id = uuid4().hex
super(Book, self).save(*args, **kwargs)
except IntegrityError:
self.id = uuid4().hex
else:
unique = True
The code snippet below uses the secrets
library that comes with Python, handles id collisions, and continues to pass integrity errors when there isn’t an id collision.
example of the ids 0TCKybG1qgAhRuEN
, yJariA4QN42E9aLf
, AZOMrzlkJ-RKh4dp
import secrets
from django.db import models, IntegrityError
class Test(models.Model):
pk = models.CharField(primary_key=True, max_length=32)
def save(self, *args, **kwargs):
unique = False
while not unique:
try:
self.pk = secrets.token_urlsafe(12)
super(Test, self).save(*args, **kwargs)
except IntegrityError as e :
# keep raising the exception if it's not id collision error
if not 'pk' in e.args[0]:
unique = True
raise e
else:
unique = True