From 32a94ab105e0531f2acbb609df10fb748ec17a14 Mon Sep 17 00:00:00 2001 From: Windpicker-owo <3431391539@qq.com> Date: Sat, 1 Nov 2025 16:50:50 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=9E=E7=8E=B0=E6=96=B9=E6=A1=88A?= =?UTF-8?q?=20-=20=E7=BC=93=E5=AD=98=E5=AD=97=E5=85=B8=E8=80=8C=E9=9D=9ESQ?= =?UTF-8?q?LAlchemy=E5=AF=B9=E8=B1=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 核心改进: - 添加 _model_to_dict() 和 _dict_to_model() 辅助函数 - CRUD.get/get_by/get_multi 现在缓存字典而非对象 - QueryBuilder.first/all 现在缓存字典而非对象 - 从缓存恢复时重建detached对象,所有字段已加载 优势: - 彻底避免'not bound to Session'错误 - 缓存数据独立于Session生命周期 - 对象反序列化后所有字段可直接访问 - 提高缓存可靠性和数据可用性 技术细节: - 缓存层存储纯字典数据(可序列化) - 查询时在session内预加载所有列 - 返回前转换为字典并缓存 - 缓存命中时从字典重建对象 - 重建的对象虽然detached但所有字段已填充 --- src/common/database/api/crud.py | 93 ++++++++++++++++++++++++-------- src/common/database/api/query.py | 31 ++++++----- 2 files changed, 91 insertions(+), 33 deletions(-) diff --git a/src/common/database/api/crud.py b/src/common/database/api/crud.py index e652072b5..ed6ab24c7 100644 --- a/src/common/database/api/crud.py +++ b/src/common/database/api/crud.py @@ -10,6 +10,7 @@ from typing import Any, Optional, Type, TypeVar from sqlalchemy import and_, delete, func, select, update from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.inspection import inspect from src.common.database.core.models import Base from src.common.database.core.session import get_db_session @@ -27,6 +28,42 @@ logger = get_logger("database.crud") T = TypeVar("T", bound=Base) +def _model_to_dict(instance: Base) -> dict[str, Any]: + """将 SQLAlchemy 模型实例转换为字典 + + Args: + instance: SQLAlchemy 模型实例 + + Returns: + 字典表示,包含所有列的值 + """ + result = {} + for column in instance.__table__.columns: + try: + result[column.name] = getattr(instance, column.name) + except Exception as e: + logger.warning(f"无法访问字段 {column.name}: {e}") + result[column.name] = None + return result + + +def _dict_to_model(model_class: Type[T], data: dict[str, Any]) -> T: + """从字典创建 SQLAlchemy 模型实例 (detached状态) + + Args: + model_class: SQLAlchemy 模型类 + data: 字典数据 + + Returns: + 模型实例 (detached, 所有字段已加载) + """ + instance = model_class() + for key, value in data.items(): + if hasattr(instance, key): + setattr(instance, key, value) + return instance + + class CRUDBase: """基础CRUD操作类 @@ -58,13 +95,14 @@ class CRUDBase: """ cache_key = f"{self.model_name}:id:{id}" - # 尝试从缓存获取 + # 尝试从缓存获取 (缓存的是字典) if use_cache: cache = await get_cache() - cached = await cache.get(cache_key) - if cached is not None: + cached_dict = await cache.get(cache_key) + if cached_dict is not None: logger.debug(f"缓存命中: {cache_key}") - return cached + # 从字典恢复对象 + return _dict_to_model(self.model, cached_dict) # 从数据库查询 async with get_db_session() as session: @@ -72,10 +110,19 @@ class CRUDBase: result = await session.execute(stmt) instance = result.scalar_one_or_none() - # 写入缓存 - if instance is not None and use_cache: - cache = await get_cache() - await cache.set(cache_key, instance) + if instance is not None: + # 预加载所有字段 + for column in self.model.__table__.columns: + try: + getattr(instance, column.name) + except Exception: + pass + + # 转换为字典并写入缓存 + if use_cache: + instance_dict = _model_to_dict(instance) + cache = await get_cache() + await cache.set(cache_key, instance_dict) return instance @@ -95,13 +142,14 @@ class CRUDBase: """ cache_key = f"{self.model_name}:filter:{str(sorted(filters.items()))}" - # 尝试从缓存获取 + # 尝试从缓存获取 (缓存的是字典) if use_cache: cache = await get_cache() - cached = await cache.get(cache_key) - if cached is not None: + cached_dict = await cache.get(cache_key) + if cached_dict is not None: logger.debug(f"缓存命中: {cache_key}") - return cached + # 从字典恢复对象 + return _dict_to_model(self.model, cached_dict) # 从数据库查询 async with get_db_session() as session: @@ -122,10 +170,11 @@ class CRUDBase: except Exception: pass # 忽略访问错误 - # 写入缓存 + # 转换为字典并写入缓存 if use_cache: + instance_dict = _model_to_dict(instance) cache = await get_cache() - await cache.set(cache_key, instance) + await cache.set(cache_key, instance_dict) return instance @@ -149,13 +198,14 @@ class CRUDBase: """ cache_key = f"{self.model_name}:multi:{skip}:{limit}:{str(sorted(filters.items()))}" - # 尝试从缓存获取 + # 尝试从缓存获取 (缓存的是字典列表) if use_cache: cache = await get_cache() - cached = await cache.get(cache_key) - if cached is not None: + cached_dicts = await cache.get(cache_key) + if cached_dicts is not None: logger.debug(f"缓存命中: {cache_key}") - return cached + # 从字典列表恢复对象列表 + return [_dict_to_model(self.model, d) for d in cached_dicts] # 从数据库查询 async with get_db_session() as session: @@ -173,7 +223,7 @@ class CRUDBase: stmt = stmt.offset(skip).limit(limit) result = await session.execute(stmt) - instances = result.scalars().all() + instances = list(result.scalars().all()) # 触发所有实例的列加载,避免 detached 后的延迟加载问题 for instance in instances: @@ -183,10 +233,11 @@ class CRUDBase: except Exception: pass # 忽略访问错误 - # 写入缓存 + # 转换为字典列表并写入缓存 if use_cache: + instances_dicts = [_model_to_dict(inst) for inst in instances] cache = await get_cache() - await cache.set(cache_key, instances) + await cache.set(cache_key, instances_dicts) return instances diff --git a/src/common/database/api/query.py b/src/common/database/api/query.py index e07935646..b34587ba7 100644 --- a/src/common/database/api/query.py +++ b/src/common/database/api/query.py @@ -18,6 +18,9 @@ from src.common.database.core.session import get_db_session from src.common.database.optimization import get_cache, get_preloader from src.common.logger import get_logger +# 导入 CRUD 辅助函数以避免重复定义 +from src.common.database.api.crud import _dict_to_model, _model_to_dict + logger = get_logger("database.query") T = TypeVar("T", bound="Base") @@ -191,13 +194,14 @@ class QueryBuilder(Generic[T]): """ cache_key = ":".join(self._cache_key_parts) + ":all" - # 尝试从缓存获取 + # 尝试从缓存获取 (缓存的是字典列表) if self._use_cache: cache = await get_cache() - cached = await cache.get(cache_key) - if cached is not None: + cached_dicts = await cache.get(cache_key) + if cached_dicts is not None: logger.debug(f"缓存命中: {cache_key}") - return cached + # 从字典列表恢复对象列表 + return [_dict_to_model(self.model, d) for d in cached_dicts] # 从数据库查询 async with get_db_session() as session: @@ -212,10 +216,11 @@ class QueryBuilder(Generic[T]): except Exception: pass - # 写入缓存 + # 转换为字典列表并写入缓存 if self._use_cache: + instances_dicts = [_model_to_dict(inst) for inst in instances] cache = await get_cache() - await cache.set(cache_key, instances) + await cache.set(cache_key, instances_dicts) return instances @@ -227,13 +232,14 @@ class QueryBuilder(Generic[T]): """ cache_key = ":".join(self._cache_key_parts) + ":first" - # 尝试从缓存获取 + # 尝试从缓存获取 (缓存的是字典) if self._use_cache: cache = await get_cache() - cached = await cache.get(cache_key) - if cached is not None: + cached_dict = await cache.get(cache_key) + if cached_dict is not None: logger.debug(f"缓存命中: {cache_key}") - return cached + # 从字典恢复对象 + return _dict_to_model(self.model, cached_dict) # 从数据库查询 async with get_db_session() as session: @@ -248,10 +254,11 @@ class QueryBuilder(Generic[T]): except Exception: pass - # 写入缓存 + # 转换为字典并写入缓存 if instance is not None and self._use_cache: + instance_dict = _model_to_dict(instance) cache = await get_cache() - await cache.set(cache_key, instance) + await cache.set(cache_key, instance_dict) return instance