瀏覽代碼

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.
tags/1.8.1
QuantumGhost 2 月之前
父節點
當前提交
d9eb1a73af
沒有連結到貢獻者的電子郵件帳戶。
共有 1 個檔案被更改,包括 32 行新增22 行删除
  1. 32
    22
      api/models/account.py

+ 32
- 22
api/models/account.py 查看文件

@@ -1,12 +1,12 @@
import enum
import json
from datetime import datetime
from typing import Optional, cast
from typing import Optional

import sqlalchemy as sa
from flask_login import UserMixin
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

@@ -118,10 +118,24 @@ class Account(UserMixin, Base):

@current_tenant.setter
def current_tenant(self, tenant: "Tenant"):
ta = db.session.scalar(select(TenantAccountJoin).filter_by(tenant_id=tenant.id, account_id=self.id).limit(1))
if ta:
self.role = TenantAccountRole(ta.role)
self._current_tenant = tenant
with Session(db.engine, expire_on_commit=False) as session:
tenant_join_query = select(TenantAccountJoin).where(
TenantAccountJoin.tenant_id == tenant.id, TenantAccountJoin.account_id == self.id
)
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
self._current_tenant = None

@@ -130,23 +144,19 @@ class Account(UserMixin, Base):
return self._current_tenant.id if self._current_tenant else None

def set_tenant_id(self, tenant_id: str):
tenant_account_join = cast(
tuple[Tenant, TenantAccountJoin],
(
db.session.query(Tenant, TenantAccountJoin)
.where(Tenant.id == tenant_id)
.where(TenantAccountJoin.tenant_id == Tenant.id)
.where(TenantAccountJoin.account_id == self.id)
.one_or_none()
),
query = (
select(Tenant, TenantAccountJoin)
.where(Tenant.id == tenant_id)
.where(TenantAccountJoin.tenant_id == Tenant.id)
.where(TenantAccountJoin.account_id == self.id)
)
if not tenant_account_join:
return
tenant, join = tenant_account_join
self.role = TenantAccountRole(join.role)
self._current_tenant = tenant
with Session(db.engine, expire_on_commit=False) as session:
tenant_account_join = session.execute(query).first()
if not tenant_account_join:
return
tenant, join = tenant_account_join
self.role = TenantAccountRole(join.role)
self._current_tenant = tenant

@property
def current_role(self):

Loading…
取消
儲存