-
-
Notifications
You must be signed in to change notification settings - Fork 13
[RFC] SQLAlchemyUserDatabase with engine injection instead of session injection #9
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
…tion - working and passing tests
When I first implemented this DB adapter, I used an approach where we pass the Indeed, the good thing about the current approach is that the approach doesn't have to know anything about how a session is created. In a FastAPI context, this session is instantiated once when we start to handle the request and closed (or rollback) at the end of the session. This is achieved very easily by using dependency injection, as showed here in the documentation: https://fastapi-users.github.io/fastapi-users/configuration/databases/sqlalchemy/#create-the-database-adapter-dependency async def get_async_session() -> AsyncGenerator[AsyncSession, None]:
async with async_session_maker() as session:
yield session This dependency callable is absolutely key here, and nothing stops you from having something more complex to instantiate it. Typically, it can be overridden during tests so each test can run on a test database and rollback at the end of the test for the next one. Said another way, the database adapter shouldn't be responsible for creating session, this is something that should be done at application level. The key takeaway here is that the FastAPI Users database adapter is included in the request session, not apart. I hope that clarifies things a bit. From my point of view, I don't really feel the need of changing this approach right now. But I'm open to discuss it further. |
Thanks for the response! Everything you said is fair, it seems like just two different approaches to the abstractions. I prefer to have architecture a bit more like:
In my experience, frameworks lean into overly-coupled patterns such as starting a session/transaction in the HTTP handler layer without even using a UnitOfWork abstraction, simply because it offers fast time-to-market and allows the framework to demonstrate value to developers quickly. However this level of coupling leads to tightly-would ball-of-mud apps over time as the codebase grows and ossifies. All in all if you don't agree, I'll figure out another way, no worries. Besides forking, another approach I have considered is to keep this library as-is and just write a I appreciate your work on this project, without it I wouldn't have all this functionality I want with only a 40-line diff. Thanks! |
I totally get what you mean and theoretically, I would tend to agree about an architecture like this. However, in my opinion, the purpose of frameworks and libraries like FastAPI Users is actually to provide easy-to-use, easy to understand and fast time-to-market abstractions. Yes, it makes your code quite opinionated and tightly coupled to those assumptions; and I agree with you that it makes things difficult if one day we want to change the framework, the database engine or whatever. But, who really does this? From my experience, that's really rare. The "bad" abstraction that blocks a project usually comes from the end-developer, not the framework, the ORM or related libraries. For the specific case of FastAPI Users, one of my main frustration right now is that it's already quite difficult to setup and make understand all its blocks and pieces. If we add even more abstraction that's not common in FastAPI projects, I think we'll lost lot of people there. That said, I completely understand that you want to apply this approach, and that's totally fine. FastAPI is indeed very good at giving you choices about how to do things. I think you can achieve something like you want by Implementing a custom database adapter working directly with your infrastructure layer, instead of a SQLAlchemy session or even engine. In other words, it's just a layer between FastAPI Users and your database layer. |
Thanks, I appreciate the input! |
Hi!
Huge fan of fastapi-users.
This library in particular is a huge help to spin up with it quickly, since I prefer to use Postgres + SQLAlchemy backend for my database repository implementations.
However, I find this library hard to use in the standard way I like, which is to perform all dependency injection on startup, instantiating the database engine and injecting it into the repository implementation (whose role is played here by
SQLAlchemyUserDatabase
) only once, in the main file (example here). The repository instance then uses the engine to instantiate the sessions as needed, encapsulating the repo functionality behind a domain-layer interface which is completely agnostic of DB implementation details - no need for the consumer of the interface to know that it's using SQLAlchemy or any other infrastructure details.However, the API of the
SQLAlchemyUserDatabase
class in this library only can take a session, meaning the session must be instantiated outside of the class, then the class must be instantiated each time the request handler is called. I have found it very difficult (maybe impossible?) to maintain proper clean/hexagonal/layered architecture this way, as all callers of the repository methods must supply a database session, which is not something business logic code should be concerned with.Would you be open to introducing something like this into this library? It's not a major change, and I could of course maintain a fork for my own purposes, but I hope it could be better for everyone to include the option in this upstream library that the vast majority of people would use by default.
The changes I have here are of course not compatible with the existing API of
SQLAlchemyUserDatabase
, but are just a proof of concept - I figured I'd wait to go for compatibility after getting your opinion on the matter.The implementations could be made compatible with the addition of the option to provide
engine
orsession
to the class init, then handling things accordingly within thesession
helper method, like:Alternatively, the version with engine injected could be a completely separate class with this slightly different implementation.