技术文档

DataTagTest 分表设计笔记

状态:待学习优先级:未设置更新:2026-06-02Notion 原文
Notion KB

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

核心配置:

c#
[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_
  • 实际表名形如:
plain
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。

当前代码:

c#
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 的含义

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

含义是:

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

例如:

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

会同步:

plain
iot_tag_test_202803 iot_tag_test_202804

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

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

对应表:

plain
iot_tag_test_202803 iot_tag_test_202804 iot_tag_test_202805 iot_tag_test_202806

4.2 SyncMonthlyTables 的执行流程

c#
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

当前构造函数注入:

c#
private readonly IFreeSql _db; private readonly IDataTagTestRepository _dataTagTestRep; public DataTagTestService(IFreeSql db, IDataTagTestRepository dataTagTestRep) { _db = db; _dataTagTestRep = dataTagTestRep; }

AddAsync 写入前同步分表:

c#
[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; }

调用顺序:

plain
接口收到新增请求 ↓ 读取 input.CollectTime ↓ 调用 DateShardingTableHelper.SyncFutureMonthlyTables<DataTagTestEntity>() ↓ 确保目标月份及未来月份物理表存在 ↓ Mapper.Map<DataTagTestEntity>(input) ↓ _dataTagTestRep.InsertAsync(entity) ↓ FreeSql 根据 CollectTime 写入对应月表

设计重点:

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

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

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

当前启动阶段配置:

c#
//配置FreeSql同步结构 ConfigureFreeSqlSyncStructure = (freeSql, dbConfig) => { },

说明:

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

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

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

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

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

在对应 Service 写入前调用:

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

或同步固定月份范围:

c#
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 的完整调用示例

c#
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,当前配置会确保以下表存在后再插入:

plain
iot_tag_test_202803 iot_tag_test_202804

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

plain
iot_tag_test_202803

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