How to generate unique slug in Django without a race condition?
I'm creating a simple API for a blog site using Django REST Framework. I want each blog post to have its own unique, human-readable slug that will be used in the URL like /api/posts/my-first-post. Here is my current solution:
# models.py
class Post(models.Model):
title = models.CharField(max_length=255)
slug = models.SlugField(unique=True, blank=True, max_length=255)
body = models.TextField()
def save(self, *args, **kwargs) -> None:
if not self.slug:
import string, random
self.slug = slugify(self.title)
while Post.objects.filter(slug=self.slug).exists():
suffix = '-' + ''.join(random.choices(string.ascii_lowercase + string.digits, k=5))
self.slug = slugify(self.title + suffix)
super().save(*args, **kwargs)
# views.py
class PostList(generics.ListCreateAPIView):
serializer_class = PostSerializer
permission_classes = [permissions.IsAuthenticatedOrReadOnly]
lookup_field = 'slug'
def perform_create(self, serializer):
return serializer.save(author=self.request.user)
But apparently Post.objects.filter(slug=self.slug).exists() is creating a race condition because it works on the application level, rather than on the database level, so this could throw an IntegrityError if two posts with the same slug end up saving at the same moment. I'm not sure how to fix this.
I think I might have found a solution, it's in EAFP style like @jonrsharpe suggested:
def save(self, *args, **kwargs) -> None:
if self.slug:
return super().save(*args, **kwargs)
for _ in range(10):
try:
self.slug = f'{slugify(self.title)[:246]}-{uuid.uuid4().hex[:8]}'
return super().save(*args, **kwargs)
except IntegrityError:
self.slug = None
raise RuntimeError(f'Could not generate unique slug for post {self.title}')
We just let it fail then we try again until it works. But then again this also has issues probably