-
-
Notifications
You must be signed in to change notification settings - Fork 337
-
Hi,
I'm using dependency-injector and SQLAlchemy with FastAPI. I have a provider that creates a new session. This provider is used in the repositories in the following way.
db = providers.Singleton(Database, db_url=config.database_url) db_session = providers.Callable(db.provided.create_session.call()) example_repository = providers.Factory(ExampleRepository, session=db_session)
The problem is, when I use multiple repositories in an endpoint, a new database session is created for every repository. So when there is an error in one of the repositories, there is no way to rollback all changes. Is there a way to create a single session in a request context and share it across repositories?
I have thought about overriding and resetting the provider in middlewares. But I think it won't work because the container instance is shared between requests.
Beta Was this translation helpful? Give feedback.
All reactions
Sorry for the super late answer @SarloAkrobata, I had some really busy workdays lately. Here is the solution I found:
First, I tried to use ContextLocalSingleton as suggested by @rmk135 , but I couldn't manage to use the same session for the endpoint, dependencies and middlewares. I don't remember exactly, but I think FastAPI was executing middlewares in a separate context. My alternate solution was to create the context manually.
# database.py looks roughly like this _request_id_ctx_var: contextvars.ContextVar[str] = contextvars.ContextVar('request_id_ctx') def get_request_id() -> str: return _request_id_ctx_var.get() @contextmanager def db_context(identifier: str): ctx_token
Replies: 6 comments 2 replies
-
Hey @hakanutku ,
Good question. Yeah, FastAPI request context is a known pain point. Try to use ContextLocalSingleton
provider for db_session
. It's not documented, but it should be what you're looking for. At least a good start point. From what I know FastAPI doesn't work with contextvars for request scope out of the box, but there is a some kind of third party middleware for it. I used ContextLocalSingleton
with aiohttp
that manages contextvars for request scope and it did exactly what you're looking for. So I believe there is a good chance to make it work with FastAPI.
Source code: https://github.com/ets-labs/python-dependency-injector/blob/master/src/dependency_injector/providers.pyx#L2997-L3047
API docs: https://python-dependency-injector.ets-labs.org/api/providers.html?highlight=contextlocalsingleton#dependency_injector.providers.ContextLocalSingleton
Beta Was this translation helpful? Give feedback.
All reactions
-
Also it you got a working example, could you please share it here? I could include it into Dependency Injector docs.
Beta Was this translation helpful? Give feedback.
All reactions
-
@hakanutku @rmk135 Hi guys, is there any update on this?
Beta Was this translation helpful? Give feedback.
All reactions
-
Sorry for the super late answer @SarloAkrobata, I had some really busy workdays lately. Here is the solution I found:
First, I tried to use ContextLocalSingleton as suggested by @rmk135 , but I couldn't manage to use the same session for the endpoint, dependencies and middlewares. I don't remember exactly, but I think FastAPI was executing middlewares in a separate context. My alternate solution was to create the context manually.
# database.py looks roughly like this _request_id_ctx_var: contextvars.ContextVar[str] = contextvars.ContextVar('request_id_ctx') def get_request_id() -> str: return _request_id_ctx_var.get() @contextmanager def db_context(identifier: str): ctx_token = _request_id_ctx_var.set(identifier) yield _request_id_ctx_var.reset(ctx_token) class Database: def __init__(self, db_url: str, pool_size: int) -> None: self.db_url = db_url self._engine = create_async_engine( self.db_url, echo=False, pool_size=pool_size, ) self._create_factory() def _create_factory(self, bind: Union[Connection, Engine, None] = None) -> None: bind = bind or self._engine self.async_session_factory = sessionmaker( bind=bind, class_=_AsyncSession, expire_on_commit=False, autoflush=False, ) self.AsyncSession = async_scoped_session(self.async_session_factory, scopefunc=get_request_id) def create_session(self): return self.AsyncSession() # In application.py I set up middlewares to set the context @app.middleware('http') async def remove_db_session(request: Request, call_next): response = await call_next(request) db = container.db() await db.AsyncSession.remove() return response @app.middleware('http') async def set_db_context(request: Request, call_next): request_id = str(uuid.uuid4()) with db_context(identifier=request_id): response = await call_next(request) return response # And in containers.py class Container(containers.DeclarativeContainer): db = providers.Singleton(Database, db_url=config.database.url, pool_size=config.database.pool_size) db_session = providers.Callable(db.provided.create_session.call()) user_repository = providers.Factory(UserRepository, session=db_session)
Disclaimers:
- I haven't tested the performance implications of generating a uuid for every request. But I don't think it should be a big overhead
- The order of middlewares is important, but as far I tested, FastAPI guarentees the execution order
Beta Was this translation helpful? Give feedback.
All reactions
-
Also @rmk135, is the example I shared okay to include in the docs? If not, what should I change?
Beta Was this translation helpful? Give feedback.
All reactions
-
@hakanutku , what do you think about using Dependecy
class for db_session
inside container and then instantiating it for each request and injecting DB session?
class Container(DeclarativeContainer):
db_session = Dependency(instance_of=Session)
class ServiceContainerMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) -> Response:
db_session = SessionLocal()
Container(db_session=db_session)
try:
return await call_next(request)
finally:
db_session.close()
I originally posted it here: #495 (comment)
2 potential downsides:
- performance penalty for instantiating container per request
- DB session is opened for each request even if it's not used
EDIT: Seems like this is not the correct solution since it prevent's me to override client's inside tests where I have to instantiate container and inject DB (Container(db_session).some_client.override(client_mock)
).
Beta Was this translation helpful? Give feedback.
All reactions
-
hey, 2024, any news or new ideas?
the dependency_injector
library should allow us to register scoped instances. In this case, we want to create an instance of DB (or other class) for the lifetime of the HTTP request.
How can that be achieved with FastAPI?
Beta Was this translation helpful? Give feedback.
All reactions
-
dependency_injector
library actually do have a way to register scoped instances using Context Local Singleton. But from my experience, it is not usable with FastAPI because FastAPI runs endpoint dependencies and middlewares in separate contexts. That's why I used a custom ContextVar and middleware to create my own scope. If you don't need to use the same singleton in middlewares and dependencies, I think you can just use ContextLocalSingleton and it works.
Beta Was this translation helpful? Give feedback.