fix(api): fix DetachedInstanceError
for Account.current_tenant_id (#24789)
The `Account._current_tenant` object is loaded by a database session (typically `db.session`) whose lifetime is not aligned with the Account model instance. This misalignment causes a `DetachedInstanceError` to be raised when accessing attributes of `Account._current_tenant` after the original session has been closed. To resolve this issue, we now reload the tenant object with `expire_on_commit=False`, ensuring the tenant remains accessible even after the session is closed.
This commit is contained in:
@@ -1,12 +1,12 @@
|
|||||||
import enum
|
import enum
|
||||||
import json
|
import json
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Optional, cast
|
from typing import Optional
|
||||||
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
from flask_login import UserMixin
|
from flask_login import UserMixin
|
||||||
from sqlalchemy import DateTime, String, func, select
|
from sqlalchemy import DateTime, String, func, select
|
||||||
from sqlalchemy.orm import Mapped, mapped_column, reconstructor
|
from sqlalchemy.orm import Mapped, Session, mapped_column, reconstructor
|
||||||
|
|
||||||
from models.base import Base
|
from models.base import Base
|
||||||
|
|
||||||
@@ -118,10 +118,24 @@ class Account(UserMixin, Base):
|
|||||||
|
|
||||||
@current_tenant.setter
|
@current_tenant.setter
|
||||||
def current_tenant(self, tenant: "Tenant"):
|
def current_tenant(self, tenant: "Tenant"):
|
||||||
ta = db.session.scalar(select(TenantAccountJoin).filter_by(tenant_id=tenant.id, account_id=self.id).limit(1))
|
with Session(db.engine, expire_on_commit=False) as session:
|
||||||
if ta:
|
tenant_join_query = select(TenantAccountJoin).where(
|
||||||
self.role = TenantAccountRole(ta.role)
|
TenantAccountJoin.tenant_id == tenant.id, TenantAccountJoin.account_id == self.id
|
||||||
self._current_tenant = tenant
|
)
|
||||||
|
tenant_join = session.scalar(tenant_join_query)
|
||||||
|
tenant_query = select(Tenant).where(Tenant.id == tenant.id)
|
||||||
|
# TODO: A workaround to reload the tenant with `expire_on_commit=False`, allowing
|
||||||
|
# access to it after the session has been closed.
|
||||||
|
# This prevents `DetachedInstanceError` when accessing the tenant outside
|
||||||
|
# the session's lifecycle.
|
||||||
|
# (The `tenant` argument is typically loaded by `db.session` without the
|
||||||
|
# `expire_on_commit=False` flag, meaning its lifetime is tied to the web
|
||||||
|
# request's lifecycle.)
|
||||||
|
tenant_reloaded = session.scalars(tenant_query).one()
|
||||||
|
|
||||||
|
if tenant_join:
|
||||||
|
self.role = TenantAccountRole(tenant_join.role)
|
||||||
|
self._current_tenant = tenant_reloaded
|
||||||
return
|
return
|
||||||
self._current_tenant = None
|
self._current_tenant = None
|
||||||
|
|
||||||
@@ -130,23 +144,19 @@ class Account(UserMixin, Base):
|
|||||||
return self._current_tenant.id if self._current_tenant else None
|
return self._current_tenant.id if self._current_tenant else None
|
||||||
|
|
||||||
def set_tenant_id(self, tenant_id: str):
|
def set_tenant_id(self, tenant_id: str):
|
||||||
tenant_account_join = cast(
|
query = (
|
||||||
tuple[Tenant, TenantAccountJoin],
|
select(Tenant, TenantAccountJoin)
|
||||||
(
|
.where(Tenant.id == tenant_id)
|
||||||
db.session.query(Tenant, TenantAccountJoin)
|
.where(TenantAccountJoin.tenant_id == Tenant.id)
|
||||||
.where(Tenant.id == tenant_id)
|
.where(TenantAccountJoin.account_id == self.id)
|
||||||
.where(TenantAccountJoin.tenant_id == Tenant.id)
|
|
||||||
.where(TenantAccountJoin.account_id == self.id)
|
|
||||||
.one_or_none()
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
with Session(db.engine, expire_on_commit=False) as session:
|
||||||
if not tenant_account_join:
|
tenant_account_join = session.execute(query).first()
|
||||||
return
|
if not tenant_account_join:
|
||||||
|
return
|
||||||
tenant, join = tenant_account_join
|
tenant, join = tenant_account_join
|
||||||
self.role = TenantAccountRole(join.role)
|
self.role = TenantAccountRole(join.role)
|
||||||
self._current_tenant = tenant
|
self._current_tenant = tenant
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def current_role(self):
|
def current_role(self):
|
||||||
|
Reference in New Issue
Block a user