ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

用 ent 实现 O2O 递归关系:构建双向链表数据模型的完整实践

用 ent 实现 O2O 递归关系:构建双向链表数据模型的完整实践 后端ORM代码生成【免费下载链接】entAn entity framework for Go项目地址https://gitcode.com/gh_mirrors/en/ent点击查看免费下载导读本文基于 entAn entity framework for Go官方示例仓库中的o2orecurone-to-one recursive一对一递归关系示例深入讲解如何在 ent 中建模链表节点这一经典自引用结构每个节点最多只能有一个next后继同时通过prev指回前驱形成双向一对一O2O的递归边。读完本文你将掌握edge.To(...).Unique().From(...).Field(...)这一双向唯一边声明语法、go generate代码生成流程、以及使用生成的 Builder API 创建、遍历、查询与改造链表甚至将其变成循环链表的完整实战方案并能在源码层面理解该关系在 SQL 层的落地方式自引用外键 唯一约束。示例概览O2O 递归关系解决什么问题在关系型数据库中经常需要表达实体与自身同类型实体之间的一对一关联例如链表的每个节点指向下一个节点组织结构中每个部门有一个直属上级单向队列或审批流中的先后承接关系。这类同一张表、自引用、单值关联的结构就是 ent 中的 O2O 递归关系one-to-one recursive relation。官方示例 examples/o2orecur/README.md 给出了一个最直观的载体——链表链表中的每个节点Node最多只能有一个next后继如果节点 A 通过next指向节点 B那么 B 可以借助prev反向取回 A。这意味着next与prev并不是两张独立的外键而是同一条边从两个方向观察得到的两个名字其底层共享同一个外键列。下文将依次展示 Schema 声明、代码生成、运行测试与源码级验证四个环节。Schema 定义声明next/prev双向唯一边示例的 Schema 位于 examples/o2orecur/ent/schema/node.go完整代码如下package schema import ( entgo.io/ent entgo.io/ent/schema/edge entgo.io/ent/schema/field ) // Node holds the schema definition for the Node entity. type Node struct { ent.Schema } // Fields of the Node. func (Node) Fields() []ent.Field { return []ent.Field{ field.Int(value), field.Int(prev_id). Optional(), } } // Edges of the Node. func (Node) Edges() []ent.Edge { return []ent.Edge{ edge.To(next, Node.Type). Unique(). From(prev). Field(prev_id). Unique(), } }Fields为反向边预留的外键列field.Int(value)节点的业务字段这里用整数表示节点在链表中的位置示例中依次填入 1~5用于打印与断言。field.Int(prev_id).Optional()这是整个示例最关键的一列。O2O 递归关系中数据库侧只需要一个外键列即可承载双向关系。prev_id记录了我的前驱是哪个节点因此它必须是**可选Optional**的——链表头没有前驱prev_id为 NULL。从生成迁移代码 examples/o2orecur/ent/migrate/schema.go 可以看到该列最终被定义为{Name: prev_id, Type: field.TypeInt, Unique: true, Nullable: true}可空、且全局唯一。Edges一条边两个方向edge.To(next, Node.Type)声明了一个指向自身类型Node.Type的出边nextUnique()保证每个节点最多只有一个后继这正是链表单 next约束的模型层保证紧接着的.From(prev)把同一条边从对面方向命名为prev.Field(prev_id)显式指定这条边落库到prev_id字段末尾再补一个Unique()让反向方向同样唯一——即每个节点最多只能被一个节点指为后继。这四行组合表达的含义是next与prev是同一条自引用外键的两个端点。只要 A 的prev_id B那么从 A 看它的prev是 B从 B 看它的next是 A。整个链表不需要额外的中间表也不需要两份外键。生成代码资产从 Schema 到完整的 ORM 层Schema 定义好之后通过go generate生成实体、Builder、谓词等全部代码资产go generate ./...这一指令由 examples/o2orecur/ent/generate.go 中的//go:generate注释驱动//go:generate go run -modmod entgo.io/ent/cmd/ent generate --header ... ./schema生成结果位于 examples/o2orecur/ent 目录下主要包括实体与边元数据在 examples/o2orecur/ent/node/node.go 中生成器把prev/next两条边的方向、表与列信息固定为常量。可以看到PrevColumn与NextColumn都指向同一个物理列prev_id且newPrevStep被标记为sqlgraph.O2O, true反向/inversenewNextStep为sqlgraph.O2O, false正向/owning——这正是一条边、两个方向在生成代码中的直接体现。实体模型examples/o2orecur/ent/node.go 定义了Node结构体含ID、Value、PrevID字段与NodeEdges边容器并生成了QueryPrev()、QueryNext()、Update()等便捷方法。创建/更新/查询 Builderexamples/o2orecur/ent/node_create.go、examples/o2orecur/ent/node_query.go 提供SetPrev、SetNext、QueryPrev、QueryNext、WithPrev、WithNext等链式方法。谓词包examples/o2orecur/ent/node/where.go 提供HasPrev()、HasNext()、Not(...)、And(...)、Or(...)等查询谓词。运行示例一条命令验证整条链表示例的测试入口是 examples/o2orecur/example_test.go使用 Go 内置的 example 测试机制运行go test即可自动完成建库 → 建表 → 造链表 → 遍历 → 改造为循环链表 → 断言的全过程。测试中通过ent.Open(sqlite3, file:ent?modememorycacheshared_fk1)打开一个内存 SQLite 数据库并显式开启了外键约束_fk1随后用client.Schema.Create(ctx)执行自动迁移。其期望输出被固化在注释中// Output: // 1 2 3 4 5 // truego test会同时校验打印结果1 2 3 4 5与最终断言true任何一步出错都会导致测试失败因此它本身就是该数据模型正确性的可执行证明。实战拆解用 Builder API 构建与遍历链表示例核心逻辑位于 examples/o2orecur/example_test.go 的Do函数中分为三个阶段。阶段一创建链表头并逐个追加节点head, err : client.Node. Create(). SetValue(1). Save(ctx) // ... curr : head // Generate the following linked-list: 1-2-3-4-5. for i : 0; i 4; i { curr, err client.Node. Create(). SetValue(curr.Value 1). SetPrev(curr). Save(ctx) // ... }链表头只有value没有前驱因此不需要设置prev_id可空字段。之后每次新建节点时通过SetPrev(curr)把当前链尾设为新节点的前驱——注意这里传的是实体对象生成代码examples/o2orecur/ent/node_create.go会自动将其转换为SetPrevID(v.ID)写入prev_id列。循环结束后得到1-2-3-4-5的双向链表。阶段二沿着next正向遍历// Loop over the list and print it. FirstX panics if an error occur. for curr head; curr ! nil; curr curr.QueryNext().FirstX(ctx) { fmt.Printf( %d, curr.Value) }QueryNext()是生成在实体上的便捷方法等价于client.Node.QueryNext(head)实现见 examples/o2orecur/ent/node.go。遍历以head为起点不断取出后继链尾的next为空FirstX在IsNotFound时返回nil见 examples/o2orecur/ent/node_query.go循环自然终止打印出1 2 3 4 5。阶段三借助反向边与谓词把链表变成循环链表// The tail of the list, has no next. tail, err : client.Node. Query(). Where(node.Not(node.HasNext())). Only(ctx) // ... tail, err tail.Update().SetNext(head).Save(ctx) // Check that the change actually applied: prev, err : head.QueryPrev().Only(ctx) // ... fmt.Printf(\n%v, prev.Value tail.Value)这里用到了两个非常有代表性的能力边存在性谓词node.HasNext()会翻译为prev_id列被其他节点引用的 EXISTS 语义生成实现见 examples/o2orecur/ent/node/where.go底层通过sqlgraph.HasNeighbors构造外层再套node.Not(...)取反即没有任何节点以我为后继从而精确定位链尾唯一一个next为空的节点。更新边引用tail.Update().SetNext(head)把链尾的next指向链头链表变为环形。随后通过反向边head.QueryPrev().Only(ctx)从头部出发反查前驱验证其确实指向tail输出true。值得一提的细节是谓词查询中Only(ctx)会在命中多于一条记录时返回NotSingularError见 examples/o2orecur/ent/node_query.go因此这里用Only而非First相当于额外校验了链尾唯一这一不变量。源码级验证这条关系在 SQL 层长什么样从生成迁移代码 examples/o2orecur/ent/migrate/schema.go 可以精确还原最终建表语义NodesColumns []*schema.Column{ {Name: id, Type: field.TypeInt, Increment: true}, {Name: value, Type: field.TypeInt}, {Name: prev_id, Type: field.TypeInt, Unique: true, Nullable: true}, } // ... ForeignKeys: []*schema.ForeignKey{ { Symbol: nodes_nodes_next, Columns: []*schema.Column{NodesColumns[2]}, // prev_id RefColumns: []*schema.Column{NodesColumns[0]}, // id OnDelete: schema.SetNull, }, },可以归纳出 O2O 递归关系的三个物理特征单外键自引用prev_id引用本表id整张表只有这一列做关联next不占任何额外存储唯一约束prev_id带有Unique: true保证每个节点最多被一个后继指回从数据库层面兜底了 O2O 的双向唯一性可空 级联置空Nullable: true让链表头可以没有前驱OnDelete: schema.SetNull意味着被引用的节点删除后指向它的prev_id会被置为 NULL而不是级联删除或报外键错误——这也解释了为什么示例在开启_fk1的 SQLite 上依然可以灵活增删节点。上述 SQL 结构正是 Schema 中edge.To(next, Node.Type).Unique().From(prev).Field(prev_id).Unique()这一连串声明的落库结果.Unique()×2 对应唯一约束.Field(prev_id)指定外键列Optional()对应可空列。小结与延伸通过o2orecur这个精简示例可以提炼出 ent 建模 O2O 递归关系的完整套路在Fields()中声明一个可空的外键列如prev_id在Edges()中用edge.To(next, Node.Type).Unique().From(prev).Field(prev_id).Unique()将正向出边 反向入边 外键列一次性绑定运行go generate ./...生成实体、Builder 与谓词利用生成的SetPrev/SetNext、QueryPrev/QueryNext、HasNext/HasPrev等 API 完成创建、遍历与查询。这一模式在仓库中并非孤例ent 官方还在 examples/o2o2types、examples/o2obidi、examples/o2m2types 等示例中演示了 O2O/O2M 的不同变体是否共享外键列、是否双向、基数差异。如果你需要在 Go 项目中建模上级部门前序任务上一层评论这类单值自引用关系o2orecur就是可直接参照的最小可运行蓝本Schema 不过十余行go test一条命令即可验证整个数据模型的正确性。赞分享后端ORM代码生成【免费下载链接】entAn entity framework for Go项目地址https://gitcode.com/gh_mirrors/en/ent点击查看免费下载相关推荐让每首歌曲都拥有灵魂一款开源智能歌词获取工具的诞生与创新让每首歌曲都拥有灵魂一款开源智能歌词获取工具的诞生与创新 你是否曾为寻找一首歌曲的完整歌词而花费数小时当喜爱的音乐响起却因为缺少歌词而无法完全沉浸其中这后端ORM代码生成Laravel 边界列表模型实现递归关系的指南Laravel 边界列表模型实现递归关系的指南 项目介绍 Laravel 边界列表模型 是一个由 Jonas Staudenmeir 开发的扩展包专为 Laent实战入门快速构建数据模型与关系图ent实战入门快速构建数据模型与关系图 本文详细介绍了如何使用ent框架进行数据建模和关系构建。从环境搭建、工具链安装配置开始到定义实体Schema与各种字后端ORM代码生成上一篇CommunityForensics-DeepfakeDet-ViT vs 传统检测方法为何Vision Transformer能实现0.992 AUC-ROC下一篇SublimeLinter 配置详解从基础到高级技巧创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表