[Fixed]-Django conditional create

10👍

EDIT: This is not an Optimistic Lock attempt. This is a direct answer to OP’s provided code.


Django offers a way to implement conditional queries. It also offers the update_or_create(defaults=None, **kwargs) shortcut which:

The update_or_create method tries to fetch an object from the database based on the given kwargs. If a match is found, it updates the fields passed in the defaults dictionary.

The values in defaults can be callables.

So we can attempt to mix and match those two in order to recreate the supplied query:

obj, created = Account.objects.update_or_create(
    id=self.id,
    version=self.version,
    defaults={
        balance: Case(
            When(version=self.version, then=F('balance')+amount),
            default=amount
        ),
        version: Case(
            When(version=self.version, then=F('version')+1),
            default=self.version
        )
    }
)

Breakdown of the Query:

The update_or_create will try to retrieve an object with id=self.id and version=self.version in the database.

  • Found: The object’s balance and version fields will get updated with the values inside the Case conditional expressions accordingly (see the next section of the answer).
  • Not Found: The object with id=self.id and version=self.version will be created and then it will get its balance and version fields updated.

Breakdown of the Conditional Queries:

  1. balance Query:

    • If the object exists, the When expression’s condition will be true, therefore the balance field will get updated with the value of:

      # Existing balance       # Added amount
         F('balance')      +        amount
      
    • If the object gets created, it will receive as an initial balance the amount value.

  2. version Query:

    • If the object exists, the When expression’s condition will be true, therefore the version field will get updated with the value of:

      # Existing version        # Next Version
         F('version')      +           1
      
    • If the object gets created, it will receive as an initial version the self.version value (it can also be a default initial version like 1.0.0).


Notes:

0👍

Except for QuerySet.update returning the number of affected rows Django doesn’t provide any primitives to deal with optimistic locking.

However there’s a few third-party apps out there that provide such a feature.

  1. django-concurrency which is the most popular option that provides both database level constraints and application one
  2. django-optimistic-lock which is a bit less popular but I’ve tried in a past project and it was working just fine.
  3. django-locking unmaintained.

Edit: It looks like OP was not after optimistic locking solutions after all.

Leave a comment