0

Почему возникает ошибка вложенной откатки в SQLAlchemy?

10

Я получил следующую ошибку в моем коде на Python, который собирает статусы с Twitter и сохраняет их в базу данных:

sqlalchemy.exc.InvalidRequestError: This Session's transaction has been rolled back by a nested rollback() call.  To begin a new transaction, issue Session.rollback() first.

Я хочу понять, в чем заключается проблема, почему она возникает и как я могу ее решить.

У меня нет ясного представления о том, что такое nested rollback. Можете ли вы привести простой пример, в котором может произойти nested rollback?

2 ответ(ов)

0

Проблема была решена. Суть в том, что откат (rollback) не выполняется до тех пор, пока мы явно не вызовем rollback. Поэтому, когда мы используем commit(), следует написать его внутри блока try, а rollback() — в блоке except (в большинстве случаев), как указано в документации: https://docs.sqlalchemy.org/en/13/faq/sessions.html#this-session-s-transaction-has-been-rolled-back-due-to-a-previous-exception-during-flush-or-similar.

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

try:
    <используйте сессию>
    session.commit()
except:
    session.rollback()
    raise
finally:
    session.close()  # опционально, зависит от конкретного случая использования
0

Согласно информации, приведённой @fbessho выше, это действительно правильный шаблон:

try:
    <использовать сессию>
    session.commit()
except:
    session.rollback()

Однако есть некоторые тонкости, которые могут нарушить обработку ошибок.

В приведённом примере (воображаемое нарушение уникального ограничения) откат (rollback) не происходит:

class Thing1(Base):
   id = Column(BigInteger, primary_key=True)

class Thing2(Base):
   id = Column(BigInteger, primary_key=True)

def do_something(s: session, thing_1: Thing1, duplicate_id):

   # предположим, что это нарушает уникальное ограничение в Thing2
   thing_2 = Thing2(id=duplicate_id)
   s.add(thing_2)

   try:
       # исключение произойдёт при выполнении команды commit
       s.commit()
   except Exception as ex:
       # это запишет детали исключения в лог
       logger.error(f"{ex.__class__.__name__}: {ex}")
       # обращение к thing_1.id вызовет второе исключение
       logger.error(f"Commit failed. Thing1 id was {thing_1.id}.")
       s.rollback()

Это второе исключение возникает, даже несмотря на то, что thing_1 не связано с неудачным вставкой. Простое обращение к thing_1 вызывает второе исключение, что предотвращает выполнение отката.

Решение 1

Это требует немного больше усилий, но всегда будет работать.

def do_something_1(s: session, thing_1: Thing1, duplicate_id):

   # создаем ссылку, которая не зависит от объекта данных
   id_for_thing = thing_1.id

   # предположим, что это нарушает уникальное ограничение в Thing2
   thing_2 = Thing2(id=duplicate_id)
   s.add(thing_2)

   try:
       # исключение произойдёт при выполнении команды commit
       s.commit()
   except Exception as ex:
       logger.error(f"{ex.__class__.__name__}: {ex}")
       # нет прямой ссылки на thing_1
       logger.error(f"Commit failed. Thing1 id was {id_for_thing}.")
       s.rollback()

Решение 2

Это будет работать, если thing_1 не затронут откатом.

def do_something_2(s: session, thing_1: Thing1, duplicate_id):

   # предположим, что это нарушает уникальное ограничение в Thing2
   thing_2 = Thing2(id=duplicate_id)
   s.add(thing_2)

   try:
       # исключение произойдёт при выполнении команды commit
       s.commit()
   except Exception as ex:
       logger.error(f"{ex.__class__.__name__}: {ex}")
       s.rollback()
       # к thing_1.id можно обращаться после отката
       logger.error(f"Commit failed. Thing1 id was {thing_1.id}.")

Таким образом, предложенные решения помогают избежать проблем с обработкой исключений и корректным выполнением отката в случае возникновения ошибок.

Чтобы ответить на вопрос, пожалуйста, войдите или зарегистрируйтесь