Как поймать ошибку psycopg2.errors.UniqueViolation в приложении Python (Flask)? - PullRequest
0 голосов
/ 07 ноября 2019

У меня есть небольшое веб-приложение 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 это скрывает?

Ответы [ 2 ]

1 голос
/ 07 ноября 2019

Ошибка, которую вы опубликовали в своем вопросе, не является ошибкой, которая возникла. Полное сообщение об ошибке:

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

Ключевой частью является ошибка SQLAlchemy, которую вы по какой-то причине решили пропустить. SQLAlchemy улавливает исходную ошибку, оборачивает ее в свою собственную ошибку и выдает ее.

но в Python это гораздо более запутанно ... Почему Python скрывает это?

Это не запутывание, ничего не скрыто, поведение задокументировано, специфично для используемых вами фреймворков и не поддерживается языком Python. SQLAlchemy - это библиотека абстракций, и если бы она вызвала исключения, специфичные для базового адаптера dpapi, это значительно уменьшило бы переносимость кода, написанного внутри него.

Из документов :

SQLAlchemy не генерирует эти исключения напрямую. Вместо этого они перехватываются из драйвера базы данных и переносятся исключением DBAIError, предоставляемым SQLAlchemy, однако обмен сообщениями в исключении генерируется драйвером, а не 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
0 голосов
/ 07 ноября 2019

Согласно psycopg2 docs :

В соответствии с DB API 2.0, модуль предоставляет информацию об ошибках через следующие исключения:

исключение psycopg2.Error

Исключение, являющееся базовым классом всех других исключений ошибок. Вы можете использовать это, чтобы перехватить все ошибки одним оператором кроме. Предупреждения не считаются ошибками и поэтому не используют этот класс в качестве базового. Это подкласс Python StandardError (Exception on Python 3).

Таким образом, правильный способ перехвата исключений:

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

В частности, у вас ошибка 23505согласно таблице ErrCodes

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...