Не откатываются изменения в транзакции SQLAlchemy
P.S. В целом можно сказать, что я хочу реализовать работу с БД как в Django: изменения автоматически фиксируются (совсем автоматически конечно не получится, тк придется вручную вызывать метод commit), а если нужно выполнить в транзакции, то создается внешняя транзакция и к ней подцепляется сессия.
Мне нужно сделать такую dependency, которая бы отдавала сессию SQLAlchemy и при это могла откатить закоммиченные изменения. Для этого написал следующий код:
from typing import Annotated
import sqlalchemy as sa
import uvicorn
from fastapi import FastAPI, Depends
from pydantic import BaseModel, Field
from sqlalchemy import MetaData
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
from sqlalchemy.orm import declarative_base
app = FastAPI()
metadata = MetaData()
Base = declarative_base(metadata=metadata)
engine = create_async_engine("postgresql+asyncpg://postgres:postgres@localhost:5433/portal-podrjadchika-local", isolation_level="AUTOCOMMIT", echo=True)
SessionLocal = async_sessionmaker(bind=engine, expire_on_commit=False)
class User(Base):
__tablename__ = "users"
id = sa.Column(sa.Integer, autoincrement=True, primary_key=True, index=True)
first_name = sa.Column(sa.String)
last_name = sa.Column(sa.String)
class InputSchema(BaseModel):
first_name: Annotated[str, Field(example="Tony")]
last_name: Annotated[str, Field(example="Stark")]
async def get_session() -> AsyncSession:
connection = await engine.connect()
transaction = await connection.begin()
session = AsyncSession(bind=connection)
await session.begin()
try:
print("отдали сессию")
yield session
print("перед коммитом")
await session.commit()
print("закоммитили")
except Exception as e:
print(f"ошибка: {e}")
print("откатываемся")
await connection.rollback() # не откатывает
print('откатились. ререйзим')
raise e
finally:
await session.close()
await transaction.close()
await connection.close()
@app.on_event("startup")
async def on_startup() -> None:
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.drop_all)
await conn.run_sync(Base.metadata.create_all)
async def create_user(session, data):
user = User(**data)
session.add(user)
await session.commit()
return user
@app.post('/users')
async def get_users(data: InputSchema, session: AsyncSession = Depends(get_session)):
user = await create_user(session, data.model_dump())
raise ValueError("Симуляция исключения после коммита")
return user
if __name__ == "__main__":
uvicorn.run("so:app", reload=True)
При исключении следующий вывод:
2024-09-04 12:52:04,250 INFO sqlalchemy.engine.Engine select pg_catalog.version()
2024-09-04 12:52:04,250 INFO sqlalchemy.engine.Engine [raw sql] ()
2024-09-04 12:52:04,254 INFO sqlalchemy.engine.Engine select current_schema()
2024-09-04 12:52:04,254 INFO sqlalchemy.engine.Engine [raw sql] ()
2024-09-04 12:52:04,257 INFO sqlalchemy.engine.Engine show standard_conforming_strings
2024-09-04 12:52:04,257 INFO sqlalchemy.engine.Engine [raw sql] ()
2024-09-04 12:52:04,259 INFO sqlalchemy.engine.Engine BEGIN (implicit; DBAPI should not BEGIN due to autocommit mode)
отдали сессию
2024-09-04 12:52:04,261 INFO sqlalchemy.engine.Engine INSERT INTO users (first_name, last_name) VALUES ($1::VARCHAR, $2::VARCHAR) RETURNING users.id
2024-09-04 12:52:04,261 INFO sqlalchemy.engine.Engine [generated in 0.00014s] ('Tony', 'Stark')
ошибка: Симуляция исключения после коммита
откатываемся
2024-09-04 12:52:04,296 INFO sqlalchemy.engine.Engine ROLLBACK using DBAPI connection.rollback(), DBAPI should ignore due to autocommit mode
откатились. ререйзим
INFO: 127.0.0.1:51870 - "POST /users HTTP/1.1" 500 Internal Server Error
ERROR: Exception in ASGI application
Traceback (most recent call last):
File "/Users/alber.aleksandrov/PycharmProjects/Playground/venv3.10/lib/python3.10/site-packages/uvicorn/protocols/http/h11_impl.py", line 408, in run_asgi
result = await app( # type: ignore[func-returns-value]
File "/Users/alber.aleksandrov/PycharmProjects/Playground/venv3.10/lib/python3.10/site-packages/uvicorn/middleware/proxy_headers.py", line 84, in __call__
return await self.app(scope, receive, send)
File "/Users/alber.aleksandrov/PycharmProjects/Playground/venv3.10/lib/python3.10/site-packages/fastapi/applications.py", line 1054, in __call__
await super().__call__(scope, receive, send)
File "/Users/alber.aleksandrov/PycharmProjects/Playground/venv3.10/lib/python3.10/site-packages/starlette/applications.py", line 123, in __call__
await self.middleware_stack(scope, receive, send)
File "/Users/alber.aleksandrov/PycharmProjects/Playground/venv3.10/lib/python3.10/site-packages/starlette/middleware/errors.py", line 186, in __call__
raise exc
File "/Users/alber.aleksandrov/PycharmProjects/Playground/venv3.10/lib/python3.10/site-packages/starlette/middleware/errors.py", line 164, in __call__
await self.app(scope, receive, _send)
File "/Users/alber.aleksandrov/PycharmProjects/Playground/venv3.10/lib/python3.10/site-packages/starlette/middleware/exceptions.py", line 65, in __call__
await wrap_app_handling_exceptions(self.app, conn)(scope, receive, send)
File "/Users/alber.aleksandrov/PycharmProjects/Playground/venv3.10/lib/python3.10/site-packages/starlette/_exception_handler.py", line 64, in wrapped_app
raise exc
File "/Users/alber.aleksandrov/PycharmProjects/Playground/venv3.10/lib/python3.10/site-packages/starlette/_exception_handler.py", line 53, in wrapped_app
await app(scope, receive, sender)
File "/Users/alber.aleksandrov/PycharmProjects/Playground/venv3.10/lib/python3.10/site-packages/starlette/routing.py", line 756, in __call__
await self.middleware_stack(scope, receive, send)
File "/Users/alber.aleksandrov/PycharmProjects/Playground/venv3.10/lib/python3.10/site-packages/starlette/routing.py", line 776, in app
await route.handle(scope, receive, send)
File "/Users/alber.aleksandrov/PycharmProjects/Playground/venv3.10/lib/python3.10/site-packages/starlette/routing.py", line 297, in handle
await self.app(scope, receive, send)
File "/Users/alber.aleksandrov/PycharmProjects/Playground/venv3.10/lib/python3.10/site-packages/starlette/routing.py", line 77, in app
await wrap_app_handling_exceptions(app, request)(scope, receive, send)
File "/Users/alber.aleksandrov/PycharmProjects/Playground/venv3.10/lib/python3.10/site-packages/starlette/_exception_handler.py", line 64, in wrapped_app
raise exc
File "/Users/alber.aleksandrov/PycharmProjects/Playground/venv3.10/lib/python3.10/site-packages/starlette/_exception_handler.py", line 53, in wrapped_app
await app(scope, receive, sender)
File "/Users/alber.aleksandrov/PycharmProjects/Playground/venv3.10/lib/python3.10/site-packages/starlette/routing.py", line 72, in app
response = await func(request)
File "/Users/alber.aleksandrov/PycharmProjects/Playground/venv3.10/lib/python3.10/site-packages/fastapi/routing.py", line 278, in app
raw_response = await run_endpoint_function(
File "/Users/alber.aleksandrov/PycharmProjects/Playground/venv3.10/lib/python3.10/site-packages/fastapi/routing.py", line 191, in run_endpoint_function
return await dependant.call(**values)
File "/Users/alber.aleksandrov/PycharmProjects/Playground/fstp/so.py", line 72, in get_users
raise ValueError("Симуляция исключения после коммита") # симуляция исключения после коммита
ValueError: Симуляция исключения после коммита
isolation_level равный AUTOCOMMIT нужен намеренно и по умолчанию - хочу сделать автокоммиты поведением по умолчанию, а если нужно выполнить в транзакции, то используется внешняя транзакция, к которой сессия присоединяется. И насколько я понял из экспериментов, дело как раз в AUTOCOMMIT. Почему так происходит? Я читал, и вроде бы так не должно быть. Как это пофиксить?