Admin Core

DataTagTest 分表设计笔记

DataTagTest 用于保存测试标签采集数据。采集数据按 CollectTime 做月分表,写入数据前由业务服务主动同步当前月份及未来月份的物理表,避免程序长期运行后因为未来月份表不存在导致插入失败。

1. 目标

DataTagTest 用于保存测试标签采集数据。采集数据按 CollectTime 做月分表,写入数据前由业务服务主动同步当前月份及未来月份的物理表,避免程序长期运行后因为未来月份表不存在导致插入失败。

当前设计不在程序启动时自动建表,建表动作由 DataTagTestService.AddAsync 在写入前触发。

2. 相关文件

文件 作用
NPP.IOT.Api.Contracts/Domain/DataCollection/DataTagTestEntity.cs 定义采集数据实体、表名模板、分表规则和索引
NPP.IOT.Api/Core/Helper/DateShardingTableHelper.cs 公共日期分表同步 Helper
NPP.IOT.Api/Services/DataTagTest/DataTagTestService.cs 写入采集数据前调用 Helper 同步物理分表
NPP.IOT.Host/Program.cs 当前不再在启动阶段自动创建DataTagTest 分表

3. 实体设计

实体类:DataTagTestEntity

位置:NPP.IOT.Api.Contracts/Domain/DataCollection/DataTagTestEntity.cs

核心配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[Table(Name = DbConsts.TableNamePrefix + "tag_test_{yyyyMM}", AsTable = "CollectTime=2026-1-1(1 month)")]
[Index("idx_{tablename}_01", nameof(TagId))]
[Index("idx_{tablename}_02", nameof(TagCode))]
[Index("idx_{tablename}_03", nameof(GatewayCode))]
[Index("idx_{tablename}_04", nameof(CollectTime))]
public partial class DataTagTestEntity : EntityData
{
public long TagId { get; set; }
public string TagCode { get; set; }
public string GatewayCode { get; set; }
public string Value { get; set; }
public int? ValueType { get; set; }
public DateTime CollectTime { get; set; }
}

说明:

  • Name = DbConsts.TableNamePrefix + "tag_test_{yyyyMM}" 定义物理表名模板。
  • DbConsts.TableNamePrefix 当前是 iot_
  • 实际表名形如:
1
2
3
iot_tag_test_202601
iot_tag_test_202602
iot_tag_test_202603
  • AsTable = "CollectTime=2026-1-1(1 month)" 表示按 CollectTime 字段从 2026-01-01 开始,每 1 个月分一张表。
  • CollectTime 是分表字段,也是写入时决定落到哪张物理表的关键字段。

4. Helper 设计

公共 Helper:DateShardingTableHelper

位置:NPP.IOT.Api/Core/Helper/DateShardingTableHelper.cs

设计目的:

  • 不把分表同步逻辑写死在 DataTagTestService 中。
  • 不再使用 DataTagTestTableSyncer 这种只服务单个实体的同步器。
  • 后续其他按日期分表的实体也可以复用同一个 Helper。

当前代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
using FreeSql;

namespace NPP.IOT.Api.Core.Helper;

/// <summary>
/// 日期分表同步帮助类
/// </summary>
public static class DateShardingTableHelper
{
/// <summary>
/// 同步当前月和指定未来月份范围的物理分表
/// </summary>
public static void SyncFutureMonthlyTables<TEntity>(IFreeSql db, DateTime baseTime, int futureMonthCount)
{
SyncMonthlyTables<TEntity>(db, baseTime, futureMonthCount + 1);
}

/// <summary>
/// 同步指定月份范围的物理分表
/// </summary>
public static void SyncMonthlyTables<TEntity>(IFreeSql db, DateTime startMonth, int monthCount)
{
if (monthCount <= 0)
{
return;
}

var entityType = typeof(TEntity);
var table = db.CodeFirst.GetTableByEntity(entityType);
var month = ToMonth(startMonth);
var tableNames = new List<string>();

for (var i = 0; i < monthCount; i++)
{
var tableName = table.AsTableImpl.GetTableNameByColumnValue(month.AddMonths(i), autoExpand: true);
tableNames.Add(tableName);
if (!db.DbFirst.ExistsTable(tableName))
{
db.CodeFirst.SyncStructure(entityType, tableName);
}
}

table.AsTableImpl.SetDefaultAllTables(_ => tableNames.ToArray());
}

private static DateTime ToMonth(DateTime value)
{
return new DateTime(value.Year, value.Month, 1);
}
}

4.1 SyncFutureMonthlyTables 的含义

1
DateShardingTableHelper.SyncFutureMonthlyTables<DataTagTestEntity>(_db, input.CollectTime, 1);

含义是:

  • input.CollectTime 所在月份为基准。
  • 同步当前月。
  • 再同步未来 futureMonthCount 个月。

例如:

1
2
baseTime = new DateTime(2028, 3, 15);
futureMonthCount = 1;

会同步:

1
2
iot_tag_test_202803
iot_tag_test_202804

如果希望同步当前月 + 未来 3 个月,则调用:

1
DateShardingTableHelper.SyncFutureMonthlyTables<DataTagTestEntity>(_db, input.CollectTime, 3);

对应表:

1
2
3
4
iot_tag_test_202803
iot_tag_test_202804
iot_tag_test_202805
iot_tag_test_202806

4.2 SyncMonthlyTables 的执行流程

1
DateShardingTableHelper.SyncMonthlyTables<DataTagTestEntity>(_db, startMonth, monthCount);

执行步骤:

  1. 判断 monthCount <= 0 时直接返回。
  2. 通过 typeof(TEntity) 获取实体类型。
  3. 调用 db.CodeFirst.GetTableByEntity(entityType) 获取 FreeSql 表元数据。
  4. 把传入时间规整到当月 1 号。
  5. 循环 monthCount 次,每次增加 1 个月。
  6. 调用 table.AsTableImpl.GetTableNameByColumnValue(...) 根据实体的 AsTable 配置计算真实物理表名。
  7. 调用 db.DbFirst.ExistsTable(tableName) 判断表是否存在。
  8. 表不存在时调用 db.CodeFirst.SyncStructure(entityType, tableName) 创建物理表。
  9. 最后通过 table.AsTableImpl.SetDefaultAllTables(...) 设置当前实体默认参与查询的物理表范围。

5. Service 中如何调用生成表

服务类:DataTagTestService

位置:NPP.IOT.Api/Services/DataTagTest/DataTagTestService.cs

当前构造函数注入:

1
2
3
4
5
6
7
8
private readonly IFreeSql _db;
private readonly IDataTagTestRepository _dataTagTestRep;

public DataTagTestService(IFreeSql db, IDataTagTestRepository dataTagTestRep)
{
_db = db;
_dataTagTestRep = dataTagTestRep;
}

AddAsync 写入前同步分表:

1
2
3
4
5
6
7
8
9
[AllowAnonymous]
public async Task<long> AddAsync(DataTagTestAddInput input)
{
DateShardingTableHelper.SyncFutureMonthlyTables<DataTagTestEntity>(_db, input.CollectTime, 1);

var entity = Mapper.Map<DataTagTestEntity>(input);
await _dataTagTestRep.InsertAsync(entity);
return entity.Id;
}

调用顺序:

1
2
3
4
5
6
7
8
9
10
11
12
13
接口收到新增请求

读取 input.CollectTime

调用 DateShardingTableHelper.SyncFutureMonthlyTables<DataTagTestEntity>()

确保目标月份及未来月份物理表存在

Mapper.Map<DataTagTestEntity>(input)

_dataTagTestRep.InsertAsync(entity)

FreeSql 根据 CollectTime 写入对应月表

设计重点:

  • 必须先同步表,再执行插入。
  • 分表基准使用 input.CollectTime,不是服务器当前时间。
  • 这样可以支持补录历史数据、写入未来采集时间等场景。

6. 程序启动时不自动建表

位置:NPP.IOT.Host/Program.cs

当前启动阶段配置:

1
2
3
4
//配置FreeSql同步结构
ConfigureFreeSqlSyncStructure = (freeSql, dbConfig) =>
{
},

说明:

  • 启动时不调用 DateShardingTableHelper
  • 启动时不创建 DataTagTest 分表。
  • 表创建由 DataTagTestService.AddAsync 在写入前按需触发。

这样可以避免程序启动时一次性创建大量未来表,也避免启动逻辑和某个业务实体强绑定。

7. 如何扩展到其他按日期分表实体

假设新增实体 TagDataEntity,只要实体上配置了 FreeSql 分表规则:

1
2
3
4
5
[Table(Name = DbConsts.TableNamePrefix + "tag_data_{yyyyMM}", AsTable = "CollectTime=2026-1-1(1 month)")]
public class TagDataEntity
{
public DateTime CollectTime { get; set; }
}

在对应 Service 写入前调用:

1
DateShardingTableHelper.SyncFutureMonthlyTables<TagDataEntity>(_db, input.CollectTime, 1);

或同步固定月份范围:

1
DateShardingTableHelper.SyncMonthlyTables<TagDataEntity>(_db, new DateTime(2028, 1, 1), 12);

8. 注意事项

  1. DateShardingTableHelper 依赖实体上的 FreeSql [Table(..., AsTable = ...)] 配置。
  2. SyncFutureMonthlyTables<TEntity>futureMonthCount 表示未来月份数量,不包含当前月。
  3. 当前月会通过 futureMonthCount + 1 一起同步。
  4. SyncMonthlyTables<TEntity> 会调用 SetDefaultAllTables,影响后续当前实体默认查询的物理表范围。
  5. 不建议在 Program.cs 中为单个业务实体写自动建表逻辑。
  6. 插入前同步表可以保证长期运行时新月份表能被创建。

9. 当前 DataTagTest 的完整调用示例

1
2
3
4
5
6
7
8
public async Task<long> AddAsync(DataTagTestAddInput input)
{
DateShardingTableHelper.SyncFutureMonthlyTables<DataTagTestEntity>(_db, input.CollectTime, 1);

var entity = Mapper.Map<DataTagTestEntity>(input);
await _dataTagTestRep.InsertAsync(entity);
return entity.Id;
}

如果 input.CollectTime = 2028-03-15,当前配置会确保以下表存在后再插入:

1
2
iot_tag_test_202803
iot_tag_test_202804

插入时 FreeSql 会根据 CollectTime 把数据写入:

1
iot_tag_test_202803

#中台 #中台/数据库分表 #FreeSql #API接口 #仓储模式