Search code examples
mysqldjangotransactions

Django increment string value without race condition


On save I want to receive names like name, name(1), name(2). I implemented the following code:

        with transaction.atomic():
            same_name_count = Folder.objects.filter(
                owner=validated_data["owner"],
                name__iregex=r"%s(\s\(\d+\))?" % validated_data["name"],
            ).count()

            if same_name_count:
                validated_data["name"] = f"{validated_data['name']} ({same_name_count+1})"

            folder = Folder.objects.create(**validated_data)

But I still receive race condition and receive same names when I run this code in celery task. I also tried select_to_update to lock all rows somehow and get correct count

My model looks like this:

class Folder(models.Model):
    owner = models.ForeignKey('User')
    name = models.CharField(max_length=255)

Solution

  • select_for_update will lock all selected rows in the table, but will not prevent adding new rows.

    What you need to do is to lock row in other table. This way the transaction will wait until this locked row is unlocked.

    In your case the most suitable is User table.

    Here is example code:

        with transaction.atomic():
            # Assuming validated_data["owner"] is an id of the user. 
            # If its the User objects then add .id at the end
            folder_owner = User.objects.select_for_update().get(id=validated_data["owner"])
    
            same_name_count = Folder.objects.filter(
                owner=folder_owner,
                name__iregex=r"%s(\s\(\d+\))?" % validated_data["name"],
            ).count()
    
            if same_name_count:
                validated_data["name"] = f"{validated_data['name']} ({same_name_count+1})"
    
            folder = Folder.objects.create(**validated_data)