Не откатываются изменения в транзакции 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. Почему так происходит? Я читал, и вроде бы так не должно быть. Как это пофиксить?


Ответы (0 шт):