From ecf1b415b6d2f04366e5afe7ae4af2d3a122e104 Mon Sep 17 00:00:00 2001 From: zheng <511927343@qq.com> Date: Sun, 28 Jun 2026 08:42:35 +0800 Subject: [PATCH 1/2] =?UTF-8?q?tdengine=E6=89=B9=E9=87=8F=E6=8F=92?= =?UTF-8?q?=E5=85=A5=E8=B6=85=E7=BA=A7=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../TDengineCrud/TDengineBatchTest.cs | 62 ++++++++++ .../TDengine/Tables/PointData.cs | 26 ++++ FreeSql/Interface/Curd/IInsert.cs | 2 + .../Internal/CommonProvider/InsertProvider.cs | 8 ++ .../Curd/TDengineInsert.cs | 112 ++++++++++++++++-- 5 files changed, 198 insertions(+), 12 deletions(-) create mode 100644 FreeSql.Tests/FreeSql.Tests.Provider.TDengine/TDengine/TDengineCrud/TDengineBatchTest.cs create mode 100644 FreeSql.Tests/FreeSql.Tests.Provider.TDengine/TDengine/Tables/PointData.cs diff --git a/FreeSql.Tests/FreeSql.Tests.Provider.TDengine/TDengine/TDengineCrud/TDengineBatchTest.cs b/FreeSql.Tests/FreeSql.Tests.Provider.TDengine/TDengine/TDengineCrud/TDengineBatchTest.cs new file mode 100644 index 000000000..222776a53 --- /dev/null +++ b/FreeSql.Tests/FreeSql.Tests.Provider.TDengine/TDengine/TDengineCrud/TDengineBatchTest.cs @@ -0,0 +1,62 @@ +using FreeSql.Tests.Provider.TDengine.TDengine.Tables; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit.Abstractions; + +namespace FreeSql.Tests.Provider.TDengine.TDengine.TDengineCrud; + +public class TDengineBatchTest +{ + private readonly ITestOutputHelper _output; + + // 通过构造函数注入 + public TDengineBatchTest(ITestOutputHelper output) + { + _output = output; + } + + [Fact] + public void BatchInsertToSuperTable() + { + var fsql = g.tdengine; + fsql.CodeFirst.SyncStructure(); + + var generateCount = 2_000; // 生成数据数量 + var dt = DateTime.Now; + var data = Enumerable.Range(1, generateCount) + .Select(t => new PointDataEntity + { + Ts = dt, + PointNumber = $"001A{t:D5}", + PointValue = 345 + }) + .ToList(); + // 生成第二批数据,时间戳加1秒,IsAlarm为true + var data2 = Enumerable.Range(1, generateCount) + .Select(t => new PointDataEntity + { + Ts = dt.AddSeconds(1), + PointNumber = $"001A{t:D5}", + IsAlarm = true, + PointValue = 123.45 + }) + .ToList(); + data.AddRange(data2); + + var insertable = fsql.Insert(data) + .AsTable(t => $"child_{t.PointNumber}") // 自定义子表名 + .BatchOptions(5000, 5000, false); // tdengine 关闭事务 + + //_output.WriteLine($"集合中总数量:{data.Count}"); + //var sql = insertable.ToSql(); + //_output.WriteLine(sql); + + var affrows = insertable.ExecuteAffrows(); + + Assert.NotEqual(0, affrows); + } +} diff --git a/FreeSql.Tests/FreeSql.Tests.Provider.TDengine/TDengine/Tables/PointData.cs b/FreeSql.Tests/FreeSql.Tests.Provider.TDengine/TDengine/Tables/PointData.cs new file mode 100644 index 000000000..d114b3070 --- /dev/null +++ b/FreeSql.Tests/FreeSql.Tests.Provider.TDengine/TDengine/Tables/PointData.cs @@ -0,0 +1,26 @@ +using FreeSql.DataAnnotations; +using FreeSql.Provider.TDengine.Attributes; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace FreeSql.Tests.Provider.TDengine.TDengine.Tables; + +[TDengineSuperTable(Name = "point_data")] +public class PointDataEntity +{ + [Column(Name = "ts")] + public DateTime Ts { get; set; } + + [Column(Name = "is_alarm")] + public bool IsAlarm { get; set; } + + [Column(Name = "point_value")] + public double PointValue { get; set; } + + [TDengineTag(Name = "point_number")] + public string PointNumber { get; set; } + +} \ No newline at end of file diff --git a/FreeSql/Interface/Curd/IInsert.cs b/FreeSql/Interface/Curd/IInsert.cs index 0ecee4b97..9400419ba 100644 --- a/FreeSql/Interface/Curd/IInsert.cs +++ b/FreeSql/Interface/Curd/IInsert.cs @@ -120,6 +120,8 @@ public interface IInsert where T1 : class /// IInsert BatchProgress(Action> callback); + IInsert AsTable(Func tableName); + /// /// 设置表名规则,可用于分库/分表,参数1:默认表名;返回值:新表名; /// diff --git a/FreeSql/Internal/CommonProvider/InsertProvider.cs b/FreeSql/Internal/CommonProvider/InsertProvider.cs index 5c32157ee..fd01053ac 100644 --- a/FreeSql/Internal/CommonProvider/InsertProvider.cs +++ b/FreeSql/Internal/CommonProvider/InsertProvider.cs @@ -41,6 +41,8 @@ public abstract partial class InsertProvider : InsertProvider, IInsert w internal List _sourceOld; public Action> _batchProgress; + public Func _customTableRule; + public InsertProvider(IFreeSql orm, CommonUtils commonUtils, CommonExpression commonExpression) { _orm = orm; @@ -744,5 +746,11 @@ public DataTable ToDataTable() } return dt; } + + public IInsert AsTable(Func tableName) + { + this._customTableRule = tableName; + return this; + } } } diff --git a/Providers/FreeSql.Provider.TDengine/Curd/TDengineInsert.cs b/Providers/FreeSql.Provider.TDengine/Curd/TDengineInsert.cs index cefc23e91..43be43e5a 100644 --- a/Providers/FreeSql.Provider.TDengine/Curd/TDengineInsert.cs +++ b/Providers/FreeSql.Provider.TDengine/Curd/TDengineInsert.cs @@ -46,19 +46,107 @@ public override List ExecuteInserted() => base.SplitExecuteInserted( public override string ToSql() { - //处理Insert忽略Tag - var ignoreColumnList = _ignoreInsertColumns.GetOrAdd(typeof(T1), s => - { - //如果是超表不处理 - if (!s.IsDefined(typeof(TDengineSubTableAttribute))) return new List(0); - var tableByEntity = _commonUtils.GetTableByEntity(s); - var keyValuePairs = tableByEntity.Properties.Where(pair => - pair.Value.GetCustomAttribute() != null); - return keyValuePairs.Select(keyValuePair => keyValuePair.Value.Name).ToList(); + if (_customTableRule != null) + { + return ToSTableBatchInsertSql(); + } + else + { + //处理Insert忽略Tag + var ignoreColumnList = _ignoreInsertColumns.GetOrAdd(typeof(T1), s => + { + //如果是超表不处理 + if (!s.IsDefined(typeof(TDengineSubTableAttribute))) return new List(0); + var tableByEntity = _commonUtils.GetTableByEntity(s); + var keyValuePairs = tableByEntity.Properties.Where(pair => + pair.Value.GetCustomAttribute() != null); + return keyValuePairs.Select(keyValuePair => keyValuePair.Value.Name).ToList(); + }); + if (InternalIsIgnoreInto == false) return base.ToSqlValuesOrSelectUnionAll(ignoreColumn: ignoreColumnList); + var sql = base.ToSqlValuesOrSelectUnionAll(ignoreColumn: ignoreColumnList); + return $"INSERT IGNORE INTO {sql.Substring(12)}"; + } + } + + /// + /// Key 为 T1 Type,Value 为 Tuple<超级表名, Tag属性数组, Value属性数组> + /// + private static ConcurrentDictionary> _superTableColumns = + new ConcurrentDictionary>(); + + private string ToSTableBatchInsertSql() + { + if (_source == null || _source.Count == 0) return null; + + var typePropertiesCached = _superTableColumns.GetOrAdd(typeof(T1), s => + { + var tableInfo = _commonUtils.GetTableByEntity(s); + var stableName = tableInfo.DbName; + var tagColumnNames = tableInfo.Properties + .Where(kvp => kvp.Value.GetCustomAttribute() != null) + .Select(kvp => kvp.Value.Name).ToList(); + var tagColumns = tableInfo.Columns.Values + .Where(col => tagColumnNames.Contains(col.CsName)) + .ToArray(); + + var valueColumns = tableInfo.Columns.Values + .Except(tagColumns) + .Where(col => !_ignore.ContainsKey(col.CsName)) + .ToArray(); + return new Tuple(stableName, tagColumns, valueColumns); }); - if (InternalIsIgnoreInto == false) return base.ToSqlValuesOrSelectUnionAll(ignoreColumn: ignoreColumnList); - var sql = base.ToSqlValuesOrSelectUnionAll(ignoreColumn: ignoreColumnList); - return $"INSERT IGNORE INTO {sql.Substring(12)}"; + + var tagNames = string.Join(",", typePropertiesCached.Item2.Select(x => x.Attribute.Name)); + var valueColumnNames = string.Join(",", typePropertiesCached.Item3.Select(x => x.Attribute.Name)); + var groups = _source.GroupBy(_customTableRule); + var sql = new StringBuilder(); + sql.Append("INSERT INTO "); + foreach (var group in groups) + { + string childTableName = group.Key; + var batchList = group.ToList(); + // 2. 取第一条数据(用于生成 TAGS) + var first = batchList.First(); + var tagValues = typePropertiesCached.Item2 + .Select(p => FormatValue(p.GetDbValue(first))) + .ToArray(); + var itemValueStringList = new List(); + foreach (var item in batchList) + { + var itemValues = typePropertiesCached.Item3.Select(p => FormatValue(p.GetDbValue(item))).ToArray(); + var valueString = string.Join(", ", itemValues); + itemValueStringList.Add($"({valueString})"); + } + + var values = string.Join(" ", itemValueStringList); + // TODO 待完善 + // 4. 拼 TDengine SQL + sql.AppendLine().Append($"{childTableName} ") //子表名,不需要使用`包裹,可以忽略大小写 + .Append($"USING {typePropertiesCached.Item1} ") //超级表名 + .Append($"({tagNames}) ") //超级表名 + .Append($"tags({string.Join(", ", tagValues)}) ") //子表tags + .AppendLine($"({valueColumnNames})") //除Tags和Ignored外的字段,不需要使用`包裹,可以忽略大小写 + .Append($"values {values}"); + } + return sql.ToString(); + } + + private static string FormatValue(object v) + { + if (v == null) + return "NULL"; + + if (v is string s) + { + return "'" + s.Replace("'", "''") + "'"; + } + + if (v is DateTime d) + { + return $"'{d:O}'"; + } + + return v.ToString(); } private static ConcurrentDictionary> _ignoreInsertColumns = From 87a0d20b639c132945757148df5eab6286eb03ad Mon Sep 17 00:00:00 2001 From: zheng <511927343@qq.com> Date: Sat, 4 Jul 2026 08:23:07 +0800 Subject: [PATCH 2/2] =?UTF-8?q?tdengine=E6=8F=92=E5=85=A5=E8=B6=85?= =?UTF-8?q?=E7=BA=A7=E8=A1=A8=E6=96=B9=E6=B3=95=E5=90=8D=E7=A7=B0=E4=BF=AE?= =?UTF-8?q?=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../TDengine/TDengineCrud/TDengineBatchTest.cs | 2 +- FreeSql/Interface/Curd/IInsert.cs | 7 ++++++- FreeSql/Internal/CommonProvider/InsertProvider.cs | 6 +++--- Providers/FreeSql.Provider.TDengine/Curd/TDengineInsert.cs | 7 +++---- 4 files changed, 13 insertions(+), 9 deletions(-) diff --git a/FreeSql.Tests/FreeSql.Tests.Provider.TDengine/TDengine/TDengineCrud/TDengineBatchTest.cs b/FreeSql.Tests/FreeSql.Tests.Provider.TDengine/TDengine/TDengineCrud/TDengineBatchTest.cs index 222776a53..fb85688a2 100644 --- a/FreeSql.Tests/FreeSql.Tests.Provider.TDengine/TDengine/TDengineCrud/TDengineBatchTest.cs +++ b/FreeSql.Tests/FreeSql.Tests.Provider.TDengine/TDengine/TDengineCrud/TDengineBatchTest.cs @@ -48,7 +48,7 @@ public void BatchInsertToSuperTable() data.AddRange(data2); var insertable = fsql.Insert(data) - .AsTable(t => $"child_{t.PointNumber}") // 自定义子表名 + .AsTdEngineTableName(t => $"child_{t.PointNumber}") // 自定义子表名 .BatchOptions(5000, 5000, false); // tdengine 关闭事务 //_output.WriteLine($"集合中总数量:{data.Count}"); diff --git a/FreeSql/Interface/Curd/IInsert.cs b/FreeSql/Interface/Curd/IInsert.cs index 9400419ba..c115fd411 100644 --- a/FreeSql/Interface/Curd/IInsert.cs +++ b/FreeSql/Interface/Curd/IInsert.cs @@ -120,7 +120,12 @@ public interface IInsert where T1 : class /// IInsert BatchProgress(Action> callback); - IInsert AsTable(Func tableName); + /// + /// 设置TdEngine子表名称规则 + /// + /// + /// + IInsert AsTdEngineTableName(Func tableNameRule); /// /// 设置表名规则,可用于分库/分表,参数1:默认表名;返回值:新表名; diff --git a/FreeSql/Internal/CommonProvider/InsertProvider.cs b/FreeSql/Internal/CommonProvider/InsertProvider.cs index fd01053ac..6cb782b65 100644 --- a/FreeSql/Internal/CommonProvider/InsertProvider.cs +++ b/FreeSql/Internal/CommonProvider/InsertProvider.cs @@ -41,7 +41,7 @@ public abstract partial class InsertProvider : InsertProvider, IInsert w internal List _sourceOld; public Action> _batchProgress; - public Func _customTableRule; + public Func _tdEngineChildTableRule; public InsertProvider(IFreeSql orm, CommonUtils commonUtils, CommonExpression commonExpression) { @@ -747,9 +747,9 @@ public DataTable ToDataTable() return dt; } - public IInsert AsTable(Func tableName) + public IInsert AsTdEngineTableName(Func tableNameRule) { - this._customTableRule = tableName; + this._tdEngineChildTableRule = tableNameRule; return this; } } diff --git a/Providers/FreeSql.Provider.TDengine/Curd/TDengineInsert.cs b/Providers/FreeSql.Provider.TDengine/Curd/TDengineInsert.cs index 43be43e5a..e7747a0a8 100644 --- a/Providers/FreeSql.Provider.TDengine/Curd/TDengineInsert.cs +++ b/Providers/FreeSql.Provider.TDengine/Curd/TDengineInsert.cs @@ -46,7 +46,7 @@ public override List ExecuteInserted() => base.SplitExecuteInserted( public override string ToSql() { - if (_customTableRule != null) + if (_tdEngineChildTableRule != null) { return ToSTableBatchInsertSql(); } @@ -98,7 +98,7 @@ private string ToSTableBatchInsertSql() var tagNames = string.Join(",", typePropertiesCached.Item2.Select(x => x.Attribute.Name)); var valueColumnNames = string.Join(",", typePropertiesCached.Item3.Select(x => x.Attribute.Name)); - var groups = _source.GroupBy(_customTableRule); + var groups = _source.GroupBy(_tdEngineChildTableRule); var sql = new StringBuilder(); sql.Append("INSERT INTO "); foreach (var group in groups) @@ -119,8 +119,7 @@ private string ToSTableBatchInsertSql() } var values = string.Join(" ", itemValueStringList); - // TODO 待完善 - // 4. 拼 TDengine SQL + // 拼 TDengine SQL sql.AppendLine().Append($"{childTableName} ") //子表名,不需要使用`包裹,可以忽略大小写 .Append($"USING {typePropertiesCached.Item1} ") //超级表名 .Append($"({tagNames}) ") //超级表名