Сохранить прерванную транзакцию, когда SQLAlchemy вызывает ProgrammingError

У меня немного необычная проблема с состоянием транзакции и обработкой ошибок в SQLAlchemy. Краткая версия: есть ли способ сохранить транзакцию, когда SQLAlchemy вызывает ProgrammingError и прерывает его?

Фон

Я работаю над набором тестов интеграции для устаревшей базы кода. Прямо сейчас я разрабатываю набор приборов, которые позволят нам запускать все тесты внутри транзакций, вдохновленные документация по SQLAlchemy . Общая парадигма включает в себя открытие соединения, запуск транзакции, привязку сеанса к этому соединению, а затем моделирование большинства методов доступа к базе данных, чтобы они использовали эту транзакцию. (Чтобы понять, как это выглядит, см. Код, приведенный в ссылке на документацию выше, включая примечание в конце.) Цель состоит в том, чтобы позволить себе запускать методы из кодовой базы, которые выполняют много обновлений базы данных в контекст теста с гарантией того, что любые побочные эффекты, которые могут изменить базу данных теста, будут отменены после завершения теста.

Моя проблема в том, что код часто полагается на обработку ошибок DBAPI для выполнения потока управления при выполнении запросов, и эти ошибки автоматически прерывают транзакции (за документы psycopg2 ). Это создает проблему, поскольку мне нужно сохранить работу, которая была выполнена в этой транзакции, до момента возникновения ошибки, и мне нужно продолжать использовать транзакцию после того, как обработка ошибок завершена.

Вот типичный метод, который использует обработку ошибок для потока управления:

from api.database import engine

def entity_count(): 
    """
    Count the entities in a project.
    """

    get_count = ''' 
        SELECT COUNT(*) AS entity_count FROM entity_browser 
    ''' 

    with engine.begin() as conn:
        try: 
            count = conn.execute(count).first().entity_count 
        except ProgrammingError: 
            count = 0 

return count 

В этом примере обработка ошибок позволяет быстро определить, существует ли таблица entity_browser: если нет, Postgres выдаст ошибка, которая обнаруживается на уровне DBAPI (psycopg2) и передается в SQLAlchemy как ProgrammingError.

В тестах я макетирую engine.begin(), чтобы он всегда возвращал соединение с текущей транзакцией, которая была установлена ​​в настройке теста , К сожалению, это означает, что когда код продолжает выполнение после того, как SQLAlchemy поднял ProgrammingError и psycopg2 прервал транзакцию, SQLAlchemy вызовет InternalError при следующем запуске запроса к базе данных с использованием открытого соединения с жалобой на то, что транзакция была прервана.

Вот пример теста, демонстрирующий такое поведение:

import sqlalchemy as sa

def test_entity_count(session):
    """
    Test the `entity_count` method.

    `session` is a fixture that sets up the transaction and mocks out
    database access, returning a Flask-SQLAlchemy `scoped_session` object
    that we can use for queries.
    """

    # Make a change to a table that we can observe later
    session.execute('''
        UPDATE users
        SET name = 'in a test transaction'
        WHERE id = 1
    ''')

    # Drop `entity_browser` in order to raise a `ProgrammingError` later
    session.execute('''DROP TABLE entity_browser''')

    # Run the `entity_count` method, making sure that it raises an error
    with pytest.raises(sa.exc.ProgrammingError):
        count = entity_count()

    assert count == 0

    # Make sure that the changes we made earlier in the test still exist
    altered_name = session.execute('''
        SELECT name
        FROM users
        WHERE id = 1
    ''')

    assert altered_name == 'in a test transaction'

Вот тип вывода, который я получаю:

> altered_name = session.execute('''
      SELECT name
      FROM users
      WHERE id = 1
  ''')

[... traceback history...]

def do_execute(self, cursor, statement, parameters, context=None):
>   cursor.execute(statement, parameters)
E   sqlalchemy.exc.InternalError: (psycopg2.InternalError) current transaction is
    aborted, commands ignored until end of transaction block

Попытки решения

Моим первым инстинктом было попытаться прервать обработку ошибок и вызвать откат с помощью handle_error слушатель событий . Я добавил слушателя в тестовое устройство, которое откатило бы необработанное соединение (поскольку экземпляры SQLAlchemy Connection не имеют API отката, насколько я понять это):

@sa.event.listens_for(connection, 'handle_error')
def raise_error(context):
    dbapi_conn = context.connection.connection
    dbapi_conn.rollback()

Это успешно сохраняет транзакцию открытой для дальнейшего использования, но в итоге откатывает все предыдущие изменения, сделанные в тесте. Пример вывода:

> assert altered_name == 'in a test transaction'
E AssertionError

Очевидно, что откат необработанного соединения слишком агрессивен в подходе. Думая, что мне удастся откатиться до последней точки сохранения, я попытался откатить сеанс с заданной областью, к которому подключен прослушиватель событий, который автоматически открывает новую вложенную транзакцию по завершении предыдущей. (См. Примечание в конце документа по SQLAlchemy на транзакции в тестах для примера того, как это выглядит.)

Благодаря макетам, настроенным в приборе session, я могу импортировать сессию с областью действия непосредственно в приемник событий и откатить его

@sa.event.listens_for(connection, 'handle_error')
def raise_error(context):
    from api.database import db
    db.session.rollback()

Однако этот подход также вызывает InternalError для следующего запроса. Похоже, что он не выполняет откат транзакции до удовлетворения основного курсора.

Краткий вопрос

Есть ли способ сохранить транзакцию после того, как ProgrammingError будет вызвано? На более абстрактном уровне, что происходит, когда psycopg2 «прерывает» транзакцию, и как я могуобойти это?

4 голоса | спросил jeancochrane 25 AMpWed, 25 Apr 2018 03:55:19 +030055Wednesday 2018, 03:55:19

1 ответ


0
Корень проблемы в том, что вы скрываете исключение от диспетчера контекста.Вы перехватываете ---- +: = 0 =: + ---- слишком рано, и оператор with никогда его не увидит.Ваш ---- +: = 1 =: + ---- должен быть:И тогда, если вы предоставите что-то вродекак высмеивается ---- +: = 4 =: + ---- , соединение остается пригодным для использования.Но @JL Peyret поднимает вопрос о логике вашего теста.---- +: = 5 =: + ---- обычно 1 предоставляет новое соединение с вооруженной транзакцией из пула, поэтому ваши ---- +: = 6 =: + ---- и ---- +: = 7 =: + ----, вероятно, даже не должно использовать одно и то же соединение.1 : зависит от конфигурации пула.
ответил Ilja Everilä 26 PMpThu, 26 Apr 2018 17:49:25 +030049Thursday 2018, 17:49:25

Похожие вопросы

Популярные теги

security × 330linux × 316macos × 2827 × 268performance × 244command-line × 241sql-server × 235joomla-3.x × 222java × 189c++ × 186windows × 180cisco × 168bash × 158c# × 142gmail × 139arduino-uno × 139javascript × 134ssh × 133seo × 132mysql × 132