组织机构的实体设计与时态表的应用

周末远程面试的时候,提到以前项目中的难点,当时提到了组织架构的问题,这个问题在当时挺棘手的,组织架构是六层的公司/部门/大区/区域/门店/店组这样的结构,除了店组其他的层级上都有负责人,其中某层会带着下面的层级整个移动到另一个层级下,查询的时候要在时间范围内查出某个组织层级下所有的人员变动以及组织变动情况以及每层组织负责人的变动,当时采用了展平式的设计,其实是借鉴了ABP框架中的组织机构设计,属于成熟框架中直接拿来复用的,同时也带来了问题,由于查询的复杂性,所以性能是一个大的问题,虽然采用了缓存策略,但是由于变更频繁并且层级较深,集中爆发访问的时候数据库的压力依然很大,所以后来采用了资源换性能的方式处理,后来一直要重构也没有时间来处理。

面试的时候提到了这个问题,晚上突然想起来,隔了几年了,应该有更好的解决方案了,经过查询,SQL Server中的时态表恰好能够解决这个问题。

时态表

SQL Server 中的时态表(Temporal Tables),也称为系统版本控制时态表,是 SQL Server 2016 及更高版本引入的一项数据库功能

它的主要作用是提供对表中数据在 任何时间点 的状态信息,而不仅仅是当前正确的数据 1 . 这意味着你可以轻松地查看数据在过去某个时间点的样子,或者跟踪数据的历史变化

以下是时态表的一些关键特性:

1.
自动历史记录管理 :时态表会自动维护表中数据的完整历史记录,而无需手动编写触发器或复杂的应用程序逻辑
2.
两个表组成 :

  • 当前表(Current Table) :存储数据的最新版本。
  • 历史表(History Table) :存储对当前表进行更改(插入、更新、删除)时,数据的旧版本。历史表的名称可以由 SQL Server 自动生成,也可以手动指定

3.
时间周期列 :每个时态表都有两个显式定义的 datetime2 数据类型列,称为“周期列” 这些列由系统独占使用,用于记录每行数据的有效时间段。当行被修改时,系统会自动更新这些周期列。
4.
查询历史数据 :你可以使用特殊的 FOR SYSTEM_TIME 子句来查询特定时间点的数据,或者查询某个时间范围内的数据变化。
5.
使用场景 :时态表非常适用于需要审计、数据恢复、趋势分析或满足法规遵从性要求(如 GDPR)的场景,例如,跟踪客户信息的变更、订单状态的历史记录等。

总而言之,SQL Server 时态表提供了一种内置的、高效的方式来管理数据的历史版本,使得跟踪数据随时间的变化变得更加简单和透明。

应用场景

由上面的介绍可以看出,时态表的应用是广泛的,对于一些需要版本控制的数据,比如组织架构、人员信息、订单信息等,都可以采用时态表来设计,但同时也有弊端,由于每次的update,delete操作都会同时操作2个数据表,所以性能上会有影响,但是这个就是见仁见智,在操作反馈的事件中,用户可接受的阈值范围内,应该是可以做到无感的。

在架构设计中,本身就会设计一些历史记录表,用来记录数据的变更历史,但是这些历史记录表一般都是手动编写的,没有采用时态表的方式,所以在查询历史数据的时候,需要手动编写查询语句,而采用时态表的方式,就可以直接使用查询语句,不需要手动编写查询语句,同时也不需要手动维护历史记录表,这就大大简化了架构设计的复杂度。并且,原生数据库的支持也减少了开发成本。

实践

EF Core 6.0的时候开始了对时态表的支持, 举一个监单的例子,如下的实体:

1
2
3
4
5
6
7
// Person 实体
public class Person
{
public int PersonId { get; set; }
public string Name { get; set; }
public string EmployeeNumber { get; set; }
}

服务注册

1
2
3
4
5
builder.Services.AddDbContext<TDBContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"), args =>
{
args.UseHierarchyId();
}));

在DbContext中进行设定:

1
2
3
4
5
6
public DbSet<Person> Persons { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Person>()
.ToTable("Person", tb => tb.IsTemporal()); //IsTemporal表示为时态表,会自动增加影子属性PeriodEnd和PeriodStart
}

引用的包

  • Microsoft.EntityFrameworkCore
  • Microsoft.EntityFrameworkCore.SqlServer
  • Microsoft.EntityFrameworkCore.SqlServer.HierarchyId

以及

  • Microsoft.EntityFrameworkCore.Design
  • Microsoft.EntityFrameworkCore.Tools

进行迁移后,会生成两张表

Person表

PersonIdNameEmployeeNumber

PersonHistory表

PersonIdNameEmployeeNumberPeriodEndPeriodStart

当我们进行数据插入的时候,只会对Person表进行写入

Person表

PersonIdNameEmployeeNumber
1人员1000001

之后对数据进行变更的时候,例如修改,删除,会同时对Person表和PersonHistory表进行写入

Person表

PersonIdNameEmployeeNumber
1人员1100001

PersonHistory表

PersonIdNameEmployeeNumberPeriodEndPeriodStart
1人员10000012025-09-15 04:44:30.0842025-09-15 04:41:56.173

查询历史数据

正常的查询只要使用dbcontext.Person就可以查询到当前的所有数据,但是如果需要查询历史数据,就需要使用特殊的查询语句,如下:

await _dBContext.Persons.TemporalAll().Where(t => t.PersonId == 1).ToListAsync();
// 或者
await _dBContext.Persons.TemporalAsOf(DateTime.Now).Where(t => t.PersonId == 1).ToListAsync();
// 或者
await _dBContext.Persons.TemporalBetween(DateTime.Now, DateTime.Now.AddDays(1)).Where(t => t.PersonId == 1).ToListAsync();

不足

遗憾的是,目前,postgresql还没有原生支持的时态表。


组织机构的实体设计与时态表的应用
https://oujun.work/2025/09/15/组织机构的实体设计与时态表的应用.html
作者
欧俊
发布于
2025年9月15日
许可协议