Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Providing new database session for each request #493

Answered by hakanutku
hakanutku asked this question in Q&A
Discussion options

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.

You must be logged in to vote

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

Comment options

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

You must be logged in to vote
0 replies
Comment options

Also it you got a working example, could you please share it here? I could include it into Dependency Injector docs.

You must be logged in to vote
0 replies
Comment options

@hakanutku @rmk135 Hi guys, is there any update on this?

You must be logged in to vote
0 replies
Comment options

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
You must be logged in to vote
0 replies
Answer selected by hakanutku
Comment options

Also @rmk135, is the example I shared okay to include in the docs? If not, what should I change?

You must be logged in to vote
0 replies
Comment options

@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)).

You must be logged in to vote
2 replies
Comment options

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?

Comment options

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

AltStyle によって変換されたページ (->オリジナル) /