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

Eve*_*ett 30 python sqlalchemy

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

(psycopg2.errors.UniqueViolation) duplicate key value violates unique constraint "uix_my_column"
Run Code Online (Sandbox Code Playgroud)

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

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')
Run Code Online (Sandbox Code Playgroud)

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

在 PHP 中,这就像捕获复制/粘贴异常的类名一样简单,但在 Python 中,这更加模糊。

有一个类似的问题在这里,但它并没有解决这个问题的具体使用情况和(重要的),它没有澄清如何识别根异常类名。

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

Sup*_*oot 28

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

sqlalchemy.exc.IntegrityError: (psycopg2.errors.UniqueViolation) duplicate key value violates unique constraint "model_name_key"
Run Code Online (Sandbox Code Playgroud)

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

但在 Python 中,这更加模糊......为什么 Python 隐藏了这一点?

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

文档

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

由DBAPI层引起的异常被包裹在所述的子类sqlalchemy.exc.DBAPIError,其中应注意:

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

因此psycopg2.errors.UniqueViolation,如您所料,捕获 SQLAlchemy 异常并检查原始异常(它是 的实例)非常简单。但是,除非您的错误处理非常特定于 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
Run Code Online (Sandbox Code Playgroud)

这引发了:

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
Run Code Online (Sandbox Code Playgroud)

  • 那是因为它可能不像我的那样在那里。不过,互联网上的人们就是喜欢假设恶意。 (9认同)
  • 哎呀,这种遗漏不是故意的——出于某种原因,我在日志中没有看到“sqlalchemy.exc.IntegrityError”,这让我想知道我们的自定义记录器在做什么。 (3认同)
  • 以下导入语句不起作用。`从 psycopg2.errors 导入 UniqueViolation`。 (2认同)

Eva*_*mir 23

我的答案略有不同,可以避免查找特定的数字错误代码。只需导入定义的常量UNIQUE_VIOLATION

from psycopg2.errorcodes import UNIQUE_VIOLATION
from psycopg2 import errors
Run Code Online (Sandbox Code Playgroud)

然后使用错误查找函数:

except errors.lookup(UNIQUE_VIOLATION) as e:
Run Code Online (Sandbox Code Playgroud)

为我解决了这个问题。您可以根据需要导入其他错误代码常量。


小智 9

根据 psycopg2文档

根据 DB API 2.0,该模块通过以下异常提供有关错误的信息:

异常 psycopg2.Error

异常是所有其他错误异常的基类。您可以使用它通过一个 except 语句捕获所有错误。警告不被视为错误,因此不使用此类作为基础。它是 Python StandardError(Python 3 上的异常)的子类。

因此,捕获异常的正确方法是:

try:
    # your stuff here
except psycopg2.Error as e:
    # get error code
    error = e.pgcode
    # then do something.
Run Code Online (Sandbox Code Playgroud)

根据ErrCodes 表,您的错误尤其是 23505


Fed*_*Baù 8

有关如何使用一些快速食谱导入 psycopg2 UniqueViolation(或任何其他错误)的快速参考。

1) 导入UniqueViolation

   import traceback   # Used for printing the full traceback | Better for debug.
   from psycopg2 import errors

   UniqueViolation = errors.lookup('23505')  # Correct way to Import the psycopg2 errors

   # ...... Code ....

    try:
        db.commit()           
    except UniqueViolation as err:
        traceback.print_exc()
        db.rollback()            

    # ...... Code ....
Run Code Online (Sandbox Code Playgroud)

2) 导入完整性错误

UniqueViolation 基本异常实际上是 IntegrityError ,因此对于更广泛的错误捕获(无论出于何种原因,通常不推荐,但规则意味着被打破)

   import traceback   # Used for printing the full traceback | Better for debug.
   from psycopg2._psycopg import IntegrityError

   # ...... Code ....

    try:
        db.commit()           
    except IntegrityError as err:
        traceback.print_exc()
        db.rollback()            

    # ...... Code ....
Run Code Online (Sandbox Code Playgroud)

3)源代码

psycopg2错误模块位于此处 --> /psycopg2/errors.py,实际上就像真实错误代码列表的网关。

在这里您可以看到用于通过给定代码调用正确错误的函数:

    #
    # NOTE: the exceptions are injected into this module by the C extention.
    #
    
    
    def lookup(code):
        """Lookup an error code and return its exception class.
    
        Raise `!KeyError` if the code is not found.
        """
        from psycopg2._psycopg import sqlstate_errors   # avoid circular import
        return sqlstate_errors[code]
Run Code Online (Sandbox Code Playgroud)

但真正有趣的东西在这里找到 --->\psycopg2\_psycopg\__init__.py 一旦在这里找到变量sqlstate_errors,它是一个包含代码作为值和实际错误作为注释的字典,这是一个小片段(相当大):

    sqlstate_errors = {
        '02000': None, # (!) real value is "<class 'psycopg2.errors.NoData'>"
        '02001': None, # (!) real value is "<class 'psycopg2.errors.NoAdditionalDynamicResultSetsReturned'>"
        '03000': None, # (!) real value is "<class 'psycopg2.errors.SqlStatementNotYetComplete'>"
        '08000': None, # (!) real value is "<class 'psycopg2.errors.ConnectionException'>"
        '08001': None, # (!) real value is "<class 'psycopg2.errors.SqlclientUnableToEstablishSqlconnection'>"
        '08003': None, # (!) real value is "<class 'psycopg2.errors.ConnectionDoesNotExist'>"
        '08004': None, # (!) real value is "<class 'psycopg2.errors.SqlserverRejectedEstablishmentOfSqlconnection'>"
        '08006': None, # (!) real value is "<class 'psycopg2.errors.ConnectionFailure'>"
        '08007': None, # (!) real value is "<class 'psycopg2.errors.TransactionResolutionUnknown'>"
        '08P01': None, # (!) real value is "<class 'psycopg2.errors.ProtocolViolation'>"
 # -------- Lots of lines ---------- # 
        '23503': None, # (!) real value is "<class 'psycopg2.errors.ForeignKeyViolation'>"
        # There you are!!!
        '23505': None, # (!) real value is "<class 'psycopg2.errors.UniqueViolation'>" 
        # ---------------- 
        '23514': None, # (!) real value is "<class 'psycopg2.errors.CheckViolation'>"
        '23P01': None, # (!) real value is "<class 'psycopg2.errors.ExclusionViolation'>"
Run Code Online (Sandbox Code Playgroud)

4) 文档