diff --git a/app/db/decorators.py b/app/db/decorators.py index 15305805c..95c7266a0 100644 --- a/app/db/decorators.py +++ b/app/db/decorators.py @@ -43,6 +43,14 @@ def run_legacy_sync_query(operation: Callable[[Session], _R]) -> _R: except Exception as close_err: # noqa: BLE001 兼容查询释放失败不改变返回语义 logger.error(f"释放数据库会话失败:{close_err}") + +async def run_legacy_async_query( + operation: Callable[[AsyncSession], Awaitable[_R]], +) -> _R: + """为移除异步查询装饰器的旧 Model ABI 提供一次性异步会话。""" + async with async_session_scope() as db: + return await operation(db) + def _get_args_db( args: tuple[Any, ...], kwargs: dict[str, Any], diff --git a/app/db/models/siteicon.py b/app/db/models/siteicon.py index e176ae7d5..3d420755b 100644 --- a/app/db/models/siteicon.py +++ b/app/db/models/siteicon.py @@ -4,7 +4,7 @@ from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import Mapped, Session, mapped_column from app.db.base import Base, get_id_column -from app.db.decorators import db_query, async_db_query +from app.db.decorators import run_legacy_async_query class SiteIcon(Base): @@ -22,12 +22,25 @@ class SiteIcon(Base): base64: Mapped[Optional[str]] = mapped_column(String) @classmethod - @db_query def get_by_domain(cls, db: Session, domain: str): + """在调用方 Session 中查询站点图标。""" return db.execute(select(cls).where(cls.domain == domain)).scalars().first() @classmethod - @async_db_query - async def async_get_by_domain(cls, db: AsyncSession, domain: str): - result = await db.execute(select(cls).where(cls.domain == domain)) - return result.scalar_one_or_none() + async def async_get_by_domain( + cls, + db: AsyncSession | None = None, + domain: str | None = None, + ): + """在调用方 AsyncSession 中查询站点图标。""" + if domain is None: + raise TypeError("domain is required") + + async def query(session: AsyncSession): + """在给定异步会话中执行站点图标查询。""" + result = await session.execute(select(cls).where(cls.domain == domain)) + return result.scalar_one_or_none() + + if isinstance(db, AsyncSession): + return await query(db) + return await run_legacy_async_query(query) diff --git a/app/db/models/sitestatistic.py b/app/db/models/sitestatistic.py index 69b645ac8..7d7ddf061 100644 --- a/app/db/models/sitestatistic.py +++ b/app/db/models/sitestatistic.py @@ -6,7 +6,7 @@ from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import Mapped, Session, mapped_column from app.db.base import get_id_column, Base -from app.db.decorators import db_query, async_db_query +from app.db.decorators import run_legacy_async_query class SiteStatistic(Base): @@ -30,15 +30,28 @@ class SiteStatistic(Base): note: Mapped[Optional[Any]] = mapped_column(JSON) @classmethod - @db_query def get_by_domain(cls, db: Session, domain: str): + """在调用方 Session 中查询站点统计。""" return db.execute(select(cls).where(cls.domain == domain)).scalars().first() @classmethod - @async_db_query - async def async_get_by_domain(cls, db: AsyncSession, domain: str): - result = await db.execute(select(cls).where(cls.domain == domain)) - return result.scalar_one_or_none() + async def async_get_by_domain( + cls, + db: AsyncSession | None = None, + domain: str | None = None, + ): + """在调用方 AsyncSession 中查询站点统计,并兼容旧无会话调用。""" + if domain is None: + raise TypeError("domain is required") + + async def query(session: AsyncSession): + """在给定异步会话中执行站点统计查询。""" + result = await session.execute(select(cls).where(cls.domain == domain)) + return result.scalar_one_or_none() + + if isinstance(db, AsyncSession): + return await query(db) + return await run_legacy_async_query(query) @classmethod def reset(cls, db: Session): diff --git a/app/db/models/userconfig.py b/app/db/models/userconfig.py index 01c5129de..47be8ed46 100644 --- a/app/db/models/userconfig.py +++ b/app/db/models/userconfig.py @@ -3,7 +3,6 @@ from sqlalchemy import String, UniqueConstraint, JSON, select from sqlalchemy.orm import Mapped, Session, mapped_column from app.db.base import get_id_column, Base -from app.db.decorators import db_query class UserConfig(Base): @@ -24,8 +23,8 @@ class UserConfig(Base): ) @classmethod - @db_query def get_by_key(cls, db: Session, username: str, key: str): + """在调用方 Session 中查询用户配置。""" return db.execute( select(cls).where(cls.username == username, cls.key == key) ).scalars().first() diff --git a/docs/refactor/backend-architecture-next-stage.md b/docs/refactor/backend-architecture-next-stage.md index 32aa3637a..7e48238a3 100644 --- a/docs/refactor/backend-architecture-next-stage.md +++ b/docs/refactor/backend-architecture-next-stage.md @@ -1042,6 +1042,8 @@ runtime provider,旧插件或测试替换模块级 `settings` 时仍保持原 无 Session ABI;用户查询与兼容专项 75 项测试、Pylint 及架构基线通过,查询装饰器由 119 个降至 117 个。 随后将 Agent 系统设置查询/更新工具切换到已装配的 `RuntimeSettingsService` 窄端口,保留工具构造和 设置更新返回 ABI;配置债务由 103 个文件降至 101 个文件,系统设置工具专项测试与架构基线通过。 +站点图标、站点统计和用户配置的只读 Model 方法随后改为显式 Session 执行,异步无 Session 旧 ABI 由 +一次性兼容查询会话承接;查询装饰器由 117 个降至 112 个,站点查询专项测试与架构基线通过。 同日修正适配器配置下沉边界:OCR、CookieCloud、DoH、Rust 和资源签名等低层实现不再直接依赖 `app.application`,由 `app.runtime.settings` 端口承接组合根注入;未启动装配时仍回退旧 Settings ABI, diff --git a/tests/fixtures/architecture/dependency-baseline.json b/tests/fixtures/architecture/dependency-baseline.json index 3da9f7f3a..7dbcad279 100644 --- a/tests/fixtures/architecture/dependency-baseline.json +++ b/tests/fixtures/architecture/dependency-baseline.json @@ -13,8 +13,8 @@ "runtime_to_db": [], "workflow_to_db": [] }, - "edge_count": 6420, - "edge_sha256": "7c586d713c6014eb485763f8b7704d9421d95ba7d145910a33163dcb36882914", + "edge_count": 6419, + "edge_sha256": "bc2f57394915b9f90921a91cc4630a4426975ee765ece75a1aa0640080e41b4f", "edges": [ "app -> app.runtime", "app -> app.runtime.compat", @@ -3557,7 +3557,6 @@ "app.db.models.user -> app.db.decorators", "app.db.models.userconfig -> app.db", "app.db.models.userconfig -> app.db.base", - "app.db.models.userconfig -> app.db.decorators", "app.db.models.workflow -> app.db", "app.db.models.workflow -> app.db.base", "app.db.models.workflow -> app.db.decorators", diff --git a/tests/fixtures/architecture/transaction-debt-baseline.json b/tests/fixtures/architecture/transaction-debt-baseline.json index b9636b031..23ecdb103 100644 --- a/tests/fixtures/architecture/transaction-debt-baseline.json +++ b/tests/fixtures/architecture/transaction-debt-baseline.json @@ -1,12 +1,12 @@ { "model_decorators": { "by_kind": { - "async_db_query": 49, + "async_db_query": 47, "async_db_update": 0, - "db_query": 68, + "db_query": 65, "db_update": 0 }, - "count": 117, + "count": 112, "methods": [ { "decorator": "async_db_query", @@ -263,26 +263,6 @@ "file": "app/db/models/site.py", "method": "Site.list_order_by_pri" }, - { - "decorator": "async_db_query", - "file": "app/db/models/siteicon.py", - "method": "SiteIcon.async_get_by_domain" - }, - { - "decorator": "db_query", - "file": "app/db/models/siteicon.py", - "method": "SiteIcon.get_by_domain" - }, - { - "decorator": "async_db_query", - "file": "app/db/models/sitestatistic.py", - "method": "SiteStatistic.async_get_by_domain" - }, - { - "decorator": "db_query", - "file": "app/db/models/sitestatistic.py", - "method": "SiteStatistic.get_by_domain" - }, { "decorator": "async_db_query", "file": "app/db/models/siteuserdata.py", @@ -548,11 +528,6 @@ "file": "app/db/models/user.py", "method": "User.async_get_by_name" }, - { - "decorator": "db_query", - "file": "app/db/models/userconfig.py", - "method": "UserConfig.get_by_key" - }, { "decorator": "async_db_query", "file": "app/db/models/workflow.py",