How do I catch a psycopg2.errors.UniqueViolation error in a Python (Flask) app?

北城余情 提交于 2021-02-04 11:57:56

问题


I have a small Python web app (written in Flask) that uses sqlalchemy to persist data to the database. When I try to insert a duplicate row, an exception is raised, something like this:

(psycopg2.errors.UniqueViolation) duplicate key value violates unique constraint "uix_my_column"

I would like to wrap the exception and re-raise my own so I can add my own logging and messaging that is specific to that particular error. This is what I tried (simplified):

from db import DbApi
from my_exceptions import BadRequest
from psycopg2.errors import UniqueViolation # <-- this does not exist!

class MyClass:
    def __init__(self):
        self.db = DbApi() 

    def create(self, data: dict) -> MyRecord:
        try:
            with self.db.session_local(expire_on_commit=False) as session:
                my_rec = MyRecord(**data)
                session.add(my_rec)
                session.commit()
                session.refresh(my_rec)
                return my_rec
        except UniqueViolation as e:
            raise BadRequest('A duplicate record already exists')

But this fails to trap the error because psycopg2.errors.UniqueViolation isn't actually a class name (!).

In PHP, this would be as easy as catching copy/pasting the classname of the exception, but in Python, this is much more obfuscated.

There was a similar question here, but it didn't deal with this specific use-case and (importantly), it did not clarify how one can identify the root exception class name.

How does one find out what exception is actually being raised? Why does Python hide this?


回答1:


The error that you have posted in your question isn't the error that has been raised. The full error message is:

sqlalchemy.exc.IntegrityError: (psycopg2.errors.UniqueViolation) duplicate key value violates unique constraint "model_name_key"

The key part being the SQLAlchemy error which you've chosen to omit for some reason. SQLAlchemy catches the original error, wraps it in it's own error and raises that.

but in Python, this is much more obfuscated... Why does Python hide this?

This isn't obfuscation, nothing is hidden, the behavior is documented, specific to the frameworks that you are using and is not enforced by the Python language. SQLAlchemy is an abstraction library and if it were to raise exceptions specific to the underlying dpapi adapter, it would significantly reduce the portability of code written within it.

From the docs:

SQLAlchemy does not generate these exceptions directly. Instead, they are intercepted from the database driver and wrapped by the SQLAlchemy-provided exception DBAPIError, however the messaging within the exception is generated by the driver, not SQLAlchemy.

Exceptions raised by the dbapi layer are wrapped in a subclass of the sqlalchemy.exc.DBAPIError, where it is noted:

The wrapped exception object is available in the orig attribute.

So it's very straightforward to catch the SQLAlchemy exception and inspect the original exception, which is an instance of psycopg2.errors.UniqueViolation, as you'd expect. However, unless your error handling is very specific to the type raised by the dbapi layer, I'd suggest that inspecting the underlying type might be unnecessary as the SQLAlchemy exception that is raised will provide enough runtime information to do what you have to do.

Here is an example script that raises a sqlalchemy.exc.IntegrityError, catches it, inspects the underlying exception through the orig attribute and raises an alternate, locally-defined exception.

from sqlalchemy import create_engine, Column, Integer, String
from sqlalchemy.exc import IntegrityError
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
from psycopg2.errors import UniqueViolation


engine = create_engine("postgresql+psycopg2://some-user:mysecretpassword@localhost:5432/some-user")

Base = declarative_base()
Session = sessionmaker(bind=engine)


class BadRequest(Exception):
    pass


class Model(Base):
    __tablename__ = "model"
    id = Column(Integer, primary_key=True)
    name = Column(String, unique=True)


if __name__ == "__main__":
    Base.metadata.drop_all(engine)
    Base.metadata.create_all(engine)
    s = Session()
    s.add(Model(name="a"))
    s.commit()
    s.add(Model(name="a"))
    try:
        s.commit()
    except IntegrityError as e:
        assert isinstance(e.orig, UniqueViolation)  # proves the original exception
        raise BadRequest from e

And that raises:

sqlalchemy.exc.IntegrityError: (psycopg2.errors.UniqueViolation) duplicate key value violates unique constraint "model_name_key"
DETAIL:  Key (name)=(a) already exists.

[SQL: INSERT INTO model (name) VALUES (%(name)s) RETURNING model.id]
[parameters: {'name': 'a'}]
(Background on this error at: http://sqlalche.me/e/gkpj)

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File ".\main.py", line 36, in <module>
    raise BadRequest from e
__main__.BadRequest



回答2:


According to psycopg2 docs:

In compliance with the DB API 2.0, the module makes informations about errors available through the following exceptions:

exception psycopg2.Error

Exception that is the base class of all other error exceptions. You can use this to catch all errors with one single except statement. Warnings are not considered errors and thus not use this class as base. It is a subclass of the Python StandardError (Exception on Python 3).

Thus, the proper way to catch the exceptions is:

try:
    # your stuff here
except psycopg2.Error as e:
    # get error code
    error = e.pgcode
    # then do something.

Yours in particular is error 23505 according to the ErrCodes Table



来源:https://stackoverflow.com/questions/58740043/how-do-i-catch-a-psycopg2-errors-uniqueviolation-error-in-a-python-flask-app

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!