66

I am trying to create a view where I save an object but I'd like to undo that save if some exception is raised. This is what I tried:

class MyView(View):

    @transaction.atomic
    def post(self, request, *args, **kwargs):
        try:
            some_object = SomeModel(...)
            some_object.save()

            if something:
                raise exception.NotAcceptable()
                # When the workflow comes into this condition, I think the previous save should be undone
                # What am I missing?

        except exception.NotAcceptable, e:
            # do something

What am I doing wrong? even when the exception is raised some_object is still in Database.

Lucas Dolsan
  • 185
  • 2
  • 13
Gocht
  • 9,924
  • 3
  • 42
  • 81

3 Answers3

81

Atomicity Documentation

To summarize, @transaction.atomic will execute a transaction on the database if your view produces a response without errors. Because you're catching the exception yourself, it appears to Django that your view executed just fine.

If you catch the exception, you need to handle it yourself: Controlling Transactions

If you need to produce a proper json response in the event of failure:

from django.db import SomeError, transaction

def viewfunc(request):
    do_something()

    try:
        with transaction.atomic():
            thing_that_might_fail()
    except SomeError:
        handle_exception()

    render_response()
Tim Tisdall
  • 9,914
  • 3
  • 52
  • 82
jlucier
  • 1,482
  • 10
  • 14
  • This view is for an API, so I think I need to handle any possible error to give a propper json response. Do there's no way to do this with atomic decorator? – Gocht Jan 11 '16 at 20:35
  • Not with the decorator imho, because it handles the transaction outside your function. Nice example with the context manager ! – jpic Jan 11 '16 at 20:39
  • You must have an atomic block inside the try.. except block as in the answer. You can use the atomic decorator on the view as well if you want. – Alasdair Jan 11 '16 at 20:43
  • Using `with transaction.atomic()` inside the function worked. @Alasdair so if I want to use only the decorator I shouldn't catch the exception? If I do not catch it how could I give a proper response? – Gocht Jan 11 '16 at 20:50
  • 1
    Realistically it seems like you should not use the decorator. You probably want the response to differ depending on the success of the save. Thus, you should do it as I did in the example and then you can easily tell whether or not you succeeded. If the response doesn't depend on the success of the execution, then use the decorator and don't catch/handle the exception (remove the try/except block). – jlucier Jan 11 '16 at 20:52
  • 4
    No, if you want to catch the exception you **must** use the `with transaction.atomic` inside the try.. except block. My point was, that you can use `@transaction.atomic` for the view at the same time. This is still useful, if you want `do_something()` and the result of the try..except block to be in one transaction that either succeeds or fails. – Alasdair Jan 11 '16 at 20:53
  • Using savepoints with try catch is not rolling back any transactions in case of failure. Can anyone tell me use savepoints with try-catch as asked in below link: https://stackoverflow.com/questions/60314464/django-unable-to-rollback-with-try-exception-block-for-atomic-transactions/60315686#60315686 – shreesh katti Feb 20 '20 at 08:48
18

However, if an exception happens in a function decorated with transaction.atomic, then you don't have anything to do, it'll rollback automatically to the savepoint created by the decorator before running the your function, as documented:

atomic allows us to create a block of code within which the atomicity on the database is guaranteed. If the block of code is successfully completed, the changes are committed to the database. If there is an exception, the changes are rolled back.

If the exception is catched in an except block, then it should be re-raised for atomic to catch it and do the rollback, ie.:

    try:
        some_object = SomeModel(...)
        some_object.save()

        if something:
            raise exception.NotAcceptable()
            # When the workflow comes into this condition, I think the previous save should be undome
            # Whant am I missing?

    except exception.NotAcceptable, e:
        # do something
        raise  # re-raise the exception to make transaction.atomic rollback

Also, if you want more control, you can rollback manually to previously set savepoint, ie.:

class MyView(View):
    def post(self, request, *args, **kwargs):
        sid = transaction.savepoint()
        some_object = SomeModel(...)
        some_object.save()

        if something:
            transaction.savepoint_rollback(sid)
        else:
            try:
                # In worst case scenario, this might fail too
                transaction.savepoint_commit(sid)
            except IntegrityError:
                transaction.savepoint_rollback(sid)
jpic
  • 32,891
  • 5
  • 112
  • 113
  • That's what I though, and that's why I did this function in this way, but as I told in the question, the object is still in database event when exception is raised, is there any aditional step to use the savepoints? – Gocht Jan 11 '16 at 20:33
  • Maybe it's because the exception was catched by the except block, and not re-raised, so atomic thought it the function executed with success. – jpic Jan 11 '16 at 20:34
  • If I do not catch the error, I cant give a good response. How could I build my function? – Gocht Jan 11 '16 at 20:37
  • Added an example with savepoint rollback, but i think the context manager is perhaps more appropriate in this case. – jpic Jan 11 '16 at 20:39
  • Using savepoints with try catch is not rolling back any transactions in case of failure. Can anyone tell me use savepoints with try-catch as asked in below link: https://stackoverflow.com/questions/60314464/django-unable-to-rollback-with-try-exception-block-for-atomic-transactions/60315686#60315686 – shreesh katti Feb 20 '20 at 08:48
5

For me this works in Django 2.2.5

First of all in your settings.py

...

DATABASES = {
    'default': {
        'ENGINE': 'xxx',  # transactional db
        ...
        'ATOMIC_REQUESTS': True,
    }
}

And in your function (views.py)

from django.db import transaction

@transaction.atomic
def make_db_stuff():

    # do stuff in your db (inserts or whatever)

    if success:
        return True
    else:
        transaction.set_rollback(True)
        return False
Antonio Cord
  • 260
  • 4
  • 5
  • 11
    Setting `ATOMIC_REQUESTS` to `True` makes *all* of your views atomic - so you should either set it to true and use `@transaction.non_atomic_requests` on the views you want to exclude, or just individually set views as atomic via `@transaction.atomic`. Source: https://docs.djangoproject.com/en/3.0/topics/db/transactions/#tying-transactions-to-http-requests – Hybrid Jan 23 '20 at 19:50