news 2026/9/20 10:50:49

Baserow Core Graph 图结构深度解析:Application Builder 与 Automation Builder 共用的节点关系引擎

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Baserow Core Graph 图结构深度解析:Application Builder 与 Automation Builder 共用的节点关系引擎

Baserow Core Graph 图结构深度解析:Application Builder 与 Automation Builder 共用的节点关系引擎

【免费下载链接】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

导读

core/graph是 Baserow 后端中一个模块无关的可复用图系统,负责存储、遍历和管理"点(point)"与"边(edge)"之间的关系。在 Application Builder 中,它管理PageElement的父子/兄弟关系;在 Automation Builder 中,它管理AutomationWorkflowAutomationNode的分支流转关系。阅读本文后,你将掌握该图结构的序列化格式、位置三元组语义、BaseGraphHandler的核心操作方法(插入/移除/移动/替换)、两个 Builder 的落地差异,以及并发安全与图修复的设计细节。


一、这是什么:一个可复用的图包

core/graph位于 backend/src/baserow/core/graph/,是一个被两个 Builder 共同依赖的图系统:

  • Application Buildercontrib/builder):用来管理和遍历Element(元素)之间的关系,例如"Heading 元素放在第 2 列容器里"。
  • Automation Buildercontrib/automation):用来管理和遍历AutomationNode(自动化节点)之间的关系,例如"条件满足时走分支 A,否则走默认分支"。

从源码结构看,该目录包含 4 个核心文件:

文件职责
models.py提供两个 DjangoModel混入:GraphModelMixin(容器模型)与GraphPointMixin(点模型),以及GraphPointTrashableItemType(图的回收站支持基类)
handler.py抽象基类BaseGraphHandler,实现图结构上几乎所有操作
types.py定义SerializedGraphGraphPointPosition(north/south/child)、位置三元组类型等
exceptions.py图相关的异常:GraphPointDoesNotExistGraphPointNotFoundInGraphGraphPointReferencePointInvalid

二、核心抽象:point、edge 与 JSON 图

文档给出了 5 条核心抽象,它们是理解整个系统的钥匙:

  1. 图由"点(points)"组成。这个术语刻意保持抽象——点具体代表什么因模块而异(见下文"两个 Builder 如何消费它")。
  2. 边(edge)连接点。边的语义同样按模块变化。
  3. 点可以被遍历,以确定"下一个(next)"、"上一个(previous)"和"父级(parent)"点。
  4. 图本身是一个 JSON 对象
  5. 空字符串""在整个系统中始终表示默认/回退边——出现在边字典、children映射以及旧格式迁移中。

2.1 序列化图格式

types.py中定义了序列化图的类型:

class SerializedGraphPoint(TypedDict, total=False): next: dict[str, list[int]] children: list[int] SerializedGraph = dict[str, int | SerializedGraphPoint]

BaseGraphHandler的 docstring 给出了一个完整示例图(handler.py):

{ "0": 1, "1": {"next": {"": [2]}}, "2": { "next": { "uuid1": [3], "uuid2": [5], "": [4], } }, "3": {}, "5": {}, "4": {"next": {"": [6]}}, "6": {"children": {"": [7], "0": [8], "1": [9]}}, "7": {}, "8": {}, "9": {"next": {"": [10]}}, "10": {} }

解读规则:

  • '0'GRAPH_ROOT_KEY)不是点 ID,而是图的起点(根)指针,其值指向第一个点的 ID(这里是1)。
  • 除根键外的每个键都是一个点的 ID。
  • 每个点的next以边 UUID 为键、以该边上点 ID 列表为值的字典。目前每条输出边最多只允许一个点(for now only one point is possible per output)。
  • children是以边/位置标识为键、以子点 ID 列表为值的字典,允许容器元素在不同"位置"(例如不同列或插槽)持有子元素。""键代表默认边。
  • 点 ID 是整数,但图 JSON 中一律以字符串键存储。

2.2 位置三元组(position triplet)

图中每个点的位置由三元组[reference_point, position, output]唯一标识,position 取值来自GraphPointPosition(types.py):

class GraphPointPosition(models.TextChoices): NORTH = "north", "North" SOUTH = "south", "South" CHILD = "child", "Child"

对应GraphPointPositionTriplet类型:tuple[GraphPoint | None, GraphPointPositionType, str]。语义示例:

  • [<Point(42)>, 'south', '']:位于点 42 的南侧(即它的下一个点),走默认输出边""
  • [<Point(42)>, 'south', 'uuid45']:位于点 42 的南侧,走边uuid45
  • [<Point(42)>, 'child', '']:作为点 42 的子点,挂在默认边;
  • [<Point(42)>, 'child', '0']:作为点 42 的子点,挂在位置/边"0"

2.3 容器模型与点模型两个混入

GraphModelMixin(models.py)为容器模型添加graph = models.JSONField(default=dict)字段,并提供:

  • get_graph_handler():抽象方法,子类必须实现,返回一个BaseGraphHandler子类;
  • get_graph():返回当前请求内按(model_label, id)缓存的共享 handler(基于baserow.core.cache.local_cache)。如果缓存被同一行数据的不同 Python 实例(例如新 fetch 出来的reference_element.page)抢先填充,则会把 handler 重新绑定到self,保证通过self.graph也能看到内存中的图变更;
  • print_graph()/assert_reference():调试与测试辅助方法。

GraphPointMixin(models.py)为点模型提供遍历与关系查询助手:

方法/属性作用
_get_graph()通过get_parent().get_graph()拿到所属容器图的 handler
graph_point_label点有label就用 label,否则回退到get_type().type,用于labeled_graph
is_root_point是否为图根点(根点永远位于键"0",图中只有一个根点)
is_nested_point是否嵌套(即作为某点的 child 存在)
get_previous_points()返回所有前驱点(含前一个兄弟点与父点)
get_child_points()/get_sibling_points()直接子点 / 兄弟点(同父、同边)
get_previous_positions()返回从根到该点的路径位置三元组列表(利用缓存的 previous-position 映射,O(depth) 完成)
get_parent_point()/get_parent_points()直接父容器点 / 全部祖先容器点(由外到内)
get_next_points(output_uid=None)返回直接后继点列表(可能多个,例如工作流多分支时)
get_previous_edge_name()/get_place_name()到达该点所用的最近 next 边名 / 最近父级 place 名

此外models.py还定义了GraphPointTrashableItemType(models.py),为图中的点模型(builder 元素、automation 节点)集中实现了图感知的回收站/恢复逻辑:trash 时捕获点(及其全部后代)的位置并级联软删除,restore 时按存储位置重新插入。它提供了should_mutate_graphbefore_cascade_deletebefore_permanent_deletevalidate_reference等钩子,供各类型覆盖差异点,而无需重写整套算法。

三、设计原则:模块无关性

文档明确了两条设计原则,这也解释了为什么这个包放在core/graph而不是任何一个 Builder 里:

  1. core/graph必须保持模块无关——ElementAutomationNode或其他模块专属概念不允许出现在core/graph代码中。
  2. 把模块专属逻辑向外推——如果 Application 或 Automation Builder 的某功能有模块专属需求,可复用部分放进core/graph,模块专属代码留在消费模块里。

这一点可以从源码得到印证:handler.py中全程使用泛化的GraphPoint/GraphModelInstance类型变量(定义于 types.py),并用base_point_classoutputs_id_mappinginstance_id_mappingdoes_not_exist_exception子类可覆盖的类属性来桥接具体模块差异,自身从不 import 任何 Builder 模型。

四、关键文件速览

文档给出的关键文件索引如下(均已在本仓库确认存在):

  • backend/src/baserow/core/graph/:图系统源码目录;
  • backend/tests/baserow/core/graph/:全部后端图系统测试目录;
  • handler.py:包含BaseGraphHandler
  • models.py:包含两个 DjangoModel混入。

测试目录下除文档提到的test_graph_handler.pytest_graph_models.py外,还有test_graph_write_guards.py(写入守卫测试)、fixtures.pyconftest.py(测试夹具与配置)。

五、Application Builder 如何使用它

在 Application Builder(contrib/builder)中:

  • 容器模型是Page,点模型是Element。对应源码:pages/models.py 与 elements/models.py 中分别混入GraphModelMixinGraphPointMixin
  • 大多数点没有边,edge 只是空字符串""。但如果元素的父级实现了ContainerElementTypeMixin(容器元素类型混入),则它的 edge 就是该元素的place_in_container字段。

文档给出了具体例子:

element1是一个ColumnElement(其类型ColumnElementType实现了ContainerElementTypeMixin),该列column_amount=3element2是一个HeadingElement,我们希望它位于第 2 列:把它的 parent 设为element1,并把place_in_container设为"1"

在实现层面,place_in_container是元素序列化器中显式暴露的字符串字段(见 serializers.py,对应MoveElement等请求),在应用导出/导入时按槽位(slot)对元素分组排序(application_types.py),并且 builder 初始化模板中会为容器元素预置place_in_container="0"/"1"/"2"等占位(builder_beta_init_application.py)。

文档同时指出:目前元素之间最多只有一条边,且它总是字符串(要么为空串,要么是数字形式的place_in_container,如"0""1")。

六、Automation Builder 如何使用它

在 Automation Builder(contrib/automation)中:

  • 容器模型是AutomationWorkflow,点模型是AutomationNode。对应 workflows/models.py 与 nodes/models.py。
  • 一个点可以有一条或多条边。获取方式:先取得节点服务类型,再调用get_edges()。文档中的示例流程:

node1是一个AutomationNodeservice1 = node1.service.specific.get_type()得到node1的服务类型。service1.get_edges()返回一个定义节点间边的字典。

边字典的两种形态:

  • 大多数节点服务get_edges()返回{"": {"label": ""}}。外层字典键""就是"默认边"约定(见核心抽象),表示无命名边、直线遍历;内层字典里的label是用户可在 UI 中看到/配置的标签。
  • **CoreRouterServiceType(核心路由/条件分支服务)**则为用户配置的每条边返回一个 UUID 作为外层键,并额外带一个默认边作为回退。文档示例:
{ "condition1Uuid": {"label": "Condition1"}, "condition2Uuid": {"label": "Condition2"}, "": {"label": "Default fallback"}, }

源码中CoreRouterServiceType被引入并用于节点类型判定(见 node_types.py 与第 262 行),同时 Automation 的 handler 大量通过workflow.get_graph()调用图的get_childrenget_pointget_previous_positionsget_point_at_positionget_next_pointsinsertreplacemoveget_positionmigrate_graph等方法(见 nodes/handler.py、nodes/service.py、workflows/handler.py),可以推断图的这些 API 是自动化编排(插入节点、复制节点、替换节点类型、移动节点、跨图迁移)的底层支柱。

七、常用操作:以 handler 为入口

7.1 标准入口

  • 给定容器模型实例,调用.get_graph()拿到图 handler——这是插入、移除、移动点的标准入口。例如 Application Builder 中page.get_graph(),Automation Builder 中workflow.get_graph()
  • 给定点模型实例,直接使用GraphPointMixin的遍历辅助方法(next/previous/parent、边标签访问等)。优先使用这些方法,而不是自己手写遍历逻辑。

7.2BaseGraphHandler核心方法

BaseGraphHandler(handler.py)是所有图操作的抽象基类,子类只需实现get_point_map()(返回{点ID: 模型实例}映射)。主要方法:

方法功能
get_point(point_id)从点映射中取模型实例,不存在则抛does_not_exist_exception
get_info(point)取某点的{"next": ..., "children": ...}信息字典;传None表示取根点
get_point_at_position(ref, position, output)取参考点指定方向/输出边上的点
get_position(point)返回点的位置三元组;根点返回(None, "north", "")
get_previous_positions(point)生成到达目标点的全部位置三元组列表(利用缓存的前驱映射)
get_next_points(point, output=None)/get_children(point, output=None, first_only=False)后继点 / 子点集合
get_siblings(point)同父同边的兄弟点
get_descendants(point)/collect_all_descendants(point)深度优先收集全部(直接+传递)后代
append(point)把点追加到默认边链的末尾
insert(point, reference_point, position, output="")按位置三元组插入点
remove(point, keep_info=False)移除点(默认级联移除其后代并返回GraphPointRemoved(point_removed, dependencies_removed)
replace(old, new)在同位置用新点替换旧点(如自动化节点更换服务类型)
move(point, ref, position, output, target_graph=None)移动点;提供target_graph时实现跨图移动,子树随点一起迁移
migrate_graph(id_mapping)导入/导出时按 ID 映射重写图中的点 ID 与边 UUID
labeled_graph()生成不依赖点 ID、可在测试间稳定比较的标签化图

7.3 插入语义细节(south / child / north)

insert的实现(handler.py)值得细读:

  • reference_point is None:把点设为图根,原根(若存在)变成新点的默认边后继。
  • position == "north":插到参考点之前,新点占据参考点位置,参考点变成新点在默认输出边上的 next。
  • position == "south":插到参考点之后,参考点在该输出边上指向新点,新点继承参考点原本的后续。
  • position == "child":新点成为参考点在该边上 children 链的头部,原头部(若有)变成新点的默认边后继。这一"头插"设计是为了让insert成为get_position的忠实逆操作,保证 move 的 undo/redo 可以精确往返,也与前端_insertAt('child')的乐观图保持一致。

insert还内置了三道防损坏校验:禁止相对于自身插入、禁止把已入图(已有入边引用)的点二次插入、禁止把点插入到它自己子树内部的参考点上(会形成环)。

7.4 位置三元组的根点特例

get_position对根点返回(None, "north", "")而不是(None, "south", ""),这是刻意设计:该三元组需要能通过move/insert往返(例如撤销对首元素的移动)。insert(reference=None, ...)总是把点放到根位置,而move(None, "south")这一特定组合解释为"追加到链尾"(供孤儿节点撤销路径使用)。因此返回(None, "north", "")能经 insert 恢复根位置,同时把(None, "south", "")留给"追加"语义。

八、常见坑位与兼容性

文档明确列出两个高频陷阱:

  1. children里只存"第一个"子点children并不包含容器的全部子点,只包含每个边的"入口"子点。要拿到全部子点,必须先取第一个子点,然后沿着next一直遍历到没有后继为止。源码中get_children(first_only=False)正是通过_get_chain_elements/_walk_chain_ids沿默认next[""]链展开整条链(handler.py),而first_only=True时只返回各边的入口点,把链式遍历交给调用方。

  2. 旧版children数组格式:可能遇到形如{"children": [7]}的遗留数据。BaseGraphHandler会兼容它并迁移为新格式{"children": {"": [7]}}——""键遵循默认边约定。源码中_get_children_dict_set_children(handler.py)同时支持两种格式:读时把数组归一化为默认边字典,写时把遗留数组升级为字典格式;migrate_graph在做导入迁移时也会把遗留 children 数组改写为{"": [...]}形态。

8.1 并发安全:行锁与确定性加锁顺序

图是以单个 JSON 文档整体读改写的,若不加锁,两个并发事务会 last-writer-wins,导致幽灵点(ghost point)或自引用等损坏。BaseGraphHandler通过以下机制防护:

  • _lock_instance_for_update()(handler.py):任何变更前对容器行执行select_for_update()就地刷新内存图;非模型实例(测试替身)或不在原子块内时跳过锁。
  • lock_for_update():公开的预取锁入口,供 service 在变更前先基于最新已提交状态做输入校验(幂等)。
  • lock_all_for_update(handlers)按主键升序确定性加锁,避免两个反向交叉图移动互相死锁。

8.2 图修复(healing)工具集

handler 还内置了一组"纯序列化图扫描 + 最小修复"的方法,可从源码结构推断其用于heal_corrupted_graph之类的自愈流程:

  • find_self_referencing_point_ids/strip_self_references:自引用检测与剥离;
  • find_dangling_reference_ids/strip_dangling_references:悬挂引用(引用不存在的点条目)检测与剥离;
  • find_cycle_reference_pairs/strip_cycle_references:用迭代式 DFS 找环回边并断开(避免 Python 递归深度限制);
  • find_converging_reference_pairs/strip_converging_references:收敛引用(一个点有多条入边)修复,保留根可达路径上的规范引用;
  • find_unreachable_point_ids/reattach_unreachable_points:把从根不可达的"游离"点重新挂到默认链尾部,使其重新可见、可删除;
  • prune_points(ids_to_remove):清理"图还在引用、但底层 DB 行已不存在"的陈旧点,用其后继原位拼接保持链连通(纯序列化图操作,可在零停机部署的旧代码硬删除后安全执行)。

九、测试策略

文档要求图系统的任何改动都必须有对应测试:

  • handler 的任何修改→ 在 test_graph_handler.py 中测试;
  • 模型混入的任何修改→ 在 test_graph_models.py 中测试;
  • 测试夹具与配置→ 位于测试目录的 fixtures.py 与 conftest.py;
  • 此外还有 test_graph_write_guards.py 专门覆盖写入守卫(自引用、子树内引用、重复插入等防损坏校验)。

配合GraphModelMixin.assert_reference()BaseGraphHandler.labeled_graph(),测试可以用不依赖点 ID 的标签化图与参考图做稳定断言——labeled_graph会把点 ID 换成graph_point_label(有 label 用 label,否则用类型名),并自动消歧重复标签(追加-后缀),保证跨测试执行的确定性。


总结

Baserow 的core/graph用一份 JSON 序列化图 + 一个抽象 handler + 两个 Django 混入,同时支撑了页面元素布局(Application Builder)与自动化流程编排(Automation Builder)两类截然不同的关系管理需求。理解其"点/边/位置三元组"模型、""默认边约定、children 首子点遍历规则,以及行锁与图修复机制,是安全修改或扩展任一 Builder 节点/元素关系逻辑的前提;相关实现细节可继续深入 handler.py、models.py 与 types.py 三个核心文件。

【免费下载链接】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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/20 22:05:58

继电器切换AD620增益电路设计与高频失真抑制

简介&#xff1a;本资源是一份面向电子工程专业学生、嵌入式开发者及硬件设计初学者的电压可控正弦波放大电路设计详解文档&#xff0c;聚焦模拟电路核心能力训练&#xff0c;解决信号动态增益调节、低失真放大与负电源构建等实际工程问题。文档以AD620精密仪表放大器为核心&am…

作者头像 李华
网站建设 2026/9/19 22:58:59

景星建材高德评分升至4.4分,见证南宁建材供应链服务口碑

近日&#xff0c;高德地图页面显示&#xff0c;景星建材综合评分更新至4.4分。其中&#xff0c;用户行为分4.4分&#xff0c;用户评价分4.4分&#xff0c;并获得高德大数据认证。对于一家建材供应服务商来说&#xff0c;地图评分不只是一个数字。它背后反映的是客户是否真实搜索…

作者头像 李华
网站建设 2026/9/20 4:11:15

网络存储系统开发:前端交互与数据库表结构设计实践

简介&#xff1a;面向计算机及相关专业毕业设计的网络存储系统设计与实现论文&#xff0c;重点围绕用户界面与数据库设计展开&#xff0c;系统介绍了分布式存储技术背景、HTML网页操作技术、系统核心功能及数据存储方案。论文以作者实际参与的网络存储系统项目为基础&#xff0…

作者头像 李华