How can I better handle this Flask-SQLAlchemy commit/rollback? - python

I'm reviewing some old code I wrote and was looking at a shared commit function I had written to handle responses to the user on certain failures when attempting to commit changes to the database (such as deletes):
def _commit_to_database():
"""A shared function to make a commit to the database and handle exceptions
if encountered.
"""
flask.current_app.logger.info('Committing changes to database...')
try:
db.session.commit()
except AssertionError as err:
flask.abort(409, err)
except (exc.IntegrityError, sqlite3.IntegrityError) as err:
flask.abort(409, err.orig)
except Exception as err:
flask.abort(500, err)
finally:
db.session.rollback()
I think I understand my thought process: attempt the commit, upon certain failures trigger a flask.abort to send the response back, but I believe I found that the database was left with an open session requiring a rollback when I did this and adding the rollback into a finally statement resolved this allowing me to still use the flask.abort.
The questions I have around me code are:
1) Is this a bug: will the Flask-SQLAlchemy extension not close out the session as normal; is calling the rollback on the finally which will be triggering after the abort going to affect successful commits?
2) If this is a bug: what should I be doing differently in handling the try-except-finally and the db session?

You need to rollback when exception occurs and finally close the session:
def _commit_to_database():
"""A shared function to make a
commit to the database and
handle exceptions if encountered.
"""
flask.current_app.logger.info('Committing changes to db...')
try:
db.session.commit()
except AssertionError as err:
db.session.rollback()
flask.abort(409, err)
except (exc.IntegrityError, sqlite3.IntegrityError) as err:
db.session.rollback()
flask.abort(409, err.orig)
except Exception as err:
db.session.rollback()
flask.abort(500, err)
finally:
db.session.close()

Related

Sqlalchemy - rollback when exception

I need to rollback a transaction in core.event 'handle_error' (catch 'KeyboardInterrupt'), but the parameter in this event is ExceptionContext, how do this?
I usually have this kind of pattern when working with sqlalchemy:
session = get_the_session_one_way_or_another()
try:
# do something with the session
except: # * see comment below
session.rollback()
raise
else:
session.commit()
To make things easier to use, it is useful to have this as a context manager:
#contextmanager
def get_session():
session = get_the_session_one_way_or_another()
try:
yield session
except:
session.rollback()
raise
else:
session.commit()
And then:
with get_session() as session:
# do something with the session
If an exception is raised within the block, the transaction will be rolled back by the context manager.
*There is an empty except: which catches literally everything. That is usually not what you want, but here the exception is always re-raised, so it's fine.

Can exceptions be written for specific errors? Like database "XXX" does not exist

I would like to write and exception for the database does not exist, so I can create it and return back to the top (retry the connection).
How do you get specific when writing exceptions?
Can you have two or more exceptions?
try:
db_con = psycopg2.connect(host='HOSTNAME', database='MYDB', user='USERNAME', password='PASSWORD')
cur = db_con.cursor()
cur.execute('SELECT version()')
ver = cur.fetchone()
print ver
except psycopg2.DatabaseError, e:
print 'Error %s' % e
sys.exit(1)
finally:
if db_con:
db_con.close()
You should read the python documentation on Exceptions.
But generally speaking yes, you can define your own exception types just like any other class by extending Exception or any other appropriate error type (in this case I'd probably use IOError)
Example:
class NoDatabaseError(IOError):
pass
And then in the code above the database open:
try:
open_database(database)
except NoDatabaseError as e:
print('Could Not Open The Database: '+str(e))
except Exception as e:
print('Something Unexpected Went On:'+str(e))
If that's not what you're asking, you should clarify your question.
Creating your own Exception is just a matter of inheritance:
class DatabaseDoesNotExist(IOError):
pass
raise DatabaseDoesNotExist()
You can also raise nested exceptions by listing one as cause for the other:
except psycopg2.DatabaseError as e:
raise DatabaseDoesNotExist('message', e) # python 2
raise DatabaseDoesNotExist('message') from e # python 3
This will preserve the traceback prior to your intervention.

How can I get an error when no results are fetched with DB query?

I'm trying to learn and advance with SQLAlchemy. Today I wanted to learn about exceptions it raises.
I'm working on a Pyramid based project, MySQL server (InnoDB) and SQLAlchemy.
I am trying to except all errors since NoResultFound error would not raise or print in console. So I except exc.SQLAlchemyError.
When I query my table and no results are found, it does not raise or catch or except anything whatsoever and continues operating.
Questions:
How can I and How should I query for .all() or .one(), and deal with
the issue of not having any rows returned?
How can I and how should I deal with others SQL or System related errors? I would like to record them and observe them to fix issues.
My code is:
try:
query = Session.query(MyTable).filter(Terms.column == my_string).all()
except exc.SQLAlchemyError, e:
print e
return False
(Instead of exc.SQLAlchemyError, I first tried NoResultFound, e)
Indeed this code will not raise an exception if no records are found. So instead you should throw your own exception:
import logging
try:
records = Session.query(MyTable).\
filter(Terms.column == my_string).all()
if len(records) == 0:
raise MyException('No records found')
except MyException, e:
logging.info('No records found')
except exc.SQLAlchemyError, e:
logging.exception('Some problem occurred')

Trying to catch integrity error with SQLAlchemy

I'm having problems with trying to catch an error. I'm using Pyramid/SQLAlchemy and made a sign up form with email as the primary key. The problem is when a duplicate email is entered it raises a IntegrityError, so I'm trying to catch that error and provide a message but no matter what I do I can't catch it, the error keeps appearing.
try:
new_user = Users(email, firstname, lastname, password)
DBSession.add(new_user)
return HTTPFound(location = request.route_url('new'))
except IntegrityError:
message1 = "Yikes! Your email already exists in our system. Did you forget your password?"
I get the same message when I tried except exc.SQLAlchemyError (although I want to catch specific errors and not a blanket catch all). I also tried exc.IntegrityError but no luck (although it exists in the API).
Is there something wrong with my Python syntax, or is there something I need to do special in SQLAlchemy to catch it?
I don't know how to solve this problem but I have a few ideas of what could be causing the problem. Maybe the try statement isn't failing but succeeding because SQLAlchemy is raising the exception itself and Pyramid is generating the view so the except IntegrityError: never gets activated. Or, more likely, I'm catching this error completely wrong.
In Pyramid, if you've configured your session (which the scaffold does for you automatically) to use the ZopeTransactionExtension, then session is not flushed/committed until after the view has executed. If you want to catch any SQL errors yourself in your view, you need to force a flush to send the SQL to the engine. DBSession.flush() should do it after the add(...).
Update
I'm updating this answer with an example of a savepoint just because there are very few examples around of how to do this with the transaction package.
def create_unique_object(db, max_attempts=3):
while True:
sp = transaction.savepoint()
try:
obj = MyObject()
obj.identifier = uuid.uuid4().hex
db.add(obj)
db.flush()
except IntegrityError:
sp.rollback()
max_attempts -= 1
if max_attempts < 1:
raise
else:
return obj
obj = create_unique_object(DBSession)
Note that even this is susceptible to duplicates between transactions if no table-level locking is used, but it at least shows how to use a savepoint.
What you need to do is catch a general exception and output its class; then you can make the exception more specific.
except Exception as ex:
print ex.__class__
There might be no database operations until DBSession.commit() therefore the IntegrityError is raised later in the stack after the controller code that has try/except has already returned.
This is how I do it.
from contextlib import(
contextmanager,
)
#contextmanager
def session_scope():
"""Provide a transactional scope around a series of operations."""
session = Session()
try:
yield session
session.commit()
except:
session.rollback()
raise
finally:
session.close()
def create_user(email, firstname, lastname, password):
new_user = Users(email, firstname, lastname, password)
try:
with session_scope() as session:
session.add(new_user)
except sqlalchemy.exc.IntegrityError as e:
pass
http://docs.sqlalchemy.org/en/latest/orm/session_basics.html#when-do-i-construct-a-session-when-do-i-commit-it-and-when-do-i-close-it
Edit: The edited answer above is a better way of doing this, using rollback.
--
If you want to handle transactions in the middle of a pyramid application or something where an automatic transaction commit is performed at the end of a sequence, there's no magic that needs to happen.
Just remember to start a new transaction if the previous transaction has failed.
Like this:
def my_view(request):
... # Do things
if success:
try:
instance = self._instance(**data)
DBSession.add(instance)
transaction.commit()
return {'success': True}
except IntegrityError as e: # <--- Oh no! Duplicate unique key
transaction.abort()
transaction.begin() # <--- Start new transaction
return {'success': False}
Notice that calling .commit() on a successful transaction is fine, so it is not necessary to start a new transaction after a successful call.
You only need to abort the transaction and start a new one if the transaction is in a failed state.
(If transaction wasn't such a poc, you could use a savepoint and roll back to the savepoint rather than starting a new transaction; sadly, that is not possible, as attempting a commit invalidates a known previous savepoint. Great stuff huh?) (edit: <--- Turns out I'm wrong about that...)
Catch the exception in a finally statement after flushing the session.
try:
new_user = Users(email, firstname, lastname, password)
DBSession.add(new_user)
return HTTPFound(location = request.route_url('new'))
finally:
try:
DBSession.flush()
except IntegrityError:
message1 = "Yikes! Your email already exists in our system. Did you forget your password?"

Psycopg2 error messages

All,
I am writing error messages to a log file and it's becoming pretty large because it errors on unique constraints in the DB. That is fine and it's actually want it to do so my question is, how can I avoid writing the duplicate key errors to the log file every time they happen?
except Exception as err:
logger.error('FunctionName: %s', err)
Thanks,
Adam
Psycopg passes the PostgreSQL error code along with the exception in the attribute pgcode: you can verify that the error is actually related to a unique violation and avoid logging them. For example:
try:
cursor.execute("...")
except psycopg2.IntegrityError as err:
if err.pgcode != '23505':
logger.error('FunctionName: %s', err)
except Exception as err:
logger.error('FunctionName: %s', err)

Categories