perf(backend): 角色/世界观入库改批量——create_many 一次 flush 落全部行
角色入库端点与 toolbox world_entities 入库端点原逐卡/逐条 create(每行一次 SELECT/flush/refresh,N+1 round-trip)。新增 create_many:世界观用 add_all, 角色用一次 IN 查已有名 + 一次 flush(保持 (project_id,name) upsert 幂等与批内 重名去重、保序返回)。单条 create 薄封装 create_many,行为不变(既有调用方/E2E 复用)。复活并复用 CharacterWriteFields、新增 WorldEntityFields 作批量入参。 补两端点「多行一次落库」测试;既有幂等/冲突 gate 测试与 m5 真 pg E2E 全绿。
This commit is contained in:
@@ -9,6 +9,7 @@ from ww_core.domain.chapter_repo import (
|
||||
SqlChapterRepo,
|
||||
)
|
||||
from ww_core.domain.character_repo import (
|
||||
CharacterWriteFields,
|
||||
CharacterWriteRepo,
|
||||
CharacterWriteView,
|
||||
SqlCharacterWriteRepo,
|
||||
@@ -73,6 +74,7 @@ from ww_core.domain.template_repo import (
|
||||
)
|
||||
from ww_core.domain.world_entity_repo import (
|
||||
SqlWorldEntityWriteRepo,
|
||||
WorldEntityFields,
|
||||
WorldEntityWriteRepo,
|
||||
WorldEntityWriteView,
|
||||
)
|
||||
@@ -82,9 +84,11 @@ __all__ = [
|
||||
"ChapterRepo",
|
||||
"ChapterView",
|
||||
"SqlChapterRepo",
|
||||
"CharacterWriteFields",
|
||||
"CharacterWriteRepo",
|
||||
"CharacterWriteView",
|
||||
"SqlCharacterWriteRepo",
|
||||
"WorldEntityFields",
|
||||
"WorldEntityWriteRepo",
|
||||
"WorldEntityWriteView",
|
||||
"SqlWorldEntityWriteRepo",
|
||||
|
||||
@@ -75,12 +75,35 @@ class CharacterWriteRepo(Protocol):
|
||||
relations: list[dict[str, Any]],
|
||||
) -> CharacterWriteView: ...
|
||||
|
||||
async def create_many(
|
||||
self, project_id: uuid.UUID, cards: list[CharacterWriteFields]
|
||||
) -> list[CharacterWriteView]:
|
||||
"""批量 upsert 角色卡(一次 flush 落全部),逐卡返回 View(保序、含重名重复)。"""
|
||||
...
|
||||
|
||||
|
||||
class CharacterWriteFields(BaseModel):
|
||||
"""从 `CharacterCard` 拆出的入库字段(端点把 schema 卡转成此形喂 create_many)。"""
|
||||
|
||||
name: str
|
||||
role: str
|
||||
traits: list[str] = Field(default_factory=list)
|
||||
appearance: str = ""
|
||||
motive: str = ""
|
||||
backstory: str
|
||||
arc: str
|
||||
speech_tics: list[str] = Field(default_factory=list)
|
||||
tags: list[Any] = Field(default_factory=list)
|
||||
relations: list[dict[str, Any]] = Field(default_factory=list)
|
||||
|
||||
|
||||
class SqlCharacterWriteRepo:
|
||||
"""SQLAlchemy 实现:upsert 一行 `characters`(schema→DB 形变;只 flush 不 commit)。
|
||||
"""SQLAlchemy 实现:upsert `characters`(schema→DB 形变;只 flush 不 commit)。
|
||||
|
||||
幂等:先按 (project_id, name) 查既有行——命中则原地更新该行字段,否则插新行。
|
||||
幂等:按 (project_id, name) 查既有行——命中则原地更新该行字段,否则插新行。
|
||||
避免同名角色重复入库产生重复卡(QA #8;不加 DB 约束,纯 app 层)。
|
||||
批量入库走 `create_many`:一次 `IN` 查已有名 + 一次 `flush` 落全部新行(少 N 次
|
||||
round-trip),语义与逐卡 `create` 完全一致(含批内重名去重与保序返回)。
|
||||
"""
|
||||
|
||||
def __init__(self, session: AsyncSession) -> None:
|
||||
@@ -101,56 +124,61 @@ class SqlCharacterWriteRepo:
|
||||
tags: list[Any],
|
||||
relations: list[dict[str, Any]],
|
||||
) -> CharacterWriteView:
|
||||
existing = (
|
||||
await self._s.execute(
|
||||
select(Character).where(Character.project_id == project_id, Character.name == name)
|
||||
)
|
||||
).scalar_one_or_none()
|
||||
if existing is not None:
|
||||
existing.role = role
|
||||
existing.traits = _traits_to_jsonb(traits)
|
||||
existing.appearance = appearance
|
||||
existing.motive = motive
|
||||
existing.backstory = backstory
|
||||
existing.arc = _arc_to_jsonb(arc)
|
||||
existing.speech_tics = _traits_to_jsonb(speech_tics)
|
||||
existing.tags = list(tags)
|
||||
existing.relations = [dict(r) for r in relations]
|
||||
await self._s.flush()
|
||||
await self._s.refresh(existing)
|
||||
return CharacterWriteView(id=existing.id, name=existing.name, role=existing.role)
|
||||
row = Character(
|
||||
project_id=project_id,
|
||||
"""单卡 upsert——薄封装 `create_many`,行为不变(供既有调用方/E2E 复用)。"""
|
||||
fields = CharacterWriteFields(
|
||||
name=name,
|
||||
role=role,
|
||||
traits=_traits_to_jsonb(traits),
|
||||
traits=list(traits),
|
||||
appearance=appearance,
|
||||
motive=motive,
|
||||
backstory=backstory,
|
||||
arc=_arc_to_jsonb(arc),
|
||||
speech_tics=_traits_to_jsonb(speech_tics),
|
||||
arc=arc,
|
||||
speech_tics=list(speech_tics),
|
||||
tags=list(tags),
|
||||
relations=[dict(r) for r in relations],
|
||||
relations=list(relations),
|
||||
)
|
||||
self._s.add(row)
|
||||
await self._s.flush()
|
||||
await self._s.refresh(row)
|
||||
return CharacterWriteView(id=row.id, name=row.name, role=row.role)
|
||||
return (await self.create_many(project_id, [fields]))[0]
|
||||
|
||||
def _apply_fields(self, row: Character, card: CharacterWriteFields) -> None:
|
||||
"""把入库字段写到 ORM 行(schema→DB JSONB 形变;新旧行共用)。"""
|
||||
row.role = card.role
|
||||
row.traits = _traits_to_jsonb(card.traits)
|
||||
row.appearance = card.appearance
|
||||
row.motive = card.motive
|
||||
row.backstory = card.backstory
|
||||
row.arc = _arc_to_jsonb(card.arc)
|
||||
row.speech_tics = _traits_to_jsonb(card.speech_tics)
|
||||
row.tags = list(card.tags)
|
||||
row.relations = [dict(r) for r in card.relations]
|
||||
|
||||
# ---- request-shaping helper(schema 字段名导出供测试/端点共用)----
|
||||
|
||||
|
||||
class CharacterWriteFields(BaseModel):
|
||||
"""从 `CharacterCard` 拆出的入库字段(端点把 schema 卡转成此 kwargs 形)。"""
|
||||
|
||||
name: str
|
||||
role: str
|
||||
traits: list[str] = Field(default_factory=list)
|
||||
appearance: str = ""
|
||||
motive: str = ""
|
||||
backstory: str
|
||||
arc: str
|
||||
speech_tics: list[str] = Field(default_factory=list)
|
||||
tags: list[Any] = Field(default_factory=list)
|
||||
relations: list[dict[str, Any]] = Field(default_factory=list)
|
||||
async def create_many(
|
||||
self, project_id: uuid.UUID, cards: list[CharacterWriteFields]
|
||||
) -> list[CharacterWriteView]:
|
||||
if not cards:
|
||||
return []
|
||||
names = [c.name for c in cards]
|
||||
existing = (
|
||||
(
|
||||
await self._s.execute(
|
||||
select(Character).where(
|
||||
Character.project_id == project_id, Character.name.in_(names)
|
||||
)
|
||||
)
|
||||
)
|
||||
.scalars()
|
||||
.all()
|
||||
)
|
||||
by_name: dict[str, Character] = {r.name: r for r in existing}
|
||||
rows: list[Character] = []
|
||||
for card in cards:
|
||||
row = by_name.get(card.name)
|
||||
if row is None:
|
||||
row = Character(project_id=project_id, name=card.name)
|
||||
self._apply_fields(row, card)
|
||||
self._s.add(row)
|
||||
by_name[card.name] = row
|
||||
else:
|
||||
self._apply_fields(row, card)
|
||||
rows.append(row) # 逐卡收集(同名多卡指向同一行 → 保序返回含重复)。
|
||||
await self._s.flush() # 一次 flush 落全部新行;server_default id 经 RETURNING 回填。
|
||||
return [CharacterWriteView(id=r.id, name=r.name, role=r.role) for r in rows]
|
||||
|
||||
@@ -36,6 +36,14 @@ def _rules_to_jsonb(rules: list[str]) -> dict[str, Any]:
|
||||
return {"rules": list(rules)}
|
||||
|
||||
|
||||
class WorldEntityFields(BaseModel):
|
||||
"""单条世界观实体入库字段(端点把 schema 卡转成此形喂 create_many)。"""
|
||||
|
||||
type: str
|
||||
name: str
|
||||
rules: list[str]
|
||||
|
||||
|
||||
class WorldEntityWriteRepo(Protocol):
|
||||
"""世界观实体写侧接口(按 project_id 隔离;只 flush 不 commit)。"""
|
||||
|
||||
@@ -48,9 +56,19 @@ class WorldEntityWriteRepo(Protocol):
|
||||
rules: list[str],
|
||||
) -> WorldEntityWriteView: ...
|
||||
|
||||
async def create_many(
|
||||
self, project_id: uuid.UUID, entities: list[WorldEntityFields]
|
||||
) -> list[WorldEntityWriteView]:
|
||||
"""批量插入世界观实体(一次 flush 落全部),逐条保序返回 View。"""
|
||||
...
|
||||
|
||||
|
||||
class SqlWorldEntityWriteRepo:
|
||||
"""SQLAlchemy 实现:插一行 `world_entities`(rules list→dict;只 flush 不 commit)。"""
|
||||
"""SQLAlchemy 实现:插 `world_entities`(rules list→dict;只 flush 不 commit)。
|
||||
|
||||
批量入库走 `create_many`:`add_all` + 一次 `flush`(少 N 次 round-trip),
|
||||
语义与逐条 `create` 一致;server_default id 经 RETURNING 回填。
|
||||
"""
|
||||
|
||||
def __init__(self, session: AsyncSession) -> None:
|
||||
self._s = session
|
||||
@@ -63,13 +81,24 @@ class SqlWorldEntityWriteRepo:
|
||||
name: str,
|
||||
rules: list[str],
|
||||
) -> WorldEntityWriteView:
|
||||
row = WorldEntity(
|
||||
project_id=project_id,
|
||||
type=type,
|
||||
name=name,
|
||||
rules=_rules_to_jsonb(rules),
|
||||
)
|
||||
self._s.add(row)
|
||||
await self._s.flush()
|
||||
await self._s.refresh(row)
|
||||
return WorldEntityWriteView(id=row.id, type=row.type, name=row.name)
|
||||
"""单条插入——薄封装 `create_many`,行为不变(供既有调用方复用)。"""
|
||||
entity = WorldEntityFields(type=type, name=name, rules=list(rules))
|
||||
return (await self.create_many(project_id, [entity]))[0]
|
||||
|
||||
async def create_many(
|
||||
self, project_id: uuid.UUID, entities: list[WorldEntityFields]
|
||||
) -> list[WorldEntityWriteView]:
|
||||
if not entities:
|
||||
return []
|
||||
rows = [
|
||||
WorldEntity(
|
||||
project_id=project_id,
|
||||
type=e.type,
|
||||
name=e.name,
|
||||
rules=_rules_to_jsonb(e.rules),
|
||||
)
|
||||
for e in entities
|
||||
]
|
||||
self._s.add_all(rows)
|
||||
await self._s.flush() # 一次 flush 落全部;server_default id 经 RETURNING 回填。
|
||||
return [WorldEntityWriteView(id=r.id, type=r.type, name=r.name) for r in rows]
|
||||
|
||||
Reference in New Issue
Block a user