树型持久化


1、方案

  树型结构是比较常见的,在数据库设计时,一般有两种方案。

  • 一是采用 ParentID 字段标记父节点的 ID,这种方案的优点是新增或移动节点比较方便,但是递归遍列子节点时比较麻烦 。

  • 二是使用类似 0001-0001-0002 这样的字符串编码进行,这种方案的优点是递归遍列子节点时使用 Like 关键字就可以,但是新增或移动节点时比较麻烦。

  Fireasy 融合了两种方法,以便能够让开发者更方便地构建树结构的功能模块。


2、定义

  首先,为了方便观察对象的操作过程,我们在 regions 表中建立以下基础数据。

名称 全名称 编码 层级别 顺序
云南 云南 0001 1 1
  昆明 云南\昆明 00010001 2 1
    五华 云南\昆明\五华 000100010001 3 1
    西山 云南\昆明\西山 000100010002 3 2
  红河 云南\红河 00010002 2 2
  玉溪 云南\玉溪 00010003 2 3
    澄江 云南\玉溪\澄江 000100030001 3 1
四川 四川 0002 1 1

  树型实体的映射可参考 树型映射Regions 类的定义如下所示:

[EntityMapping("regions")]
[EntityTreeMapping(
    InnerSign = nameof(Regions.Code),
    Name = nameof(Regions.Name),
    FullName = nameof(Regions.FullName),
    Level = nameof(Regions.Level),
    Order = nameof(Regions.Order),
    SignLength = 4)]
public class Regions : LightEntity<Regions>
{
    [PropertyMapping(ColumnName = "region_id", IsPrimaryKey = true, GenerateType = IdentityGenerateType.AutoIncrement, IsNullable = false)]
    public virtual int DeptId { get; set; }
    
    [PropertyMapping(ColumnName = "code", IsNullable = false)]
    public virtual string Code { get; set; }
    
    [PropertyMapping(ColumnName = "name", IsNullable = false)]
    public virtual string Name { get; set; }
    
    [PropertyMapping(ColumnName = "full_name", IsNullable = false)]
    public virtual string FullName { get; set; }
    
    [PropertyMapping(ColumnName = "level", IsNullable = false)]
    public virtual int Level { get; set; }
    
    [PropertyMapping(ColumnName = "order", IsNullable = false)]
    public virtual int Order { get; set; }
}

3、持久化

  EntityContext 类提供了一个 CreateTreeRepository 方法,它用于返回一个 ITreeRepository<T> 实例,该接口提供了针对树型结构的一些操作方法,将在后续一一介绍。如下所示:

[TestMethod]
public void TestCreateTreeRepository()
{
    using (var db = new DbContext())
    {
        var treeRep = db.CreateTreeRepository<Regions>();
    }
}

4、判断是否有子节点

  HasChildren 方法用于判断某一节点下面是否有子节点。如下所示:

[TestMethod]
public void TestHasChildren()
{
    using (var db = new DbContext())
    {
        var parent = db.Regions.FirstOrDefault(s => s.Name == "云南");
        var treeRep = db.CreateTreeRepository<Regions>();
        Assert.IsTrue(treeRep.HasChildren(parent));
    }
}

  你可以在 HasChildren 方法中使用 lambda 表达式附加其他查询条件。如下所示:

[TestMethod]
public void TestHasChildren()
{
    using (var db = new DbContext())
    {
        var parent = db.Regions.FirstOrDefault(s => s.Name == "云南");
        var treeRep = db.CreateTreeRepository<DRegionsepts>();
        Assert.IsFalse(treeRep.HasChildren(parent), s => s.Name.EndsWith("州"));
    }
}

5、返回子节点

  QueryChildren 方法用于返回某一个节点下面的子节点。如下所示:

[TestMethod]
public void TestQueryChildren()
{
    using (var db = new DbContext())
    {
        var parent = db.Regions.FirstOrDefault(s => s.Name == "云南");
        var treeRep = db.CreateTreeRepository<Regions>();
        var children = treeRep.QueryChildren(parent);
    }
}

  QueryChildren 方法的返回类型是 IQueryable 接口,意味着你可以在其后面使用 Where、OrderBy、Select 等方法。如下所示:

[TestMethod]
public void TestQueryChildren()
{
    using (var db = new DbContext())
    {
        var parent = db.Regions.FirstOrDefault(s => s.Name == "云南");
        var treeRep = db.CreateTreeRepository<Regions>();
        var children = treeRep.QueryChildren(parent)
            .Where(s => s.Name.EndsWith("州"))
            .Select(s => new { s.Name, s.Code });
    }
}

  当你在返回子节点的同时,可以与 HasChildren 方法一起使用,可以将子节点是否还有子节点一同带回来。如下所示:

[TestMethod]
public void TestQueryChildren()
{
    using (var db = new DbContext())
    {
        var parent = db.Regions.FirstOrDefault(s => s.Name == "云南");
        var treeRep = db.CreateTreeRepository<Regions>();
        var children = treeRep.QueryChildren(parent)
            .Where(s => s.Name.EndsWith("州"))
            .Select(s => new 
            { 
                Name = s.Name, 
                Code = s.Code, 
                HasChildren = treeReps.HasChildren(s, null) 
            });
    }
}

6、插入 / 批量插入节点

  Insert 方法用于将一个子节点插入到参考的节点之下(即孩子)。如下所示:

[TestMethod]
public void TestInsert()
{
    using (var db = new DbContext())
    {
        var parent = db.Regions.FirstOrDefault(s => s.Name == "云南");
        var treeRep = db.CreateTreeRepository<Regions>();
        var child = new Regions { Name = "西双版纳" };
        treeRep.Insert(child, parent);
    }
}

  可以看到,插入节点时,不需要设置较繁琐的 Code,以及其他相关属性,Fireasy 会通通给你设置好。

  BatchInsert 方法的使用与 Insert 相类似,用于批量插入多个子节点。如下所示:

[TestMethod]
public void TestBatchInsert()
{
    using (var db = new DbContext())
    {
        var parent = db.Regions.FirstOrDefault(s => s.Name == "云南");
        var treeRep = db.CreateTreeRepository<Regions>();
        var children = new List<Regions>
        {
            new Regions { Name = "西双版纳" },
            new Regions { Name = "德宏" },
            new Regions { Name = "楚雄" },
            new Regions { Name = "大理" }
        };
        
        treeRep.BatchInsert(children, parent);
    }
}

7、移动节点

  Move 方法用于将一个节点移动到另外一个节点之下,移到之前和之后的逻辑暂未实现。如下所示:

[TestMethod]
public void TestMove()
{
    using (var db = new DbContext())
    {
        var treeRep = db.CreateTreeRepository<Regions>();
        var region1 = db.Depts.FirstOrDefault(s => s.Name == "昆明");
        var region2 = db.Depts.FirstOrDefault(s => s.Name == "四川");

        treeRep.Move(region1, region2);
    }
}

9、递归父节点

  RecurrenceParent 方法可以递归返回一个节点的所有父节点。如下所示:

[TestMethod]
public void TestQueryChildren()
{
    using (var db = new DbContext())
    {
        var child = db.Regions.FirstOrDefault(s => s.Name == "澄江");
        var treeRep = db.CreateTreeRepository<Regions>();
        var children = treeRep.RecurrenceParent(child)
            .Select(s => new { s.Name, s.Code });
    }
}

  以上示例方法将会依次返回 玉溪、云南 两个父节点。


10、数据隔离

  由于一张表里编码都是连续的,但有些时候需要按特定的字段来进行隔离,比如使用机构隔离后,编码将在某一机构里是唯一的,对于全表来说,编码不是唯一的。 Insert / BatchInsert / Move 方法都有一个 lambda 表达式的参数 isolation,它允许你指定所需要作为隔离的实体条件。如下所示:

[TestMethod]
public void TestInsertWithIsolation()
{
    using (var db = new DbContext())
    {
        var parent = db.Regions.FirstOrDefault(s => s.Name == "云南");
        var treeRep = db.CreateTreeRepository<Regions>();
        var child = new Regions { Name = "西双版纳" };
        treeRep.Insert(child, parent, () => new Regions { SysCode = "01" });
    }
}

  以上的示例模拟了使用 SysCode 来隔离数据,这样就不需要把数据拆分成几张表了。