13

Does the Django ORM provide a way to conditionally create an object?

For example, let's say you want to use some sort of optimistic concurrency control for inserting new objects.
At a certain point, you know the latest object to be inserted in that table, and you want to only create a new object only if no new objects have been inserted since then.

If it's an update, you could filter based on a revision number:

updated = Account.objects.filter(
    id=self.id,
    version=self.version,
).update(
    balance=balance + amount,
    version=self.version + 1,
)

However, I can't find any documented way to provide conditions for a create() or save() call.

I'm looking for something that will apply these conditions at the SQL query level, so as to avoid "read-modify-write" problems.

John Moutafis
  • 22,254
  • 11
  • 68
  • 112
samfrances
  • 3,405
  • 3
  • 25
  • 40
  • What's the condition that you want to test before? take a look at this [answer](https://stackoverflow.com/a/50113495/5644965) – Lemayzeur Jun 05 '18 at 12:46
  • It could literally be anything. "Create a new X if there are less than 100 of them". Anything where a table-level lock might make sense (if you wanted to go for a locking rather than an optimistic approach). – samfrances Jun 05 '18 at 13:36
  • Ok! so the link in my above comment will work – Lemayzeur Jun 05 '18 at 17:12
  • I'm not sure I understand how. The first version appears to create an instance if one doesn't already exist, which is a much more restricted set of conditions. The second creates or updates (I only want to create). Both seem like they are susceptible to race conditions, since they make two queries, one to check if some condition obtains, and then (assuming nothing has changed in the meantime) to create an object. I was hoping for a way that the django ORM would facilitate a doing this operation in a single query. But it's quite possible that my understanding is flawed. – samfrances Jun 06 '18 at 09:30
  • 3
    @samfrances: but if you put a `unique_together` on the `revision` number, then the create will simply fail, if somebody constructed such revision already. Wouldn't that solve the problem? For example if you have `iban, rev, value`, you can constrain `unique_together=(('iban', 'rev'),)`, so then the database will enforce no two rows with the same `iban` and `rev` exist. Therefore a `.create(..)` will fail, so you make it condition against such constraints. If you need more complex constraints, you can encode these typically at the database (with for example `CHECK value > 0`. – Willem Van Onsem Jun 10 '18 at 14:01
  • How often do you check the condition and create objects? Although, there is no such thing being offered by Django to achieve what you are looking for. But still you can create your own script and trigger it either periodically or can be triggered by some action. – Anadi Sharma Jun 11 '18 at 07:24

2 Answers2

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:

John Moutafis
  • 22,254
  • 11
  • 68
  • 112
  • I don't think using `update_or_create` is appropriate here. The purpose of optimistic locking is to avoid overwriting existing entries (e.g. on version mismatch) not to create new ones when there's a mismatch. – Simon Charette Jun 12 '18 at 20:39
  • 1
    @SimonCharette This is not an attempt on optimistic locking. This is a direct answer, using the actual OP's code (I should probably edit this into the answer...). – John Moutafis Jun 12 '18 at 20:40
  • 1
    You are right, I removed my downvote as I misread OP's question. I assumed OP was looking for optimistic locking solutions. – Simon Charette Jun 12 '18 at 20:42
  • 1
    @SimonCharette No problem. I edited my answer to reflect that it answers directly for the provided code! – John Moutafis Jun 12 '18 at 20:43
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.

Simon Charette
  • 5,009
  • 1
  • 25
  • 33