Danra Danra - 1 month ago 8
Python Question

Atomically Compare-Exchange a Model Field in Django

How can I atomically compare-exchange-save a value of Django

Model
instance
Field
? (Using PostgreSQL as the DB backend).

An example use case is making sure multiple posts with similar content (e.g. submits of the same form) take effect only once, without relying on insecure and only sometimes-working client-side javascript or server-side tracking of form UUIDs, which isn't secure against malicious multiple-posts.

For example:

def compare_exchange_save(model_object, field_name, comp, exch):
# How to implement?
....


from django.views.generic.edit import FormView
from django.db import transaction
from my_app.models import LicenseCode

class LicenseCodeFormView(FormView):
def post(self, request, ...):

# Get object matching code entered in form
license_code = LicenseCode.objects.get(...)

# Safely redeem the code exactly once
# No change is made in case of error
try:
with transaction.atomic()
if compare_exchange_save(license_code, 'was_redeemed', False, True):
# Deposit a license for the user with a 3rd party service. Raises an exception if it fails.
...
else:
# License code already redeemed, don't deposit another license.
pass
except:
# Handle exception
...

Answer

What you are looking for is the update function on a QuerySet object.

Depending on the value, you can do a comparison with Case, When objects - check out the docs on conditional updates NOTE that link is for 1.10 - Case/When came in in 1.8.

You might also find utility in using F which is used to reference a value in a field.

For example:

I need to update a value in my model Model:

(Model.objects
 .filter(id=my_id)
 .update(field_to_be_updated=Case(
      When(my_field=True, then=Value(get_new_license_string()),
      default=Value(''),
      output_field=models.CharField())))

If you need to use an F object, just reference it on the right hand side of the equals in the update expression.

The update doesn't necessitate the use of transaction.atomic() context manager but if you need to do any other database operations you should continue to wrap that code with transaction.atomic()

Edit:

You may also like to use the queryset select_for_update method that implements row locks when the queryset is executed docs.