树型持久化
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 来隔离数据,这样就不需要把数据拆分成几张表了。