Django - Rollback save with transaction atomic - python

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.

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()

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)

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

Related

TypeError: Object of type CustomException is not JSON serializable

As per the flask-restful doc, it calls handle_error() function on any 400 or 500 error that happens on a Flask-RESTful route. So I tried to handle exceptions through out my application using this call back, thinking it would leverage multiple try catch block in my application. But while raising custom exception(ResourceNotFound) from a model raises another exception TypeError: Object of type ResourceNotFound is not JSON serializable
from werkzeug.exceptions import HTTPException
from flask_restful import Api
class ExtendedAPI(Api):
def handle_error(self, err):
"""
This class overrides 'handle_error' method of 'Api' class ,
to extend global exception handing functionality of 'flask-restful'
and helps preventing writing unnecessary
try/except block though out the application
"""
# import pdb; pdb.set_trace()
logger.error(err)
# Handle HTTPExceptions
if isinstance(err, HTTPException):
return jsonify({
'message': getattr(
err, 'description', HTTP_STATUS_CODES.get(err.code, '')
)
}), err.code
if isinstance(err, ValidationError):
resp = {
'success': False,
'errors': err.messages
}
return jsonify(resp), 400
# If msg attribute is not set,
# consider it as Python core exception and
# hide sensitive error info from end user
# if not getattr(err, 'message', None):
# return jsonify({
# 'message': 'Server has encountered some error'
# }), 500
# Handle application specific custom exceptions
return jsonify(**err.kwargs), err.http_status_code
Custom exception:
class Error(Exception):
"""Base class for other exceptions"""
def __init__(self, http_status_code: int, *args, **kwargs):
# If the key `msg` is provided, provide the msg string
# to Exception class in order to display
# the msg while raising the exception
self.http_status_code = http_status_code
self.kwargs = kwargs
msg = kwargs.get('msg', kwargs.get('message'))
if msg:
args = (msg,)
super().__init__(args)
self.args = list(args)
for key in kwargs.keys():
setattr(self, key, kwargs[key])
class ResourceNotFound(Error):
"""Should be raised in case any resource is not found in the DB"""
The handle_error function handles HTTPException, marshmallow validation errors and the last statement to handle my custom exceptions.
but using pdb, I saw the err object received by handle_error() differs from the custom exception I raised from the model. Not able to figure out any solution for this. Any thoughts on solving this problem or any different approach I can follow??

Django rollback not working after second objects.create fails to create a record

Here is the snippet code of the view:
#transaction.atomic()
def insert_in_sample_table(request):
try:
with transaction.atomic():
insert_obj = SampleTable1.objects.create(fld_id=2, fld_name='abc')
raise Exception ("This is manual exception")
insert_obj2 = SampleTable2.objects.create(fld_id=1, fld_name='xyz')
return HttpResponse("SUCCESS")
except Exception as e:
return HttpResponse(str(e))
There are two models:
SampleTable1
SampleTable2
I've manually raised an exception after first create and now I expect the changed done in first model(SampleTable1) must be undone.
But unfortunately I don't see the rollback
I've added 'ATOMIC_REQUESTS': True, in DATABASE in settings.py file
I removed the
#transaction.atomic()
and checked, it still does not rollback.
How to make the first database transaction rollback?
You can to use the '#transaction.atomic' decorator, like this:
#transaction.atomic
def insert_in_sample_table(request):
tra = transaction.savepoint()
try:
insert_obj = SampleTable1.objects.create(fld_id=2, fld_name='abc')
insert_obj2 = SampleTable2.objects.create(fld_id=1, fld_name='xyz')
transaction.savepoint_commit(tra)
return HttpResponse("SUCCESS")
except IntegrityError:
transaction.savepoint_rollback(tra)
return HttpResponse("DB ERROR")
If you are using multiples databases, you should to specific the connection name and replace the line:
with transaction.atomic():
For this:
with transaction.atomic(using='connection_name'):

how to properly refactor this function into another file? django

I have a try and except which is used super often, so I am thinking of taking it out and make it into a function in other file but I cannot seem to make it work.
Can someone please give me a hand?
this is the code which I use really really often
try:
user = get_object_or_404(User, email=data['email'])
except Exception as e:
print(e)
return JSONresponse(False, e.message)
I tried making it into another file and did it this way
def auth_token(data):
try:
return get_object_or_404(User, email=data['email'])
except Exception as e:
return JSONresponse({'status': False})
then when I call auth_token I did it this way in another way and of course I did import it
user = auth_token(data)
I can understand this would then return me JSONresponse({'status': False}) and I tried using raise and it would tell me that I can't use JSONresponse
Can someone give me an idea how this can be done?
Best practice is to use as simple parameters as possible (this makes your function easier to test).
What you're looking for in the exception case is something that can be sent from auth_token through your view:
def auth_token(email): # this returns a User not a token..?
try:
return User.objects.get(email=email)
except User.DoesNotExist:
raise ImmediateHttpResponse(JSONresponse({'status': False}))
Usage would then be:
def myview(request, ...):
token = auth_token(data['email'])
...
Any ImmediateHttpResponse exception will escape the view and must be picked up by a middleware class.
Unfortunately, Django doesn't come with an ImmediateHttpResponse class, but TastyPie has one that we can steal. First the exception class:
class ImmediateHttpResponse(Exception):
"""This exception is used to interrupt the flow of processing to immediately
return a custom HttpResponse.
Common uses include::
* for authentication (like digest/OAuth)
* for throttling
(from TastyPie).
"""
_response = http.HttpResponse("Nothing provided.")
def __init__(self, response):
self._response = response
#property
def response(self):
return self._response
then the middleware:
from myexceptions import ImmediateHttpResponse
class ImmediateHttpResponseMiddleware(object):
"""Middleware that handles ImmediateHttpResponse exceptions, allowing
us to return a response from deep in form handling code.
"""
def process_exception(self, request, exception):
if isinstance(exception, ImmediateHttpResponse):
return exception.response
return None
This class must be added to your MIDDLEWARE_CLASSES in your settings.py file as 'mymiddleware.ImmediateHttpResponseMiddleware'.

transaction based in django

I would like to get some idea on how to improve (if any) my code in implementing transaction based query in Django.
This is how I understand the ATOMIC_REQUEST I read on django documentation. I have this function view:
from django.db import transaction
import sys
#transaction.atomic
def save_progress(request):
try:
with atomic.transaction():
qs = AllProgressModel()
qs.name = 'level up'
qs.level = 25
qs.description = 'Increased 2 level'
qs.save()
except:
print(sys.exc_info())
-Am I doing it right?
-Is the progress will be saved or not if the connection lost occur during saving?
Thank you in advance!
You don't need both decorator #transaction.atomic and with atomic.transaction(), one is usually enough.
While using with atomic.transaction(), catch IntegrityError exceptions instead of broadly handling all exceptions at once.
Edit: If you're handling exceptions outside the atomic block (like below) then it's a better practice to have an outer atomic wrap as well, for handling rollbacks and other database operations you might need in exception handling part.
from django.db import IntegrityError, transaction
def save_progress(request):
try:
# with atomic.transaction() -> produce error ( typo )
with transaction.atomic():
...
qs.name = 'level up'
qs.level = 25
qs.description = 'Increased 2 level'
qs.save()
except IntegrityError:
# You are here if something goes within the transaction, after rollback
# HANDLE exception

Django validation exception from Model Manager create method

I am using a custom Django model Manager to create an instance. The fields of the instance are derived from data fetched from a given URL. One of the fields is a filename containing a JSON report that I need to open. My question is - if I determine an error has occurred (file not found, content not valid, etc.), is it reasonable to throw an exception in a create() method and is there a preferred exception type to throw?
The model needs the parsed data to create a valid instance so I would already know the model is not valid before executing the create() method.
class IndexingUpdateRunManager(models.Manager):
def create_from_run(self,run_history_url):
run_info_dict = self.extract_fields_from_url(run_history_url)
run_config_file = run_info_dict["run_config_file"]
report_filename = run_info_dict["status_report_file"]
try:
out_fh = open(report_filename,'r')
report_data = json.loads(out_fh)
status_code=report_data["status"]
except Exception, e:
# throw an exception?
this_run=self.create(run_config_file_used=run_config_file,
report_filename = report_filename,
run_status_code=status_code)
return this_run
class MyUpdateRun(models.Model):
run_config_file_used = models.FilePathField(max_length=1024,
help_text="config file for run")
report_filename = models.FilePathField(max_length=1024,
help_text="status report file for run")
run_status_code = models.IntegerField(help_text="status code for overall run execution")
objects = MyUpdateRunManager()
>>MyUpdateRun.objects.create_from_run("https://server/job_status/builds/200/")
You can raise one ObjectDoesNotExist ( from django.core.exceptions ) and as well you can create your own exception call like:
class MyException(ObjectDoesNotExist):
pass
and raise it in some specific situation that Django doesn't provide a satisfying exception.
Ps.: Your exception can still inherit from the base exception:
class MyException(Exception):
pass

Categories