Reputation: 9785
I need to make sure that an object that is read from the database and written back, cannot be modified in the meantime by another request/process.
Does transaction.atomic() guarantee that?
My tests so far tell me no. If there's nothing wrong with them what would be the right way to achieve atomic READS and WRITES?
My example that I have tested.
Put the Test class somewhere in your model. atomic_test.py and atomic_test2.py should be saved as management commands. Run python manage.py atomic_test first, then python manage.py atomic_test2. The second script does not block and its changes are lost.
models.py
class Test(models.Model):
value = models.IntegerField()
atomic_test.py
from django.core.management.base import NoArgsCommand
from django.db import transaction
from time import sleep
from core.models import Test
class Command(NoArgsCommand):
option_list = NoArgsCommand.option_list
def handle(self, **options):
Test.objects.all().delete()
t = Test(value=50)
t.save()
print '1 started'
with transaction.atomic():
t = Test.objects.all()[0]
sleep(10)
t.value = t.value + 10
t.save()
print '1 finished: %s' %Test.objects.all()[0].value
atomic_test2.py
from django.core.management.base import NoArgsCommand
from django.db import transaction
from time import sleep
from core.models import Test
class Command(NoArgsCommand):
option_list = NoArgsCommand.option_list
def handle(self, **options):
print '2 started'
with transaction.atomic():
t = Test.objects.all()[0]
t.value = t.value - 20
t.save()
print '2 finished: %s' %Test.objects.all()[0].value
Upvotes: 13
Views: 15370
Reputation: 49102
Django's transaction.atomic()
is a thin abstraction over the transaction facilities of the database. So its behavior really depends on the database layer, and is specific to the type of database and its settings. So to really understand how this works you need to read and understand the transaction documentation for your database. (In PostgreSQL, for example, the relevant documentation is Transaction Isolation and Explicit Locking).
As for your specific test case, the behavior you want can be achieved by using the select_for_update()
method on a Django queryset (if your database supports it). Something like:
in atomic_test.py
with transaction.atomic():
t = Test.objects.filter(id=1).select_for_update()[0]
sleep(10)
t.value = t.value + 10
t.save()
in atomic_test2.py
with transaction.atomic():
t = Test.objects.filter(id=1).select_for_update()[0]
t.value = t.value - 20
t.save()
The second one should block until the first one finishes, and see the new value of 60.
Other options include using the SERIALIZABLE
transaction isolation level or using a row lock, though Django doesn't provide a convenient API for doing those things.
Upvotes: 17
Reputation: 427
It would be much better to update atomically on the database using the F function:
from django.db.models import F
Test.objects.filter(id=1).update(value=F("value") + 10)
(This generates SQL something like "UPDATE test_test SET value = value + 10 WHERE id = 1"
)
Upvotes: 3