Django decorator @transaction.non_atomic_requests not working in a ViewSet method
Asked Answered
R

3

10

I recently ran into the need to disable transaction requests in one of my views, in order to be able to call db.connection.close() and connect() during requests in an effort to improve performance.

I have a DRF ViewSet, and used the following very simple view to verify that the non_atomic_requests decoractor seems to have no effect. ATOMIC_REQUESTS=True is enabled in settings.py, and DEBUG=False.

from django.db import transaction    

@transaction.non_atomic_requests
def create(self, *args, **kwargs):
    m = MyModel(stuff="hello")
    m.save()
    raise Exception('exception! row should still be saved though')
    return Response()

After calling the view, I open Django shell, and verify that the amount of rows in the db has not grown, even though it should have. Also opening a debugger during the request to halt execution after the line m.save(), I can observe in Django shell that a new row is not visible yet.

If I set ATOMIC_REQUESTS=False in settings.py, the code works as expected, and the number of rows in the db is grown by one, even if an error is raised before returning from the view.

When ATOMIC_REQUESTS=False, using @transaction.atomic decorator does work as expected though. So as a workaround, I could use it to set every other view as atomic instead...

I am currently thinking this is a bug in the framework. Can anybody verify my findings, or point out if I am misunderstanding how this decorator is meant to function?

I am using Python 3.6, Django 2.0 and DRF 3.7.7.

Reckon answered 18/4, 2018 at 15:4 Comment(1)
The return line is never reached because of the previous raiseSnowfield
J
16

As documented, non_atomic_requests only works if it's applied to the view itself.

In your case, create is a viewset method, it is not the view itself. With a regular class based view in Django, you'd need to wrap the dispatch method using method_decorator.

@method_decorator(transaction.non_atomic_requests, name='dispatch') 
class MyViewSet(ViewSet):
    ...

    def create(self, *args, **kwargs):
        ...

I'm not familiar enough with the rest framework internals to say whether this will work or not. Note that it will disable atomic requests for all views handled by the viewset, not just the create method.

The non_atomic_requests method has this limitation because the Django request handler has to inspect the view before it runs so that it knows whether to run it in a transaction. The transaction.atomic decorator does not have the same requirement - Django can simply start the transaction as soon as it enters an atomic function or block.

Jabiru answered 18/4, 2018 at 15:25 Comment(3)
Has anyone confirmed that this works with Django Rest Framework ViewSets? It has not worked for me.Whitebook
@JoshuaSwain it looks to me as if it should work, because the viewset copies attributes from the dispatch method here. I haven’t tried it to confirm that it does work.Jabiru
@JoshuaSwain it's working for me. ``` from django.db import transaction from django.utils.decorators import method_decorator from rest_framework import viewsets from rest_framework.permissions import IsAuthenticated @method_decorator(transaction.non_atomic_requests, name="dispatch") class MyViewSet(viewsets.ViewSet): permission_classes = [IsAuthenticated] def create(self, request): pass ```Possessed
D
0

In case you are using db other than 'default': you need to explicitly mention the 'using' property. otherwise, it would default to 'default'

transaction.non_atomic_requests(using='db_name')

in case of class based views- either apply it on dispatch in views:

@method_decorator(transaction.non_atomic_requests(using='db_name'), name='dispatch') 
class MyViewSet(ViewSet):
...

or apply it on as_view method in urls

path(r'endpoint/', transaction.non_atomic_requests(using='db_name')(MyViewSet.as_view()), name='myview')
Dysart answered 22/1, 2023 at 7:47 Comment(0)
T
0

When using Django REST Framework ViewSets, you have to consider that the actual Django route will be the result of calling .as_view({}) on the ViewSet.

Thus, you'd need to decorate the .dispatch() method of the resulting Class-based view. Given that rest_framework.viewsets.ViewSet > rest_framework.views.APIView > django.views.generic.base.View, and that the Django documentation explicitly mentions the View.dispatch() method, you should know you can either use @method_decorator over the ViewSet.

In an implementation I'm working on, we found out that decorating the dispatch method works as well:

class MyView(APIView):
    def post(self, request):
        return Response("Hello!")

    @transaction.non_atomic_requests()
    def dispatch(self, request, *args, **kwargs):
        return super().dispatch(request, *args, **kwargs)

Beware that this will make requests for all methods to be decorated, and you will have to handle transactions manually in all of them.

Thai answered 26/2, 2024 at 12:48 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.