-
Notifications
You must be signed in to change notification settings - Fork 15
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
schema: multi-tenancy #1140
base: main
Are you sure you want to change the base?
schema: multi-tenancy #1140
Changes from all commits
3056b22
4537bc1
03119d8
c83a186
5bf3677
1a52076
f046828
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,7 +14,7 @@ | |
from dotenv import load_dotenv | ||
from fastapi import Depends | ||
from rich.logging import RichHandler | ||
from sqlalchemy import AsyncAdaptedQueuePool | ||
from sqlalchemy import AsyncAdaptedQueuePool, text | ||
from sqlalchemy.dialects.postgresql import insert | ||
from sqlalchemy.ext.asyncio import ( | ||
AsyncEngine, | ||
|
@@ -70,6 +70,7 @@ def __init__(self): | |
self.engine: AsyncEngine | None = None | ||
self.session_maker = None | ||
self.session = None | ||
self.schema = None | ||
|
||
def init_db(self): | ||
""" | ||
|
@@ -94,6 +95,11 @@ def init_db(self): | |
}, | ||
) | ||
|
||
if self.schema: | ||
self.engine = self.engine.execution_options( | ||
schema_translate_map={None: self.schema} | ||
) | ||
|
||
async_session_factory = async_sessionmaker( | ||
bind=self.engine, | ||
autocommit=False, | ||
|
@@ -115,17 +121,20 @@ async def close(self): | |
|
||
|
||
@lru_cache(maxsize=None) | ||
def get_session_manager() -> DatabaseSessionManager: | ||
def get_session_manager(request: Optional[Request] = None) -> DatabaseSessionManager: | ||
""" | ||
Get session manager | ||
""" | ||
session_manager = DatabaseSessionManager() | ||
session_manager.schema = request.headers.get("tenant") | ||
settings = get_settings() | ||
settings.customSchema = request.headers.get("new_tenant") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @sadath-12 from our discussions I remember you telling me that you had to do this so that the alembic script would run properly when you init a new tenant's schema, but since this commit the alembic scripts are now configurable to target a specific schema. This means in any of your custom endpoints you could do something like this: from alembic.config import Config
from alembic import command
...
SCHEMA = request.headers.get("tenant") # For example, "tenant1"
if SCHEMA:
alembic_cfg = Config()
alembic_cfg.set_main_option("script_location", "./alembic")
alembic_cfg.cmd_opts = {"x": [f"uri=postgresql+psycopg://username:password@server:5432/{SCHEMA}"]}
command.upgrade(alembic_cfg, "head")
... Which means you don't need to add this customSchema to the settings class at all. |
||
session_manager.init_db() | ||
return session_manager | ||
|
||
|
||
@lru_cache(maxsize=None) | ||
def get_engine() -> AsyncEngine: | ||
def get_engine(schema: str) -> AsyncEngine: | ||
""" | ||
Create the metadata engine. | ||
""" | ||
|
@@ -143,14 +152,16 @@ def get_engine() -> AsyncEngine: | |
"connect_timeout": settings.db_connect_timeout, | ||
}, | ||
) | ||
if schema: | ||
engine = engine.execution_options(schema_translate_map={None: schema}) | ||
return engine | ||
|
||
|
||
async def get_session() -> AsyncIterator[AsyncSession]: | ||
async def get_session(request: Request = None) -> AsyncIterator[AsyncSession]: | ||
""" | ||
Async database session. | ||
""" | ||
session_manager = get_session_manager() | ||
session_manager = get_session_manager(request) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Related to my other comment, I would just pass in the schema here. so: session_manager = get_session_manager(schema=request.headers.get("schema") |
||
session = session_manager.session() | ||
try: | ||
yield session | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@sadath-12 can you help me understand why you need to modify the settings instance directly like this? Also, since you're pulling schema from the "tenant" header, you don't need this settings property at all anymore, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi @samredai , this is to tell dj to switch to the schema when we hit register schema endpoint (created internally) so dj can run migrations on it on runtime or else it will have to be restarted
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @sadath-12 I understand the intent here now. I don't think modifying the application settings is the right approach though. Instead we should parameterize the alembic migration scripts (I think I recall @shangyian proposing this). Then in your custom endpoint logic, you can programmatically set that schema parameter using the context.