Почему возникает ошибка вложенной откатки в SQLAlchemy?
Я получил следующую ошибку в моем коде на 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 ответ(ов)
Проблема была решена. Суть в том, что откат (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() # опционально, зависит от конкретного случая использования
Согласно информации, приведённой @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}.")
Таким образом, предложенные решения помогают избежать проблем с обработкой исключений и корректным выполнением отката в случае возникновения ошибок.
SQLAlchemy: Как использовать ORDER BY по убыванию?
SQLAlchemy: В чем разница между flush() и commit()?
Как создать подзапрос в SQLAlchemy?
SQLAlchemy: выполнение сырого SQL с параметрами в привязках
Создание и чтение временного файла