资讯动态

Teable v2 应用服务层架构解析:领域编排、跨表副作用与事务发布机制

发布时间:2026/9/13 19:43:24 来源:尧图企业网站定制
Teable v2 应用服务层架构解析领域编排、跨表副作用与事务发布机制【免费下载链接】teable✨ AI Spreadsheet for Business项目地址: https://gitcode.com/GitHub_Trending/te/teable导读本文围绕 Teable 仓库中 packages/v2/core/src/application/services/ARCHITECTURE.md 这份架构说明展开深入讲解 v2 核心包中application/services 层的职责划分与八个核心应用服务的实现细节。你将掌握应用服务如何在领域模型 / Visitor / Spec与仓储、事件总线、事务单元之间充当编排者字段创建/删除如何触发跨表副作用批量记录更新与行序重排如何协调持久化、事件与撤销/重做undo/redo。文章同时提供源码级佐证便于后续阅读与二次开发。一、层职责应用服务只做编排不掺领域逻辑ARCHITECTURE.md 对该层给出了三条明确的职责定义实现应用服务协调仓储repositories、Schema 更新与事件发布围绕领域变更与 Spec 提供事务性编排transactional orchestration领域逻辑保留在领域模型/Visitor 中本层只负责连接端口ports并为跨表校验提供预加载数据。用一句话概括领域模型决定改什么应用服务决定怎么改、何时提交、改完通知谁。这一点在源码中有直接体现——每个服务类的注释几乎都重复着同一句约定Domain logic lives in visitors/specs; this class only orchestrates persistence and events.例如 FieldCreationSideEffectService.ts 的类注释即为该约定的样板。这种分层保证Schema 语义如删除链接字段要同步删掉对端的对称链接字段只存在于领域 Visitor/Spec 中应用服务可以被替换、复用而不会污染业务规则。二、八个核心应用服务全景ARCHITECTURE.md 在 Files 一节列出了八个服务文件及其角色。下表完整继承原文档并补充了各自的关键产出服务文件角色核心职责FieldCreationSideEffectService.ts应用服务通过 Visitor 校验跨表字段依赖并在字段创建后施加副作用FieldDeletionSideEffectService.ts应用服务字段删除后施加跨表副作用如移除对称链接字段ForeignTableLoaderService.ts应用服务一次性加载外部表并校验缺失引用TableDeletionSideEffectService.ts应用服务删除表之前在其它表中派发显式的OnTeableTableDeleted反应包括链接转文本与依赖元数据清理RecordBulkUpdateService.ts应用服务准备并执行选择器selector或显式记录批量更新由领域模型构建 Spec/结果服务协调仓储持久化、事件与 undo/redoRecordReorderService.ts应用服务应用批量行序更新为需要原生 v2 重排流程的调用方构建重排事件与 undo/redo 负载TableQueryService.ts应用服务在 CommandHandler 与 QueryHandler 间共享的通用表查询操作getById、getByIdInBase、existsTableUpdateFlow.ts应用服务共享的表更新工作流mutate persist publish下面逐一深入各服务的源码实现。三、ForeignTableLoaderService跨表引用的一次加载 缺失校验链接link、查找lookup等字段天然会引用其它表若每个字段各自去查库一次命令会产生 N 次查询。ForeignTableLoaderService的职责就是每个命令只加载一次外部表并统一校验引用是否完整。其输入类型定义如下见 ForeignTableLoaderService.tsexport type ForeignTableLoaderInput { baseId?: BaseId; references: ReadonlyArrayLinkForeignTableReference; /** * When true, missing foreign tables are skipped instead of failing. * Used by delete-field flows so orphan link/lookup fields remain deletable * after their foreign table was soft-deleted or permanently removed. */ allowMissing?: boolean; };实现要点按 Base 分组批量查询load()先把引用按baseId分组再对每组通过TableAggregate.specs(baseId).byIds(...)构造 Spec、调用tableRepository.find()一次性取回避免逐表查询缺失引用校验查询结束后比对实际返回的表集合若存在missingForeignTableIds且未开启allowMissing则返回domainError.notFound错误详情中携带缺失表 ID 列表allowMissing 的用途删除字段流程中外部表可能已被软删除或永久移除此时允许孤儿链接/查找字段仍然可删除避免删除操作被缺失的表卡死链接标题回填loadForLinkTitleFill()通过MissingLinkTitleForeignTableCollector一个ICellValueSpecVisitor实现遍历SetLinkValueSpec收集需要标题解析needsTitleResolution()的外部表引用再统一加载——它同时做了按foreignTableId的去重。对应的测试 ForeignTableLoaderService.spec.ts 覆盖了无引用返回空、加载被引用表、从引用自带 baseId 加载外部表等场景并使用MemoryTableRepository作为内存仓储可直接作为理解该服务行为的示例。四、字段创建/删除的跨表副作用服务4.1 FieldCreationSideEffectService创建即同步创建一个 link 字段通常需要在关联表创建对称字段、或为 rollup/lookup 建立依赖关系。该服务把有哪些副作用的判断完全交给领域 Visitorpreview()在内存中构建foreignTableState外部表 当前表调用 FieldCreationSideEffectVisitor 的collect()收集副作用随后对每个sideEffect.mutateSpec在状态快照上执行mutate()做预览返回更新后的表集合——不落库、不产生持久事件用于调用方在真正提交前观察影响execute()在真实执行路径中对每个副作用调用tableUpdateFlow.execute(context, { table: foreignTable }, mutateFn, { publishEvents: false })将更新后的表写回状态并收集领域事件。注意publishEvents: false——事件先聚合不发布由上层命令在事务边界统一决定何时发布。FieldCreationSideEffectVisitor是标准的IFieldVisitor实现对于SingleLineTextField、NumberField、FormulaField等绝大多数类型直接返回ok([])无副作用只有 link 相关字段类型会产出{ foreignTable, mutateSpec }副作用描述。4.2 FieldDeletionSideEffectService删除即清理删除字段的跨表副作用例如删除 link 字段后同步移除对端对称链接字段由 FieldDeletionSideEffectService.ts 负责同样先经FieldDeletionSideEffectVisitor.collect()收集副作用对每个副作用走tableUpdateFlow.execute(..., { publishEvents: false })持久化更新并聚合事件特殊之处当mutateSpec instanceof TableRemoveFieldSpec时会把该副作用记录到appliedDeletions携带deletedField、previousTable更新前快照与table更新后供上层在事务提交后做后续清理或审计。五、TableDeletionSideEffectService删表前的群发通知删除一张表时其它表里可能残留指向它的 link/lookup 字段以及相关的元数据。TableDeletionSideEffectService的职责是在被删表真正删除之前让其它表做出反应反应逻辑由领域侧定义的OnTeableTableDeleted协议承载见 TableDeletionSideEffectService.ts。执行流程execute()加载候选表用TableAggregate.specs().byIncomingReferenceToTable(deletedTable.id())查出所有引用被删表的其它表排除自身优先级排序prioritizeReactingFieldIds()对每个候选表按reactionPriority排序字段——直接引用被删表的 link 字段优先优先级 0引用被删表但非 link 的字段其次优先级 1其余字段最后优先级 2确保删除依赖先被解除收集反应collectDeletionReactions()对排序后的字段逐一检查是否implementsOnTeableTableDeleted调用field.value.onTableDeleted(deletedTable, deletionContext)带afterPersist钩子的反应单独隔离执行纯 Spec 类的反应被归入batchableSpecs分批执行隔离反应逐个走tableUpdateFlow.execute()并携带afterPersist钩子可批处理反应则用composeAndSpecs()组合成单个 Spec 一次执行——尽量减少表更新事务次数链接转文本通过createDeletionContext()提供的createFieldUpdateAfterPersistHook把字段更新副作用FieldUpdateSideEffectService接入 afterPersist 阶段实现链接字段在被删表场景下向文本的转换等清理动作。该服务的整体设计意图在类注释中写得很清楚数据变更仍然通过显式的表删除钩子与表 Spec 流出以便适配器adapter把工作留在 SQL 层完成。六、TableUpdateFlow共享的mutate persist publish工作流TableUpdateFlow是本层最核心的编排器字段创建/删除副作用、表更新命令全部复用它。其execute()完整路径如下见 TableUpdateFlow.ts解析目标表resolveTable()支持直接传Table实例或通过baseId tableId走仓储查询表不存在时返回table.not_found错误领域变更调用调用方传入的mutate(table)得到TableUpdateResult并pullDomainEvents()收集宿主表产生的领域事件判断是否需要物理 Schema 修复mayRequirePhysicalSchemaRepair()会递归展平mutateSpec若包含TableAddFieldSpec、TableRemoveFieldSpec、TableUpdateFieldDbFieldNameSpec、UpdateLinkConfigSpec、RemoveSymmetricLinkFieldSpec等或为类型转换TableUpdateFieldTypeSpec.isTypeConversion()则先调用beginTableSchemaOperation登记待执行的 Schema 操作——这是为了处理元数据/数据分库部署下 DDL 先于元数据事务提交的一致性窗口双层事务外层unitOfWork.withTransaction(..., { scope: meta })提交元数据tableRepository.updateOne内层{ scope: data }提交物理 SchematableSchemaRepository.update并把更新后的最新表写入事务作用域recordLatestTableInTransactionScope钩子机制prepare钩子在元数据持久化前执行afterPersist钩子在数据阶段落库后执行——表删除服务的链接转文本正是挂在这里失败兜底事务失败时按元数据是否已落库分别走failRecoverableTableSchemaOperation可恢复或completeTableSchemaOperation不可恢复并保留原始错误提交后收尾通过registerAfterCommit/registerAfterRollback在父事务提交后再将 Schema 操作标记为 ready复用外层数据事务时避免提前标记完成导致表不可用事件版本回填attachPersistedEventVersions()用仓储返回的fieldVersionChanges/viewVersionChanges为FieldUpdated、FieldOptionsAdded、ViewColumnMetaUpdated事件补全oldVersion/newVersion保证事件携带真实的版本号供投影projection消费发布默认publishEvents: true时经eventBus.publishMany发布事件注释明确投影自行拉取数据事件本身不携带投影所需的全量快照。七、记录维度批量更新与行序重排7.1 RecordBulkUpdateService选择器批量更新与显式记录更新RecordBulkUpdateService是记录更新的主入口支持两种更新变体record.update.variant追踪属性可区分selector 变体按筛选条件输入提供fieldValues与filter或recordIds先经FieldKeyResolverService.resolveFieldKeys把字段键解析为字段 ID再buildRecordConditionSpec构建条件 Spec随后在事务内用合成记录 IDrec 16 个 0在tableForWrite.updateRecord上构建mutateSpec若 Spec 需要解析needsResolution()则调用RecordMutationSpecResolverService.resolveAndReplace最后tableRecordRepository.updateMany按条件批量落库explicit 变体按记录列表输入提供records: IRecordBulkUpdateItem[]每条含recordId与fieldValues。流程更重先解析字段键 → 插件准备与守卫 → 加载当前记录 → 授权过滤缺失记录、被插件过滤的记录会被统计剔除→ 事务内updateRecordsStream以EXPLICIT_UPDATE_MAX_BATCH_SIZE 1000为批大小流式生成更新批次逐批解析、持久化并生成RecordUpdateDTO变更数据事件与撤销/重做为每个实际变更生成RecordsBatchUpdated事件source: user同时把oldValue构造成UpdateRecords撤销命令、newValue构造成重做命令与行序命令、副作用撤销计划一起通过undoRedoStackService.appendEntry入栈——注意撤销/重做命令的入栈顺序是相反的redo 先执行副作用再执行更新undo 则相反可观测性批量更新全程埋点BulkUpdateBatchTraceCollector对单条记录 trace 采样上限为 3 条GENERATE_UPDATE_BATCH_RECORD_TRACE_SAMPLE_LIMIT并在 span 上记录批次耗时、字段赋值总数、最大单记录字段数等指标用于定位大批量更新瓶颈。7.2 RecordReorderService原生 v2 重排RecordReorderService提供与RecordInsertOrderviewId anchorId position配套的行序重排通过recordOrderCalculator.calculateOrders()计算每条记录的下一组 order 值批量大小常量UPDATE_BATCH_SIZE 500对每条记录构造SetRowOrderValueSpec(viewId, nextOrder)用updateManyStream分批持久化只对 order 实际变化的记录生成RecordReordered事件携带ordersByRecordId与previousOrdersByRecordId撤销/重做命令统一为ApplyRecordOrdersundo 写回 previousOrder、redo 写回 nextOrder。八、TableQueryServiceHandler 间的共享查询约定TableQueryService是跨 CommandHandler 与 QueryHandler 复用的查表服务其文档注释对何时使用/何时不使用给出了明确约定见 TableQueryService.ts适用Handler 在执行操作前需要按 ID 取表、多个 Handler 共享查询 not-found 检查模式、需要一致的表缺失错误信息不适用复杂查询应直接用仓储 自定义 Spec、已在使用TableUpdateFlow它自带resolveTable、涉及领域逻辑应放领域服务或聚合内。提供的操作getById(context, tableId)按 ID 查询活动表统一返回table.not_found错误错误信息含表 IDgetByIdInBase(context, baseId, tableId)限定 Base 范围查询作为额外的授权/作用域检查错误信息同时含表 ID 与 Base IDgetDeletedByIdInBase(...)以state: deleted查询已删除表供回收站/恢复流程使用exists(context, tableId)存在性检查当前实现仍加载完整聚合注释提示后续可用 count 查询优化。文档注释中还给出了 Handler 内的典型用法// In a CommandHandler: const table yield* await this.tableQueryService.getById(context, command.tableId); const record yield* table.createRecord(command.fieldValues); // With baseId constraint: const table yield* await this.tableQueryService.getByIdInBase( context, command.baseId, command.tableId );九、设计原则总结从 ARCHITECTURE.md 与源码可以提炼出该应用服务层的四条设计原则Visitor 收集副作用、Spec 描述变更跨表副作用的是什么由FieldCreationSideEffectVisitor、FieldDeletionSideEffectVisitor等 Visitor 决定怎么改由TableAddFieldSpec、TableRemoveFieldSpec、SetRowOrderValueSpec等 Spec 描述应用服务从不直接拼装业务规则事务范围显式声明通过unitOfWork.withTransaction的scope: meta | data区分元数据与数据两阶段提交配合TableUpdateTransactionScope、TableSchemaOperationLifecycleService处理分库部署下的 DDL 一致性事件先聚合、后发布副作用服务统一使用publishEvents: false由上层在事务边界决定发布时机避免中间态事件泄漏端口驱动、依赖注入所有服务通过injectable()v2CoreTokens.*注入tableRepository、eventBus、unitOfWork等端口测试可用MemoryTableRepository等内存实现替换见 ForeignTableLoaderService.spec.ts。十、进一步阅读层职责声明packages/v2/core/src/application/services/ARCHITECTURE.md字段创建/删除副作用FieldCreationSideEffectService.ts、FieldDeletionSideEffectService.ts及对应 Visitor FieldCreationSideEffectVisitor.ts表更新核心工作流TableUpdateFlow.ts记录批量更新/重排RecordBulkUpdateService.ts、RecordReorderService.ts表查询约定TableQueryService.ts单元测试样例ForeignTableLoaderService.spec.ts、TableUpdateFlow.spec.ts、TableQueryService.spec.ts同目录下综上application/services 层是 Teable v2 中连接领域模型与基础设施端口的枢纽它用 Visitor/Spec 保持领域纯净用统一的事务流与事件发布保证跨表操作的一致性再用清晰的查询与撤销/重做约定支撑 CommandHandler/QueryHandler 的复用——理解这一层就抓住了整个 v2 核心包的命令处理骨架。【免费下载链接】teable✨ AI Spreadsheet for Business项目地址: https://gitcode.com/GitHub_Trending/te/teable创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价