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_。- 实际表名形如:
plainiot_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;
会同步:
plainiot_tag_test_202803 iot_tag_test_202804
如果希望同步当前月 + 未来 3 个月,则调用:
c#DateShardingTableHelper.SyncFutureMonthlyTables<DataTagTestEntity>(_db, input.CollectTime, 3);
对应表:
plainiot_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);
执行步骤:
- 判断
monthCount <= 0时直接返回。 - 通过
typeof(TEntity)获取实体类型。 - 调用
db.CodeFirst.GetTableByEntity(entityType)获取 FreeSql 表元数据。 - 把传入时间规整到当月 1 号。
- 循环
monthCount次,每次增加 1 个月。 - 调用
table.AsTableImpl.GetTableNameByColumnValue(...)根据实体的AsTable配置计算真实物理表名。 - 调用
db.DbFirst.ExistsTable(tableName)判断表是否存在。 - 表不存在时调用
db.CodeFirst.SyncStructure(entityType, tableName)创建物理表。 - 最后通过
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. 注意事项
DateShardingTableHelper依赖实体上的 FreeSql[Table(..., AsTable = ...)]配置。SyncFutureMonthlyTables<TEntity>的futureMonthCount表示未来月份数量,不包含当前月。- 当前月会通过
futureMonthCount + 1一起同步。 SyncMonthlyTables<TEntity>会调用SetDefaultAllTables,影响后续当前实体默认查询的物理表范围。- 不建议在
Program.cs中为单个业务实体写自动建表逻辑。 - 插入前同步表可以保证长期运行时新月份表能被创建。
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,当前配置会确保以下表存在后再插入:
plainiot_tag_test_202803 iot_tag_test_202804
插入时 FreeSql 会根据 CollectTime 把数据写入:
plainiot_tag_test_202803
#中台 #中台/数据库分表 #FreeSql #API接口 #仓储模式