Nested Set 树形结构
使用官方 nestedset 扩展存储和查询层级数据。
extensions/nestedset 提供类型化树服务,用 ParentID、_lft、_rgt 和 Depth 保存节点,并提供创建、移动、读取、删除、检查和重建方法。
安装
import "github.com/duxweb/oro/extensions/nestedset"
打开数据库时注册扩展:
db, err := oro.Open(oro.Config{
Connections: map[string]oro.ConnectionConfig{
"default": {Driver: sqlite.Open("app.db")},
},
Extensions: []oro.Extension{
nestedset.Extension(),
},
})
模型
直接嵌入 nestedset.NodeFields:
type Category struct {
oro.Model
nestedset.NodeFields
Name string
}
func (Category) Define(s *oro.SchemaBuilder) {
s.Table("categories")
s.Field("Name").String().Size(120)
}
默认列名:
| 字段 | 列名 |
|---|---|
ParentID |
parent_id |
Lft |
_lft |
Rgt |
_rgt |
Depth |
depth |
如果需要自定义字段名,在 Define 和 Use 两处使用同一份 nestedset.FieldNames(parent, left, right, depth)。
创建节点
tree := nestedset.Use[Category](db)
root, err := tree.Create(ctx, &Category{Name: "Catalog"})
phones, err := tree.Create(ctx, &Category{
Name: "Phones",
NodeFields: nestedset.NodeFields{
ParentID: oro.NullOf(root.ID),
},
})
Create 会读取模型里的 ParentID。ParentID 为空时创建根节点;ParentID 有值时创建为该父节点的最后一个子节点。
也可以使用明确位置的方法:
root, err := tree.CreateRoot(ctx, &Category{Name: "Catalog"})
phones, err := tree.CreateChild(ctx, root.ID, &Category{Name: "Phones"})
_, err = tree.CreateFirstChild(ctx, phones.ID, &Category{Name: "Android"})
_, err = tree.CreateAfter(ctx, phones.ID, &Category{Name: "Laptops"})
CreateRoot新建根节点。CreateChild追加为最后一个子节点。CreateFirstChild插入为第一个子节点。CreateBefore和CreateAfter插入为兄弟节点。
结构性写入会自动运行在事务中。
普通 ORM 写入,例如 db.Use[Category]().Create(...),不会维护 nested set 的左右值。树形相关写入统一使用 tree 服务。
更新节点
phones.Name = "Smart Phones"
phones.ParentID = oro.NullOf(newParentID)
updated, err := tree.Update(ctx, phones)
Update 只写入模型上已设置(非零值)的字段,不再用零值覆盖未设置的其他列。它会把模型里的 ParentID 和数据库当前行对比,如果 ParentID 变化,会移动整棵子树,并重新计算 _lft、_rgt 和 Depth。
如果只想按 id 更新部分列,而不必重新加载整个结构体,使用 UpdateValues:
updated, err := tree.UpdateValues(ctx, node.ID, oro.Map{"Name": "New name"})
func (tree *Tree[T]) UpdateValues(ctx context.Context, nodeID any, values oro.Map, options ...oro.WriteOption) (*T, error)
由于 Update 会跳过零值,如果需要把某个字段显式设置为零值,请使用带 oro.Map 的 UpdateValues。
如果需要指定移动到兄弟节点前后或第一个子节点,使用下面的显式移动方法。
读取树
roots, err := tree.Roots(ctx)
all, err := tree.All(ctx)
nested, err := tree.Tree(ctx)
subtree, err := tree.Subtree(ctx, phones.ID)
flatSubtree, err := tree.FlatSubtree(ctx, phones.ID)
Tree 和 Subtree 返回嵌套节点:
type Node[T any] struct {
Model *T
Children []*Node[T]
}
读取关系
parent, err := tree.Parent(ctx, nodeID)
children, err := tree.Children(ctx, nodeID)
first, err := tree.FirstChild(ctx, nodeID)
last, err := tree.LastChild(ctx, nodeID)
siblings, err := tree.Siblings(ctx, nodeID)
prev, err := tree.PrevSibling(ctx, nodeID)
next, err := tree.NextSibling(ctx, nodeID)
ancestors, err := tree.Ancestors(ctx, nodeID)
ancestorsAndSelf, err := tree.AncestorsAndSelf(ctx, nodeID)
descendants, err := tree.Descendants(ctx, nodeID)
descendantsAndSelf, err := tree.DescendantsAndSelf(ctx, nodeID)
单节点读取未命中返回 nil, nil。多节点读取未命中返回空切片。
相对层级
数据库里的 Depth 是整棵树内的绝对层级。分销树、组织树等业务界面通常需要相对层级:当前节点是 0,直接子级是 1,孙级是 2。
rows, err := tree.DescendantsWithDepth(ctx, nodeID)
rows, err := tree.DescendantsAndSelfWithDepth(ctx, nodeID)
返回结构:
type RelativeNode[T any] struct {
Model *T
Depth int // 相对层级
}
按相对层级限制后代:
items, err := tree.DescendantsWithinDepth(ctx, nodeID, 2) // 子级和孙级
items, err := tree.DescendantsAtDepth(ctx, nodeID, 1) // 只查直接子级
DescendantsAndSelfWithinDepth 和 DescendantsAndSelfAtDepth 会包含当前节点。这些方法仍然使用 SQL 的左右值和 depth 条件过滤,不会先加载整张表。
判断方法
ok, err := tree.IsRoot(ctx, nodeID)
ok, err := tree.IsLeaf(ctx, nodeID)
ok, err := tree.IsAncestorOf(ctx, ancestorID, nodeID)
ok, err := tree.IsDescendantOf(ctx, nodeID, ancestorID)
移动节点
err := tree.MoveToRoot(ctx, nodeID)
err := tree.MoveToChildOf(ctx, nodeID, parentID)
err := tree.MoveToFirstChildOf(ctx, nodeID, parentID)
err := tree.MoveBefore(ctx, nodeID, targetID)
err := tree.MoveAfter(ctx, nodeID, targetID)
err := tree.MoveUp(ctx, nodeID)
err := tree.MoveDown(ctx, nodeID)
节点不能移动到自身或自己的后代中。移动会保留整个子树。
删除子树
deleted, err := tree.Delete(ctx, nodeID)
deleted, err := tree.DeleteSubtree(ctx, nodeID)
Delete 和 DeleteSubtree 都会删除当前节点和所有后代,然后闭合树区间。
多棵树隔离
一张表里放多棵独立树时使用 Scope,例如租户树或应用树:
tree := nestedset.Use[Category](db, nestedset.Scope(oro.Map{
"TenantID": uint64(1),
}))
scope 会应用到所有读取和结构性写入。新建节点时,scope 值也会写入模型。
统一 Apply 查询
复杂读取推荐直接使用核心查询链的 Apply:
nodes, err := db.Use[Category]().
Apply(nestedset.DepthGte(1), nestedset.DefaultOrder()).
Get(ctx)
items, err := db.Use[Category]().
Apply(nestedset.DescendantsOf(root), nestedset.DefaultOrder()).
Where("Status", "active").
Get(ctx)
DescendantsOf(root) 可以传当前模型;扩展会按模型 ID 重新读取最新左右值,避免使用过期对象上的 _lft/_rgt。
支持 Roots、AncestorsOf、AncestorsAndSelfOf、DescendantsOf、DescendantsAndSelfOf、DescendantsWithinDepthOf、DescendantsAtDepthOf、SiblingsOf、Depth、DepthGte、DepthLte、DefaultOrder 和 Reversed。
tree.Query() 仍作为兼容薄封装保留,但新代码推荐统一使用 db.Use[Category]().Apply(...),这样不会让扩展包再维护一套 Where/Get/Limit API。
检查和重建
result, err := tree.Check(ctx)
if err != nil {
return err
}
if !result.Valid {
err = tree.Rebuild(ctx)
}
Rebuild 会根据 ParentID 重新计算 Lft、Rgt 和 Depth。
也可以传入显式嵌套结构并持久化:
err := tree.RebuildTree(ctx, []*nestedset.Node[Category]{
{
Model: &Category{Name: "Root"},
Children: []*nestedset.Node[Category]{
{Model: &Category{Name: "Child"}},
},
},
})
payload 里的新模型会插入;带 ID 的已有模型会更新。