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:
QuantumGhost
2025-08-29 19:12:02 +08:00
committed by GitHub
parent 1a34ff8a67
commit d9eb1a73af

View File

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