mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-06 07:56:52 +08:00
refactor(db): 修复异步连接池无界增长,并完成 SQLAlchemy 2.0 迁移与分层归位 (#6320)
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
"""
|
||||
媒体身份的持久化不变量。
|
||||
|
||||
「media_source 与 media_id 必须成对、非零、去空白」这条规则此前由六张表的各个 Oper
|
||||
在建模前各调一次 normalize_media_identity_payload 来保证——靠调用点的纪律,新加一条
|
||||
写入路径忘了调,就会静静写进半对身份,而按身份去重从此对这行失效。
|
||||
|
||||
这里把它下沉成 flush 前的 mapper 事件:凡同时具备两列的表,任何 ORM 写入都会经过,
|
||||
忘不掉也绕不开。app/db 里没有 core insert()/bulk 写法(已核对),因此覆盖是完整的。
|
||||
|
||||
与 DTO 侧的失败语义有意不同,见下方 _normalize_identity 的说明。
|
||||
"""
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import event
|
||||
from sqlalchemy.orm import Mapper
|
||||
|
||||
from app.runtime.log import logger
|
||||
from app.schemas.media import resolve_media_identity
|
||||
|
||||
# 构成媒体身份的两列,缺一不可
|
||||
IDENTITY_COLUMNS = ("media_source", "media_id")
|
||||
|
||||
|
||||
def _normalize_identity(mapper: Mapper, connection: Any, target: Any) -> None:
|
||||
"""
|
||||
写库前归一媒体身份;半对、非法或零值身份清空两列并记一条告警。
|
||||
|
||||
为什么是「清空 + 告警」而不是像 DTO 侧那样抛错:这六张表都是记账性写入(整理历史、
|
||||
下载历史、失败冷却、媒体服务器同步、订阅历史)。因身份不成对就让整条记录写不进去,
|
||||
等于用一个次要字段的问题换掉整条记账——而丢一条整理历史意味着那个文件可能被重复
|
||||
整理。所以持久化侧选择降级保留,但**不再沉默**:告警让半对身份从「查不出的脏数据」
|
||||
变成「日志里可检索的事件」。DTO 侧仍然抛错,那里是用户输入的边界,该当场拒绝。
|
||||
|
||||
:param mapper: 触发事件的映射器
|
||||
:param connection: 本次 flush 使用的连接,未用到
|
||||
:param target: 待写入的模型实例
|
||||
"""
|
||||
columns = mapper.columns.keys()
|
||||
if not all(name in columns for name in IDENTITY_COLUMNS):
|
||||
return
|
||||
raw_source = getattr(target, "media_source", None)
|
||||
raw_id = getattr(target, "media_id", None)
|
||||
if raw_source is None and raw_id is None:
|
||||
return
|
||||
media_source, media_id = resolve_media_identity(
|
||||
media_source=raw_source, media_id=raw_id
|
||||
)
|
||||
if not (media_source and media_id):
|
||||
# 用映射类名而非 local_table.name:后者的静态类型是 FromClause,没有 name
|
||||
logger.warn(
|
||||
f"{mapper.class_.__name__} 的媒体身份不成对,已清空:"
|
||||
f"media_source={raw_source!r}, media_id={raw_id!r}"
|
||||
)
|
||||
target.media_source = media_source.value if media_source else None
|
||||
target.media_id = media_id
|
||||
|
||||
|
||||
def register_identity_normalizer() -> None:
|
||||
"""
|
||||
注册身份归一事件。
|
||||
|
||||
监听 Mapper 类本身而非某个基类:Base 自身没有表、不是映射类,挂不上 mapper 事件;
|
||||
挂在 Mapper 上则覆盖进程内全部映射,包括仓外插件自建的模型。开销由上面那行列名
|
||||
检查兜住——不具备身份列的表直接返回。
|
||||
"""
|
||||
event.listen(Mapper, "before_insert", _normalize_identity)
|
||||
event.listen(Mapper, "before_update", _normalize_identity)
|
||||
|
||||
|
||||
register_identity_normalizer()
|
||||
Reference in New Issue
Block a user