[Fixed]-Does "select_for_update()" work with the update method in Django?

13👍

As far as I’m aware update just performs an UPDATE … WHERE query, with no SELECT before it

Yes, that’s correct. You could confirm this by looking at the actual queries made. Using the canonical django tutorial "polls" app as an example:

with transaction.atomic():
    qs = polls.models.Question.objects.select_for_update().all()
    qs.update(question_text='test')

print(connection.queries)
# {'sql': 'UPDATE "polls_question" SET "question_text" = \'test\'', 'time': '0.008'}

So, as you expect, there is no SELECT.

Though, ensuring the lock is acquired would be as simple as doing anything to cause the queryset to be evaluated.

with transaction.atomic():
    qs = polls.models.Question.objects.select_for_update().all()
    list(qs) # cause evaluation, locking the selected rows
    qs.update(question_text='test')

print(connection.queries)
#[...
# {'sql': 'SELECT "polls_question"."id", "polls_question"."question_text", "polls_question"."pub_date" FROM "polls_question" FOR UPDATE', 'time': '0.003'},
# {'sql': 'UPDATE "polls_question" SET "question_text" = \'test\'', 'time': '0.001'}
#]

A secondary question is whether a lock even adds any protection against race conditions if one makes a single UPDATE query against the locked rows

In general, yes. Whether it is necessary in a particular situation depends what kind of race condition you’re worried about. The lock will prevent race conditions where another transaction may try to update the same row, for example.

Race conditions can be avoided without locks, too, depending on the nature of the update/race condition. Sometimes a transaction is sufficient, sometimes it’s not. You may also use expressions which are evaluated server-side on the db to prevent race conditions (e.g. using Django’s F() expressions).

There are also other considerations, like your db dialect, isolation levels, and more.

Additional reference on race condition thoughts: PostgreSQL anti-patterns: read-modify-write cycles (archive)

👤sytech

1👍

To run SELECT FOR UPDATE, you need to use select_for_update() and update() separately then put print(qs) between them as shown below. *You can also put bool(qs), len(qs) or list(qs) instead of print(qs):

qs = Entry.objects.select_for_update().filter(author=request.user)
print(qs) # bool(qs), len(qs) or list(qs) is also fine
qs.update(foo="bar", wobble="wibble")

You can see my question and answer explaining more about select_for_update() in Django:

Leave a comment