如何在 Python (Flask) 应用程序中捕获 psycopg2.errors.UniqueViolation 错误?

新手上路,请多包涵

我有一个小型 Python 网络应用程序(用 Flask 编写),它使用 sqlalchemy 将数据保存到数据库。当我尝试插入重复的行时,会引发异常,如下所示:

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

我想包装异常并重新引发我自己的异常,这样我就可以添加我自己的特定于该特定错误的日志记录和消息传递。这是我尝试过的(简化):

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

但这无法捕获错误,因为 psycopg2.errors.UniqueViolation 实际上不是类名(!)。

在 PHP 中,这就像捕获复制/粘贴异常的类名一样简单,但在 Python 中,这要复杂得多。

这里 有一个类似的问题,但它没有处理这个特定的用例,并且(重要的是)它没有阐明如何识别根异常类名。

如何找出实际引发的异常?为什么 Python 隐藏这个?

原文由 Everett 发布,翻译遵循 CC BY-SA 4.0 许可协议

阅读 941
1 个回答

您在问题中发布的错误不是已引发的错误。完整的错误信息是:

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

关键部分是您出于某种原因选择忽略的 SQLAlchemy 错误。 SQLAlchemy 捕获原始错误,将其包装在自己的错误中并引发错误。

但在 Python 中,这要混淆得多……为什么 Python 隐藏这个?

这不是混淆,没有隐藏任何内容,行为已记录,特定于您正在使用的框架,并且不是由 Python 语言强制执行的。 SQLAlchemy 是一个抽象库,如果它引发特定于底层 dpapi 适配器的异常,它将显着降低在其中编写的代码的可移植性。

文档

SQLAlchemy 不会直接生成这些异常。相反,它们从数据库驱动程序中拦截并由 SQLAlchemy 提供的异常 DBAPIError 包装,但是异常中的消息是由驱动程序生成的,而不是 SQLAlchemy。

dbapi 层引发的异常被包装在 sqlalchemy.exc.DBAPIError 的子类中,其中指出:

包装的异常对象在 orig 属性中可用。

因此,捕获 SQLAlchemy 异常并检查原始异常非常简单,正如您所期望的那样,原始异常是 psycopg2.errors.UniqueViolation 的一个实例。但是,除非您的错误处理非常特定于 dbapi 层引发的类型,否则我建议可能不需要检查基础类型,因为引发的 SQLAlchemy 异常将提供足够的运行时信息来执行您必须执行的操作。

这是一个引发 sqlalchemy.exc.IntegrityError 的示例脚本,捕获它,通过 orig 属性检查底层异常并引发一个替代的、本地定义的异常。

 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

这就提出了:

 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

原文由 SuperShoot 发布,翻译遵循 CC BY-SA 4.0 许可协议

推荐问题