尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

Baserow 撤销/重做(Undo/Redo)技术指南:ActionType、Action 表与 ActionHandler 全解析

发布时间:2026/9/17 19:49:26

资讯中心
01
ARTICLE

Baserow 撤销/重做(Undo/Redo)技术指南:ActionType、Action 表与 ActionHandler 全解析

Baserow 撤销/重做(Undo/Redo)技术指南:ActionType、Action 表与 ActionHandler 全解析
Baserow 撤销/重做Undo/Redo技术指南ActionType、Action 表与 ActionHandler 全解析【免费下载链接】baserowBuild databases, automations, apps agents with AI — no code. Open source platform available on cloud and self-hosted. GDPR, HIPAA, SOC 2 compliant. Best Airtable alternative.项目地址: https://gitcode.com/GitHub_Trending/ba/baserowBaserow 的后端内置了一套完整、可扩展的撤销/重做机制允许用户在表格、数据库、工作区等任意层级回退或重放自己刚刚执行过的操作。本文以 docs/technical/undo-redo-guide.md 为骨架结合 backend/src/baserow/core/action 与 backend/src/baserow/contrib/database/table/actions.py 等源码系统讲解ActionType抽象、Action数据表、ActionHandler的 undo/redo 流程、作用域scope过滤以及失败回退语义。读完本文你将能理解 Baserow 撤销/重做的完整调用链并掌握为任意业务动作新增可撤销 ActionType 的实战方法。一、核心抽象ActionType 定义 do / undo / redo 三态Baserow 的撤销/重做建立在**动作Action**这一抽象之上。一个ActionType是一个类它定义了某个特定操作如何被do执行、undo撤销、redo重做。它可以自由调用 Handler 来完成业务逻辑但几乎不应调用其他 ActionType——除非将来出现某种 meta Action 类型。ActionType 会通过注册表按类型type取出并由 API 方法触发调用例如文档中的典型调用action_type_registry.get_by_type(DeleteWorkspaceAction).do(user, workspace_to_delete)在 backend/src/baserow/core/action/registries.py 中可以看到完整的类体系ActionType抽象基类必须实现do并通过register_action落库UndoableActionTypeMixin为ActionType补充undo/redo抽象方法并实现真正的register_actionregistries.py#L300-L373UndoableActionType组合UndoableActionTypeMixin与ActionType的最终基类绝大部分可撤销动作继承它UndoableActionCustomCleanupMixin可选混入允许在动作被清理时执行额外的数据清理逻辑action_type_registry模块级注册表实例ActionTypeRegistryname action_type。必须实现的三个方法一个ActionType必须实现以下三个方法通过UndoableActionTypeMixin约定do在用户请求执行动作时执行实际操作必须调用cls.register_action保存一条Action记录。undo撤销do所做的事不得保存任何新的Action记录否则撤销本身会污染历史栈。签名固定为undo(cls, user, params, action_being_undone)。redo在undo之后重做该动作同样不得保存任何Action记录。签名固定为redo(cls, user, params, action_being_redone)。从 registries.py#L300-L331 的注释可以确认设计约束undo 绝不应调用另一个 ActionType 的 do 方法因为那会注册一条我们不希望出现的新动作。Params 数据类撤销/重做的参数载体每个ActionType还必须实现一个内部的Paramsdataclass用于保存撤销/重做所需的一切参数。流程为do方法把本次操作的关键信息填入Params实例cls.register_action(user, params, scope, workspace)将Params序列化为 JSON 存入Action表的params字段当undo/redo被调用时后端从Action行的 JSON 重新构造出该 dataclass 实例action_type.serialized_to_params(action.params)并传入undo/redo函数。ActionType还提供两个可覆写的钩子registries.py#L189-L204params_to_serializable(params)在序列化前改写参数对象serialized_to_params(serialized_params)默认实现为cls.Params(**deepcopy(serialized_params))把 JSON 字典还原成Params实例。一个完整的示例UpdateTableActionType以文档中反复提及的表格重命名为例backend/src/baserow/contrib/database/table/actions.py#L224-L293 中的UpdateTableActionType展示了完整的实现模式class UpdateTableActionType(UndoableActionType): type update_table description ActionTypeDescription( _(Update table), _(Table (%(table_id)s) name changed from %(original_table_name)s to %(table_name)s), DATABASE_ACTION_CONTEXT, ) analytics_params [database_id, table_id] dataclasses.dataclass class Params: database_id: int database_name: str table_id: int table_name: str original_table_name: str classmethod def do(cls, user, table, name): original_table_name table.name TableHandler().update_table(user, table, namename) database table.database params cls.Params(database.id, database.name, table.id, name, original_table_name) cls.register_action(user, params, cls.scope(database.id), workspacedatabase.workspace) return table classmethod def scope(cls, database_id): return ApplicationActionScopeType.value(database_id) classmethod def undo(cls, user, params, action_being_undone): TableHandler().update_table_by_id(user, params.table_id, nameparams.original_table_name) classmethod def redo(cls, user, params, action_being_redone): TableHandler().update_table_by_id(user, params.table_id, nameparams.table_name)关键点一目了然do先执行真实的改名操作再把旧名original_table_name和新名一起放进Paramsundo用original_table_name把名字改回去redo用params.table_name再次改名type update_table作为注册表中的唯一标识也直接写入Action表的type字段。二、Action 表撤销/重做的持久化存储每次do成功UndoableActionTypeMixin.register_action都会创建一条Action记录。模型定义在 backend/src/baserow/core/action/models.py与文档中的示例表结构一一对应字段类型说明idserial自增主键user_idFK - user 表可空执行动作的用户撤销/重做时据此做权限校验workspaceFK - core.Workspace可空动作关联的工作区sessiontext可空带索引客户端会话 IDClientSessionId头typetext带索引动作类型名如update_table、workspace_createdparamsJSONB撤销/重做所需的参数JSON 序列化存储scopetext带索引动作所在作用域如root、workspace1、application2created_onauto_now_add DateTimeField动作创建时间来自CreatedAndUpdatedOnMixinundone_atnullable DateTimeField带索引撤销时间戳null表示尚未撤销errortext可空撤销/重做失败时写入的异常堆栈action_groupUUID可空带索引动作组 ID用于把一组原子动作绑定在一起整体撤销/重做文档给出的示例行iduser_idsessioncategory(scope)created_ontypeparamsundone_aterror12some-uuid-from-clientrootdatetimeworkspace_created{created_workspace_id:10}nullnull模型上还提供了两个便捷方法is_undone()判断undone_at是否非空和has_error()判断error是否非空并被Meta.ordering (-created_on,)及三个组合索引-created_on/-id、-undone_at/-id、updated_on/id支撑查询性能。注意文档中category列在现版本源码中已演化为scope字段语义相同——描述动作发生在 Baserow 的哪个逻辑区域。后文统一使用scope。三、ActionHandlerundo / redo 的执行引擎ActionHandler定义在 backend/src/baserow/core/action/handler.py它提供undo与redo两个类方法对应两个 REST 端点文档中为/api/user/undo与/api/user/redo新版路由定义于 backend/src/baserow/api/urls.py 下的 user 视图。触发一次 undo/redo 需要三方面信息触发者用户user用于校验其是否仍有权撤销/重做该动作。例如用户正在重做一次工作区删除但如果他期间已被移出该工作区则应阻止这次重做。源码中所有查询都以useruser为第一过滤条件。客户端会话 IDclient session id每次用户执行动作时后端检查ClientSessionId请求头backend/src/baserow/api/sessions.py 中读取settings.CLIENT_SESSION_ID_HEADER若存在则把动作关联到该会话。undo/redo 时前端也携带该头后端只允许撤销/重做同一会话内的动作。这使得每个浏览器标签页拥有独立的撤销/重做历史——每个标签页生成唯一的ClientSessionId。作用域scope/category每次动作执行时都会关联一个作用域本质是Action表上的文本列取值形如root、table10、workspace20由ActionType在调用register_action时自行决定。undo/redo 时web 前端把用户当前正在浏览的区域对应的作用域集合发给后端。例如当前打开表格 20、侧边栏处于工作区 6 时发送的请求体为{ root: true, table: 20, workspace: 6 }后端据此把可撤销的动作限定在 root、table20、workspace6 这三个作用域内。作用域Scope的实现作用域并非自由文本而是由ActionScopeType体系规范化生成。在 backend/src/baserow/core/action/scopes.py 中可以看到RootActionScopeTypetype rootvalue()直接返回root请求序列化字段为 BooleanField设为 true 才纳入撤销范围WorkspaceActionScopeTypetype workspacevalue(workspace_id)返回workspace str(workspace_id)如workspace6ApplicationActionScopeTypetype applicationvalue(application_id)返回application str(application_id)如application2。每种ActionScopeType都实现get_request_serializer_field()DRF 请求字段与valid_serializer_value_to_scope_str(value)把合法请求值转成 scope 字符串供 undo/redo 端点反序列化请求并生成查询条件。ActionScopeStr则是一个 NewType 别名registries.py#L36用于类型系统上区分普通字符串与作用域字符串。作用域嵌套语义为什么看得到才能撤得掉作用域过滤遵循包含语义发送给 undo/redo 端点的作用域集合等价于撤销发生在这些作用域内的动作。文档给出了典型场景我把表格 20 重命名了那么update_table动作会被记录在 workspace6 作用域内因为表格 20 位于工作区 6。如果此时我正看着表格 20 并按撤销UI 会把 workspace6 也作为活跃作用域发送于是可以撤销这次重命名如果我先切到工作区 5 再按撤销UI 只发送 workspace5我便无法撤销对表格 20 的重命名直到回到 workspace6 活跃的界面区域。这就是每个界面区域各自维护撤销上下文的设计——作用域把全局历史切分成了彼此隔离的若干条栈。undo 的内部查询逻辑ActionHandler.undohandler.py#L92-L151的核心步骤将user.web_socket_id置空确保执行撤销的用户能收到该动作触发的实时事件查询该用户、该会话、undone_at IS NULL、作用域命中、按-created_on, -id排序的最新一条动作并加select_for_update行锁防止并发竞争若该动作属于某个action_group则把整个动作组上限settings.MAX_UNDOABLE_ACTIONS_PER_ACTION_GROUP条一起撤销保证一组原子动作要么整体撤销、要么整体回滚在transaction.atomic()内逐个调用_undo_action反序列化params→ 调用action_type.undo(user, params, action)→ 将undone_at置为datetime.now(tztimezone.utc)并保存任一步抛异常LockConflict除外则整体回滚并把相同的错误写入该组所有动作的error字段、同时把undone_at置为当前时间跳过失败动作最后刷新动作列表并广播action_done信号ActionCommandType.UNDO。redohandler.py#L174-L265的查询逻辑与之镜像查找该用户/会话/作用域下最新被撤销过undone_at IS NOT NULL的动作按-undone_at, -created_on, -id排序。此外 redo 还多了一道保护如果自撤销以来created_on undone_at又发生了新的未撤销动作则拒绝重做返回空列表保证历史栈不回退覆盖新操作。四、完整工作流示例重命名表格后撤销文档以用户 A 重命名表格为例完整走了一遍撤销流程结合源码可整理为如下时间线页面加载用户 A 打开位于应用 2、工作区 1 内的表格 10。前端生成一个ClientSessionId通常为 UUID存入authstoreundoRedostore 把当前页面作用域设为{root: true, table_id: 10, application_id: 2, workspace_id: 1}。执行动作用户 A 修改表格名称请求发往表格更新端点请求头携带ClientSessionId: example_client_session_idAPI 调用action_type_registry.get(UpdateTableActionType).do(user, ...)do完成改名register_action写入新Actionscopeapplication2UpdateTableActionType.scope调用ApplicationActionScopeType.value(database.id)对应文档所描述的workspace1层级归属sessionexample_client_session_id取自请求头user 用户 Aparams 包含新旧表名的 JSON供 undo/redo 使用。按下撤销用户 A 在界面上按 Undo请求发往 undo 端点请求体categoryscope取undoRedostore 中的当前作用域集合请求头携带同一ClientSessionIdActionHandler.undo执行在会话example_client_session_id、作用域[root, workspace1, application2, table10]内查找用户 A 的最新未撤销动作命中表格重命名动作会话匹配、作用域命中、用户匹配、undone_at为 null从表中反序列化params为UpdateTableActionType.Params调用action_type_registry.get(UpdateTableActionType).undo(user, params, action_to_undo)undo用original_table_name恢复旧名Action.undone_at置为datetime.now(tztimezone.utc)动作标记为已撤销。之后用户再按 Redoredo会用params.table_name再次改名并把undone_at置回 null。五、undo/redo 失败时会发生什么场景并发编辑导致撤销失败假设两位用户同时编辑同一张表按时间顺序用户 A 修改了名为 date 的字段中的单元格用户 A 修改了名为 Name 的字段中的单元格用户 B 删除了 name 字段用户 A 按撤销——当前实现下会得到错误提示撤销失败已跳过用户 A 再次按撤销——此时用户 A 的第一次修改date 单元格被成功撤销。原因用户 A 的最新动作作用于已被删除的 name 字段该字段已不存在无法撤销。失败处理流程对应 handler.py#L127-L151尝试调用ActionHandler.undo撤销该动作底层undo抛出异常ActionHandler.undo捕获异常并把异常堆栈写入动作的error字段Action.objects.filter(pk__in...).update(errortb, undone_atundone_at)把动作标记为已撤销undone_at置为当前 UTC 时间向用户返回撤销失败已跳过的特定错误。有趣的后续连续两次 redo 会怎样文档指出一个值得注意的语义失败后用户如果连按两次 redo——第一次 redo成功重做用户 A 的第一个动作第二次 redo尝试重做之前失败的那个动作。它带有error字段后端检测到后向用户返回cant redo due to error, skipping.无法重做因存在错误跳过——但同时会清除该动作的 error 并把undone_at置回 null即标记为已重做此时用户再按撤销该动作会被第二次尝试撤销。如果期间用户 B 已恢复了被删字段这次撤销就可能成功。这正是 handler.py#L228-L237 的注释所描述的语义我们正在重做一个撤销时失败的动作组实际上没什么可重做的。这种情况下我们把它标记为已重做这样用户可以再次尝试撤销它看看这次是否可行。因此error 标记不是终态它提供的是跳过 重试的容错循环而非永久卡死。并发与一致性保障undo/redo 全程使用select_for_update行锁 transaction.atomic一个动作组内任一条失败都会整体回滚LockConflict除外它会被原样上抛处理redo 遇到撤销后又发生新动作时直接返回空无动作可重做避免历史栈被破坏结果码与状态常量可对照前端 web-frontend/modules/core/utils/undoRedoConstants.jsNOTHING_TO_DO/SUCCESS/SKIPPED_DUE_TO_ERROR以及NO_MORE_UNDO/NO_MORE_REDO/ERROR_WITH_UNDO/ERROR_WITH_REDO等。六、相关机制与边界说明action_group动作组多个原子动作可以绑定同一个action_groupUUID由 backend/src/baserow/api/sessions.py 的set_client_undo_redo_action_group_id_from_request_or_raise_if_invalid从请求头读取undo/redo 时整组一起处理上限由settings.MAX_UNDOABLE_ACTIONS_PER_ACTION_GROUP控制。without_undo_redo_registration上下文backend/src/baserow/core/action/context.py 提供without_undo_redo_registration(user)在块内清空会话 ID 与动作组 ID使动作照常注册并发送action_done信号行历史、审计日志、webhook、实时更新都正常但不会进入用户的撤销栈。过期清理ActionHandler.clean_up_old_undoable_actions会删除超过settings.MINUTES_UNTIL_ACTION_CLEANED_UP分钟未更新的动作实现了UndoableActionCustomCleanupMixin的类型会先调用各自的clean_up_any_extra_action_data再做删除清理失败的单个动作不会回滚其他成功清理handler.py#L267-L356。文档与代码的命名演进本文档撰写时使用 category 一词而当前仓库源码中对应概念已命名为scopeAction.scope字段与ActionScopeType体系语义与用途完全一致同理文档中的 Application 概念与源码中的Database/Application对应表格的撤销作用域实际落在其所属 application/database 层级。七、小结Baserow 的撤销/重做体系可以概括为三句话写动作继承UndoableActionType实现do执行并register_action、undo、redo并定义Paramsdataclass 保存撤销所需参数存动作Action表以 JSONB 持久化params以sessionscopeuserundone_at四个维度索引和筛选撤动作ActionHandler.undo/redo依据用户 会话 作用域定位最新动作按动作组原子撤销/重做失败动作写入error并跳过形成可重试的容错循环。对于需要新增可撤销功能的开发者最直接的参考实现就是 backend/src/baserow/contrib/database/table/actions.py 中的CreateTableActionType、DeleteTableActionType、UpdateTableActionType等它们共同构成了 Baserow 全平台撤销/重做能力的样板。【免费下载链接】baserowBuild databases, automations, apps agents with AI — no code. Open source platform available on cloud and self-hosted. GDPR, HIPAA, SOC 2 compliant. Best Airtable alternative.项目地址: https://gitcode.com/GitHub_Trending/ba/baserow创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。