jetlinks默认支持TDengine存储数据,但是没有支持作为时序库。
TDengine如果要实现,需要扩展一下。
TDengine
作为时序库, 不讲理论,官方文档很清楚。
官网:时序数据库_实时数据库 - TDengine | 涛思数据
大家自己了解一下即可。主要是讲支持jetlinks的问题。
TDengine注意事项
执行接口
官方提供了2种方式。
(1)api接口
直接作为web服务,访问数据库
(2)客户端驱动
和传统数据库一样,提供各种语言的驱动包
要说明一下的是,jdbc不支持批量参数化sql.
语法
这个数据库的语法检查很严格,尤其是空格,转义要小心。
jetlinks扩展
继承接口即可,我把演示代码贴出来供大家参考。
扩展类:
java
package cn.iot.things.tdengine.timeseries;
import com.zaxxer.hikari.HikariDataSource;
import lombok.extern.slf4j.Slf4j;
import org.jetlinks.community.timeseries.TimeSeriesManager;
import org.jetlinks.community.timeseries.TimeSeriesMetadata;
import org.jetlinks.community.timeseries.TimeSeriesMetric;
import org.jetlinks.community.timeseries.TimeSeriesService;
import org.jetlinks.core.metadata.PropertyMetadata;
import org.jetlinks.core.metadata.types.*;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
import java.sql.*;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import static cn.iot.things.tdengine.timeseries.TDengineHelper.*;
@Slf4j
@Component
public class TdengineTimeSeriesManager implements TimeSeriesManager {
/**
* 所有表的tag列
*/
private static final ConcurrentHashMap<String,Map<String,String>> map=new ConcurrentHashMap<>();
private static final String TD_URL = "jdbc:TAOS-RS://localhost:6041/?";
private static final String TD_USER = "root";
private static final String TD_PWD = "taosdata";
private static final String TD_DB = "jetlinks_db";
private static volatile boolean db_init=false;
private static HikariDataSource hikariDs;
static {
hikariDs = new HikariDataSource();
hikariDs.setJdbcUrl(TD_URL.replace("/?","/"+TD_DB));
hikariDs.setUsername(TD_USER);
hikariDs.setPassword(TD_PWD);
hikariDs.setDriverClassName("com.taosdata.jdbc.rs.RestfulDriver");
hikariDs.setMaximumPoolSize(20);
hikariDs.setMinimumIdle(2);
hikariDs.setConnectionTimeout(3000);
hikariDs.setIdleTimeout(600000);
hikariDs.setMaxLifetime(1800000);
hikariDs.addDataSourceProperty("socketTimeout", "5000");
log.info("TDengine连接池初始化完成(写死配置)");
}
private Connection getConnection() throws SQLException {
Connection conn = hikariDs.getConnection();
conn.setAutoCommit(true);
return conn;
}
@Override
public TimeSeriesService getService(TimeSeriesMetric metric) {
return getService(metric.getId());
}
@Override
public TimeSeriesService getServices(TimeSeriesMetric... metric) {
if (metric.length == 1) {
return getService(metric[0].getId());
}
throw new UnsupportedOperationException("TDengine暂不支持多metric合并");
}
@Override
public TimeSeriesService getServices(String... metric) {
if (metric.length == 1) {
return getService(metric[0]);
}
throw new UnsupportedOperationException("TDengine暂不支持多metric合并");
}
@Override
public TimeSeriesService getService(String metric) {
String tableName = metric;
TdengineTimeSeriesService.ConnectionFactory factory = new TdengineTimeSeriesService.ConnectionFactory() {
@Override
public Connection getConnection() throws SQLException {
return TdengineTimeSeriesManager.this.getConnection();
}
};
return new TdengineTimeSeriesService(tableName, factory, TD_DB, map,hikariDs);
}
/**
* 创建数据库
* @throws SQLException
*/
private void createDatabase() throws SQLException {
if(db_init){
return;
}
HikariDataSource hikari = new HikariDataSource();
hikari.setJdbcUrl(TD_URL);
hikari.setUsername(TD_USER);
hikari.setPassword(TD_PWD);
hikari.setDriverClassName("com.taosdata.jdbc.rs.RestfulDriver");
hikari.setMaximumPoolSize(20);
Connection conn=hikari.getConnection();
String createSql = String.format(
"CREATE DATABASE IF NOT EXISTS %s " +
"KEEP 365 " + // 数据保留365天
"CACHEMODEL 'none' " + // 缓存模式
"WAL_LEVEL 1", // WAL级别
TD_DB
);
try (Statement stmt = conn.createStatement()) {
stmt.executeUpdate(createSql);
}
try (Statement stmt = conn.createStatement()) {
stmt.executeUpdate("use "+TD_DB);
}
db_init=true;
hikari.close();
}
@Override
public Mono<Void> registerMetadata(TimeSeriesMetadata metadata) {
try {
createDatabase();
} catch (SQLException e) {
throw new RuntimeException(e);
}
// 1. 前置校验(强化)
if (metadata == null || metadata.getMetric() == null || metadata.getMetric().getId() == null) {
return Mono.error(new IllegalArgumentException("metadata/metric/metric.id 不能为空"));
}
String tableName = metadata.getMetric().getId().trim();
// 过滤表名特殊字符(避免语法错误)
tableName =getSafeTableName(tableName);
List<PropertyMetadata> properties = metadata.getProperties() == null ? new ArrayList<>() : metadata.getProperties();
// 2. 字段分类(严格按规则)
List<String> dataColumnList = new ArrayList<>(); // 数据列(带类型,如 "provider VARCHAR(255)")
List<String> tagColumnList = new ArrayList<>(); // TAG列(带类型,如 "`id` VARCHAR(255)")
Map<String, String> finalTagMap = new HashMap<>();// TAG映射
// 第一步:筛选JSON字段
List<PropertyMetadata> jsonFields = new ArrayList<>();
for (PropertyMetadata prop : properties) {
String fieldId = escapeIfKeyword(prop.getId());
if (fieldId == null) continue;
String tdType = mapToTDengineType(prop.getValueType(), fieldId);
if ("JSON".equals(tdType)) {
jsonFields.add(prop);
}
}
boolean hasJson = !jsonFields.isEmpty();
// 第二步:构建数据列/TAG列(用数组+join,避免空格错误)
if (hasJson) {
// 有JSON:所有非JSON字段(含id/Id)为数据列,JSON合并为1个TAG
for (PropertyMetadata prop : properties) {
String fieldId = escapeIfKeyword(prop.getId());
if (fieldId == null) continue;
String tdType = mapToTDengineType(prop.getValueType(), fieldId);
if ("JSON".equals(tdType)) continue;
// 数据列:字段名转义 + 类型,强制空格
dataColumnList.add(escapeIfKeyword(fieldId) + " " + tdType);
}
// JSON TAG:合并字段名
String jsonTagName = jsonFields.stream()
.map(p -> escapeIfKeyword(p.getId()))
.filter(Objects::nonNull)
.reduce((a, b) -> a + "_" + b)
.orElse("json_meta");
jsonTagName = escapeIfKeyword(jsonTagName);
tagColumnList.add(jsonTagName + " JSON");
finalTagMap.put(jsonTagName, jsonTagName);
} else {
// 无JSON:id/Id为TAG,其余为数据列
for (PropertyMetadata prop : properties) {
String fieldId = escapeIfKeyword(prop.getId());
if (fieldId == null) continue;
String tdType = mapToTDengineType(prop.getValueType(), fieldId);
boolean isIdField = (fieldId.endsWith("id") || fieldId.endsWith("Id"))&&!tagNo(fieldId.toLowerCase());
if (isIdField) {
// TAG列:转义 + 类型,强制空格
String tagColumn = escapeIfKeyword(fieldId) + " " + tdType;
tagColumnList.add(tagColumn);
finalTagMap.put(fieldId, escapeIfKeyword(fieldId));
} else {
// 数据列:转义 + 类型,强制空格
dataColumnList.add(escapeIfKeyword(fieldId) + " " + tdType);
}
}
// 无JSON无id/Id:默认TAG
if (tagColumnList.isEmpty()) {
tagColumnList.add("id BIGINT");
finalTagMap.put("id", "id");
}
}
// 3. 构建最终SQL(强制语法规范)
// 数据列:固定ts TIMESTAMP + 动态列,用", "连接(逗号+空格)
List<String> finalDataColumns = new ArrayList<>();
finalDataColumns.add("ts TIMESTAMP"); // 基础时间列
finalDataColumns.addAll(dataColumnList);
String dataColumnsStr = String.join(", ", finalDataColumns);
// TAG列:用", "连接
String tagColumnsStr = String.join(", ", tagColumnList);
// 最终SQL:强制关键字前后空格,避免拼接错误
String createSql = String.format(
"CREATE STABLE IF NOT EXISTS %s.%s (%s) TAGS (%s);",
TD_DB,
tableName,
dataColumnsStr,
tagColumnsStr
);
// 4. 执行SQL(响应式+资源关闭+日志)
String finalTableName = tableName;
return Mono.fromRunnable(() -> {
try (Connection conn = getConnection();
Statement stmt = conn.createStatement()) {
// 打印格式化后的SQL,便于排查
System.out.println("=== 格式化后的SQL ===");
System.out.println(createSql);
System.out.println("====================");
stmt.execute(createSql);
map.put(TD_DB + "." + metadata, finalTagMap);
System.out.println("超级表 " + finalTableName + " 创建成功");
} catch (SQLException e) {
throw new RuntimeException(
"创建超级表失败\nSQL:" + createSql + "\n错误:" + e.getMessage(),
e
);
}
})
.then(Mono.empty());
}
}
服务类:
java
package cn.iot.things.tdengine.timeseries;
import com.alibaba.fastjson.JSON;
import com.zaxxer.hikari.HikariDataSource;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.hswebframework.ezorm.core.param.QueryParam;
import org.hswebframework.web.api.crud.entity.PagerResult;
import org.jetlinks.community.timeseries.TimeSeriesData;
import org.jetlinks.community.timeseries.TimeSeriesService;
import org.jetlinks.community.timeseries.query.*;
import org.jetlinks.community.timeseries.utils.TimeSeriesUtils;
import org.jetlinks.core.metadata.types.*;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;
import java.sql.*;
import java.util.*;
import java.util.Date;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import static cn.iot.things.tdengine.timeseries.TDengineHelper.*;
/**
*/
@Slf4j
public class TdengineTimeSeriesService implements TimeSeriesService {
private static final String TD_DB ="jetlinks_db" ;
// ========== 核心配置(一次配置,终身受用) ==========
private final String metric; // 超级表名
private final ConnectionFactory connFactory;// 连接工厂
private final String dbName; // TDengine 数据库名
private final Map<String, CacheValue<Boolean>> tableCache = new ConcurrentHashMap<>(); // 子表存在性缓存(避免重复查)
private static final int TD_TABLE_NAME_MAX_LEN = 64; // TDengine 3.7.8 表名最大长度
private static final String TD_TABLE_NAME_REGEX = "[^a-zA-Z0-9_]"; // 仅允许字母/数字/下划线
private static final long CACHE_EXPIRE_SECONDS = 300; // 表缓存过期时间(5分钟)
private static final ConcurrentHashMap<String,String> mapCheck=new ConcurrentHashMap<>();//检查古过的表
private static final Map<String,BatchProcessor> mapBatch=new ConcurrentHashMap<>();
private static final Map<String,Integer> mapNUm=new ConcurrentHashMap<>();
private final HikariDataSource hikariDs;
private static class CacheValue<T> {
T value;
long expireTime;
CacheValue(T value) {
this.value = value;
this.expireTime = System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(CACHE_EXPIRE_SECONDS);
}
boolean isExpired() {
return System.currentTimeMillis() > expireTime;
}
}
private final ConcurrentHashMap<String,Map<String,String>> map;
@FunctionalInterface
public interface ConnectionFactory {
Connection getConnection() throws SQLException;
}
/**
* 场景1:系统表(无标签列)
* @param metric 超级表名
* @param connFactory 连接工厂
* @param dbName 数据库名
*/
public TdengineTimeSeriesService(String metric, ConnectionFactory connFactory, String dbName, ConcurrentHashMap<String, Map<String, String>> map, HikariDataSource hikariDs) {
this(metric, connFactory, null, false, "", dbName, hikariDs, map);
}
/**
* 全量自定义(通用场景)
*/
public TdengineTimeSeriesService(String metric, ConnectionFactory connFactory, String tagColumn, boolean requireTag, String defaultTagValue, String dbName, HikariDataSource hikariDs, ConcurrentHashMap<String, Map<String, String>> map) {
this.metric = getSafeTableName(metric);
this.connFactory = connFactory;
this.dbName = dbName;
this.hikariDs = hikariDs;
this.map = map;
}
@Override
public Mono<Void> commit(Publisher<TimeSeriesData> data) {
return Flux.from(data)
.flatMap(this::commitSingle, 1) // 单线程串行入库,避免并发问题
.retry(1) // 仅重试1次,避免死循环
.then()
.onErrorResume(e -> {
log.error("批量单条入库失败(超级表:{})", metric, e);
return Mono.empty();
});
}
@Override
public Mono<Void> commit(TimeSeriesData data) {
return commitSingle(data);
}
private Mono<Void> commitSingle(TimeSeriesData data) {
if (data == null || data.getData() == null) {
log.warn("空数据跳过入库(超级表:{})", metric);
return Mono.empty();
}
check(data);
if(mapBatch.containsKey(metric)){
BatchProcessor processor= mapBatch.get(metric);
processor.submit(data);
return Mono.empty();
}
else {
int num= mapNUm.getOrDefault(metric,1);
num++;
mapNUm.put(metric,num);
if(num>20){
BatchProcessor processor= new BatchProcessor(metric,20,3,map.get(metric),hikariDs);
mapBatch.put(metric,processor);
processor.submit(data);
return Mono.empty();
}
}
// 串行执行:避免并发抢占连接池
return Mono.fromRunnable(() -> {
// Connection 用 try-with-resources 自动释放
try (Connection conn = connFactory.getConnection()) {
// 禁用自动提交(可选,提升性能)
conn.setAutoCommit(true);
insertData(conn, data);
} catch (SQLException e) {
try {
Connection conn = connFactory.getConnection();
checkAddColumn(data,conn);
conn.close();
} catch (SQLException ex) {
throw new RuntimeException(ex);
}
throw new RuntimeException("TDengine 入库失败", e);
}
})
.subscribeOn(Schedulers.boundedElastic())
// 异常时释放资源,避免连接泄漏
.doOnError(e -> log.error("入库异常,资源已释放", e))
.then()
.onErrorResume(e -> {
log.error("入库失败(时间戳:{})", data.getTimestamp(), e);
return Mono.empty();
});
}
/**
* 检测表是否存在
* @return
* @throws SQLException
*/
private boolean checkSuperTableExists() throws SQLException {
String checkSql = String.format("SHOW %s.TABLES", dbName);
try (Connection conn = connFactory.getConnection();
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(checkSql)) {
while (rs.next()) {
String tableName = rs.getString("table_name");
if (tableName == null) {
tableName = rs.getString(1); // 尝试第一列
}
if (metric.equals(tableName)) {
return true;
}
}
return false;
}
}
/**
* 管理安全表
* @param tableName
* @param data
* @return
*/
private String getTdTableNameRegex(String tableName,TimeSeriesData data) {
if(map.containsKey(tableName)){
Map<String,String> coloumns= map.get(tableName);
StringBuilder builder = new StringBuilder();
builder.append("ts");
for (String col : coloumns.keySet()) {
//查找一下
if(data.getData().containsKey(col)){
builder.append("_").append(data.getData().get(col));
}
else {
Map<String,Object> objectMap=data.getData();
for (String key : objectMap.keySet()) {
if(key.toLowerCase().equals(col.toLowerCase())){
builder.append("_").append(objectMap.get(key));
}
}
}
}
// 仅保留字母、数字、下划线,其余替换为下划线
String child=builder.toString();
String cleanName = child.trim().replaceAll("[^a-zA-Z0-9_]", "_");
// 确保以字母/下划线开头(避免数字开头)
if (!cleanName.isEmpty() && Character.isDigit(cleanName.charAt(0))) {
cleanName = "t_" + cleanName;
}
return cleanName;
}
return "db001";
}
/**
* 修改添加列
* @param data
* @param con
*/
private void checkAddColumn(TimeSeriesData data,Connection con) {
if(mapCheck.containsKey(metric)){
return;
}
//同时查询结构
String sqlTag="DESCRIBE jetlinks_db."+metric;
try {
Statement stm = con.createStatement();
ResultSet resultSet = null;
resultSet = stm.executeQuery(sqlTag);
HashMap<String,String> tmp=new HashMap<>();
while (resultSet.next()) {
String column = resultSet.getString("field");
tmp.put(column.toLowerCase(), column);
}
Map<String,Object> map= data.getData();
Map<String,Object> coloumns=new HashMap<>();
for(Map.Entry<String, Object> entry:map.entrySet()) {
if(!tmp.containsKey(entry.getKey().toLowerCase())) {
coloumns.put(entry.getKey(), entry.getValue());
}
}
//
if(!coloumns.isEmpty()){
//有需要添加的列
StringBuilder builder = new StringBuilder();
builder.append("ALTER STABLE jetlinks_db."+metric);
builder.append(" ADD COLUMN ");
for(Map.Entry<String, Object> entry:coloumns.entrySet()) {
builder.append(entry.getKey()).append(" ").append(mapClassToTDengineType(entry.getValue().getClass().getName())).append(",");
}
builder.deleteCharAt(builder.length()-1);
stm= con.createStatement();
stm.executeUpdate(builder.toString());
}
mapCheck.put(metric,"");
stm.close();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
/**
* 检测超级表存在否则创建
* @param data
*/
private void check(TimeSeriesData data) {
String name = metric;
if (map.containsKey(name)) {
return;
}
String sql = "SHOW jetlinks_db.STABLES LIKE '"+name+"'";
try {
Connection con = connFactory.getConnection();
Statement stm = null;
stm = con.createStatement();
ResultSet resultSet = stm.executeQuery(sql);
boolean exists = resultSet.next();
if (exists) {
//同时查询结构
String sqlTag="DESCRIBE jetlinks_db."+name;
stm=con.createStatement();
resultSet= stm.executeQuery(sqlTag);
HashMap<String,String> tmp=new HashMap<>();
while (resultSet.next()) {
String column= resultSet.getString("field");
String value= resultSet.getString("note");
if("TAG".equals(value)){
tmp.put(column,column);
}
}
map.put(name, tmp);
} else {
//创建表
createTable(data,con);
}
stm.close();
con.close();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
/**
* 创建表
* @param data
* @param connection
* @throws SQLException
*/
private void createTable(TimeSeriesData data, Connection connection) throws SQLException {
// 1. 前置校验:避免空指针和无效参数
if (data == null || data.getData() == null || connection == null) {
throw new IllegalArgumentException("参数不能为空:data/connection/data.getData()");
}
if (metric == null || metric.trim().isEmpty()) {
throw new IllegalArgumentException("表名(metric)不能为空");
}
System.out.println("正在创建," + metric + "," + data.getData().size());
String safeTableName = metric.trim();
Map<String, Object> properties = data.getData();
StringBuilder builder = new StringBuilder();
// 2. 初始化创建语句:规范空格,避免语法错误
builder.append("CREATE STABLE IF NOT EXISTS ")
.append(TD_DB).append(".").append(safeTableName)
.append(" (ts TIMESTAMP"); // 先加ts,后续用逗号拼接,避免末尾逗号问题
// 3. 分离字段:普通字段/ID类Tag/JSON类Tag(待合并)
Map<String, Object> idTag = new HashMap<>(); // id/Id结尾的Tag(非JSON)
Map<String, Object> jsonTagFields = new HashMap<>(); // 待合并的JSON Tag字段
Map<String, String> finalTagMap = new HashMap<>(); // 最终Tag名(转义后)
for (Map.Entry<String, Object> property : properties.entrySet()) {
String fieldName = property.getKey();
Object fieldValue = property.getValue();
// 空值处理:默认按VARCHAR(512)映射为普通字段
if (fieldValue == null) {
builder.append(", ").append(escapeIfKeyword(fieldName)).append(" VARCHAR(512)");
continue;
}
// 3.1 识别ID类Tag(id/Id结尾,非JSON)
if ((fieldName.endsWith("id") || fieldName.endsWith("Id"))&&tagNo(fieldName.toLowerCase())) {
idTag.put(fieldName, fieldValue);
continue;
}
// 3.2 识别JSON类型字段(需合并为一个Tag)
String typeName = getSimpleClassName(fieldValue.getClass());
String tdType = mapClassToTDengineType(typeName);
if ("JSON".equals(tdType)) {
jsonTagFields.put(fieldName, fieldValue); // 暂存,后续合并
continue;
}
// 3.3 普通字段:拼接到STABLE列定义
builder.append(", ").append(escapeIfKeyword(fieldName)).append(" ").append(tdType);
}
// 3.4 处理ID类Tag:拼接到普通字段后(作为列,非Tag)
if (!idTag.isEmpty()) {
for (Map.Entry<String, Object> entry : idTag.entrySet()) {
String fieldName = entry.getKey();
Object fieldValue = entry.getValue();
String typeName = fieldValue == null ? "String" : getSimpleClassName(fieldValue.getClass());
String tdType = mapClassToTDengineType(typeName);
builder.append(", ").append(escapeIfKeyword(fieldName)).append(" ").append(tdType);
}
}
// 4. 闭合列定义,开始拼接Tag(核心:合并JSON Tag)
builder.append(") TAGS (");
// 4.1 合并多个JSON Tag字段为一个
String mergedJsonTagName = "";
String mergedJsonTagValue = "{}"; // 默认空JSON
if (!jsonTagFields.isEmpty()) {
// 拼接Tag名称:字段名用下划线连接(如 "field1_field2")
mergedJsonTagName = String.join("_", jsonTagFields.keySet());
// 转义Tag名称(避免关键字冲突)
mergedJsonTagName = escapeIfKeyword(mergedJsonTagName);
// 合并字段值为JSON字符串
mergedJsonTagValue = JSON.toJSONString(jsonTagFields);
// 拼接到Tag定义:仅一个JSON Tag
builder.append(mergedJsonTagName).append(" JSON");
// 记录最终Tag名
finalTagMap.put(mergedJsonTagName, mergedJsonTagName);
}
// 4.2 补充ID类Tag(若有,拼接到JSON Tag后)
if (!idTag.isEmpty() && !jsonTagFields.isEmpty()) {
builder.append(", "); // 有JSON Tag时加逗号分隔
}
for (Map.Entry<String, Object> entry : idTag.entrySet()) {
String fieldName = entry.getKey();
Object fieldValue = entry.getValue();
String typeName = fieldValue == null ? "String" : getSimpleClassName(fieldValue.getClass());
String tdType = mapClassToTDengineType(typeName);
String escapedName = escapeIfKeyword(fieldName);
builder.append(escapedName).append(" ").append(tdType);
finalTagMap.put(fieldName, escapedName);
// 多个ID Tag时加逗号(最后一个手动移除)
if (entry != idTag.entrySet().toArray()[idTag.size() - 1]) {
builder.append(", ");
}
}
// 4.3 无任何Tag时:默认添加id BIGINT(兼容空Tag场景)
if (jsonTagFields.isEmpty() && idTag.isEmpty()) {
builder.append("id BIGINT");
finalTagMap.put("id", "id");
}
// 5. 闭合Tag定义,补全语句
builder.append(");");
// 6. 执行SQL:自动关闭资源,避免泄漏
try (Statement stmt = connection.createStatement()) {
System.out.println("执行创建表SQL:" + builder.toString());
stmt.execute(builder.toString());
// 记录表名与Tag映射
map.put(TD_DB + "." + safeTableName, finalTagMap);
} catch (SQLException e) {
System.err.println("创建表失败,SQL:" + builder.toString());
System.err.println("合并后的JSON Tag名称:" + mergedJsonTagName);
System.err.println("合并后的JSON Tag值:" + mergedJsonTagValue);
throw new SQLException("创建TDengine超级表失败:" + e.getMessage(), e);
}
}
/**
* 入库
* @param conn
* @param data
* @throws SQLException
*/
private void insertData(Connection conn, TimeSeriesData data) throws SQLException {
Map<String, Object> dataMap = data.getData();
Map<String, String> tags = map.get(metric);
dataMap
.put("ts", new Timestamp(data.getTimestamp()));
// 获取配置的超级表名(可以根据metric映射)
String superTable = metric;
// 生成子表名(基于tags或时间戳)
String childTable =getTdTableNameRegex(superTable,data);
// 构建列名和占位符
List<String> columns = new ArrayList<>(dataMap.keySet());
List<String> placeholders = new ArrayList<>();
// 调试:打印所有数据
log.info("=== 插入数据调试信息 ===");
log.info("metric: {}", metric);
log.info("子表名: {}", childTable);
log.info("tags配置: {}", tags);
log.info("dataMap内容: {}", dataMap);
for (int i = 0; i < columns.size(); i++) placeholders.add("?");
List<String> dataColumns = new ArrayList<>();
// 构建标签部分
List<String> tagColumns = new ArrayList<>(tags.values());
if(tagColumns.size()==1){
//判断JSON合并
if(tagColumns.get(0).contains("_")&&!dataMap.containsKey(tagColumns.get(0))&&!dataMap.containsKey(tagColumns.get(0).toLowerCase())){
//JSON
Map<String,Object> tmp=new HashMap<>();
String[] cols=tagColumns.get(0).split("_");
for(int j=0;j<cols.length;j++) {
if (dataMap.containsKey(cols[j])) {
tmp.put(cols[j], dataMap.get(cols[j]));
dataMap.remove(cols[j]);
} else if (dataMap.containsKey(cols[j].toLowerCase())) {
tmp.put(cols[j], dataMap.get(cols[j].toLowerCase()));
dataMap.remove(cols[j]);
}
}
//
dataMap.put(tagColumns.get(0).toLowerCase(),tmp);//直接替换
}
}
List<String> tagPlaceholders = new ArrayList<>();
for (String column : columns) {
// 检查是否是标签字段(注意大小写)
boolean isTag = false;
for (String tagCol : tagColumns) {
if (column.equalsIgnoreCase(tagCol)) {
isTag = true;
break;
}
}
if (!isTag) {
dataColumns.add(column);
}
}
for (int i = 0; i < tagColumns.size(); i++) tagPlaceholders.add("?");
// 构建插入SQL:使用USING子句自动建表
String insertSql = buildInsertSql(dbName, childTable, superTable,
dataColumns
, tagColumns, tagPlaceholders);
log.info("生成的SQL: {}", insertSql);
// log.info("预计参数数量: {} (1子表 + {}标签 + {}字段)", totalParams, tagColumns.size(), columns.size());
try (PreparedStatement pstmt = conn.prepareStatement(insertSql)) {
int idx = 1;
// 1. 设置子表名
pstmt
.setString(idx++, childTable);
// 2. 设置标签值
for (String tagCol : tagColumns) {
Object tagVal =dataMap.get(tagCol.replaceAll("'",""));
setPreparedStatementValue(pstmt, idx++, tagVal);
}
// 3. 设置数据字段值
for (String col : dataColumns) {
Object val = dataMap.get(col);
setPreparedStatementValue(pstmt, idx++, val);
}
pstmt
.executeUpdate();
}
}
/**
* 构建插入SQL语句
*/
private String buildInsertSql(String dbName, String childTable, String superTable,
List<String> columns, List<String> tagColumns,
List<String> tagPlaceholders) {
StringBuilder sql = new StringBuilder();
// 基本格式:INSERT INTO 子表名 USING 超级表名 TAGS (标签值) (字段列) VALUES (字段值)
sql.append("INSERT INTO ? USING ")
.append(dbName).append(".").append(superTable)
.append(" TAGS (");
// 添加标签占位符
sql
.append(String.join(",", tagPlaceholders))
.append(") (")
.append(String.join(",", columns))
.append(") VALUES (")
.append(String.join(",", Collections.nCopies(columns.size(), "?")))
.append(")");
return sql.toString();
}
/**
* 设置PreparedStatement的值(通用方法)
*/
private void setPreparedStatementValue(PreparedStatement pstmt, int index, Object value)
throws SQLException {
if (value == null) {
pstmt.setNull(index, Types.VARCHAR);
} else if (value instanceof Integer) {
pstmt.setInt(index, (Integer) value);
} else if (value instanceof Long) {
pstmt.setLong(index, (Long) value);
} else if (value instanceof Double) {
pstmt.setDouble(index, (Double) value);
} else if (value instanceof Float) {
pstmt.setFloat(index, (Float) value);
} else if (value instanceof Boolean) {
pstmt.setBoolean(index, (Boolean) value);
} else if (value instanceof Timestamp) {
pstmt.setTimestamp(index, (Timestamp) value);
} else if (value instanceof Date) {
java.sql.Date date=new java.sql.Date(((Date)value).getTime());
pstmt.setDate(index, date);
} else if (value instanceof byte[]) {
pstmt.setBytes(index, (byte[]) value);
} else if (value instanceof String) {
String str = (String) value;
if(str.length() > 512){
//截断
str = str.substring(0, 512 - 3) + "...";
}
pstmt.setString(index, str);
} else {
// 其他类型转为JSON字符串
try {
pstmt.setString(index, JSON.toJSONString(value));
} catch (Exception e) {
pstmt.setString(index, value.toString());
}
}
}
@Override
public Flux<TimeSeriesData> query(QueryParam queryParam) {
return Mono.fromCallable(() -> checkSuperTableExists())
.flatMapMany(exists -> {
if (!exists) return Flux.empty();
return buildQuerySql(queryParam)
.flatMapMany(sql -> executeQuery(sql));
})
.onErrorResume(e -> {
log.error("查询失败(超级表:{})", metric, e);
return Flux.empty();
});
}
@Override
public Flux<TimeSeriesData> multiQuery(Collection<QueryParam> queries) {
return Flux.fromIterable(queries).flatMap(this::query, 4);
}
@Override
public Mono<Integer> count(QueryParam queryParam) {
return Mono.fromCallable(() -> checkSuperTableExists())
.flatMap(exists -> {
if (!exists) return Mono.just(0);
return buildCountSql(queryParam)
.flatMap(this::executeCount);
})
.onErrorReturn(0);
}
// 补全 buildCountSql 方法(放在类内,与 buildQuerySql 方法同级)
private Mono<QuerySql> buildCountSql(QueryParam queryParam) {
return Mono.fromCallable(() -> {
// 构建 TDengine 3.7.8 兼容的 COUNT 查询 SQL
StringBuilder sql = new StringBuilder("SELECT COUNT(1) FROM ").append(dbName).append(".").append(metric);
List<String> conditions = new ArrayList<>();
List<Object> params = new ArrayList<>();
// 处理查询条件(与查询逻辑一致,保证条件过滤后计数准确)
if (queryParam.getTerms() != null) {
queryParam.getTerms().forEach(term -> {
if (term.getColumn() != null && term.getValue() != null) {
String col = term.getColumn();
Object val = term.getValue();
if ("ts".equals(col) || "timestamp".equals(col)) {
if (val instanceof Object[]) {
Object[] range = (Object[]) val;
conditions.add("ts >= ? AND ts <= ?");
params.add(range[0]);
params.add(range[1]);
} else {
conditions.add("ts = ?");
params.add(val);
}
} else {
conditions.add(col + " = ?");
params.add(val);
}
}
});
}
// 拼接 WHERE 条件
if (!conditions.isEmpty()) {
sql.append(" WHERE ").append(String.join(" AND ", conditions));
}
// 返回封装后的 SQL + 参数
return new QuerySql(sql.toString(), params);
}).subscribeOn(Schedulers.boundedElastic()); // 与其他 SQL 构建方法保持一致的线程调度
}
@Override
public Flux<AggregationData> aggregation(AggregationQueryParam param) {
return Mono.fromCallable(() -> checkSuperTableExists())
.flatMapMany(exists -> {
if (!exists) return Flux.empty();
ProcessedAggregation processed = preProcessAggregation(param);
return buildAggregationSql(param, processed.getDbColumns())
.flatMapMany(sql -> executeAggregation(sql, param, processed));
})
.onErrorResume(e -> {
log.error("聚合失败(超级表:{})", metric, e);
return Flux.empty();
});
}
@Override
public Mono<PagerResult<TimeSeriesData>> queryPager(QueryParam queryParam) {
return TimeSeriesService.super.queryPager(queryParam);
}
@Override
public Mono<Void> save(Publisher<TimeSeriesData> data) {
return commit(data);
}
private Mono<QuerySql> buildQuerySql(QueryParam param) {
return Mono.fromCallable(() -> {
StringBuilder sql = new StringBuilder("SELECT * FROM ").append(dbName).append(".").append(metric);
List<String> conditions = new ArrayList<>();
List<Object> params = new ArrayList<>();
// 时间条件(3.7.8 时序查询核心)
if (param.getTerms() != null) {
param.getTerms().forEach(term -> {
if (term.getColumn() != null && term.getValue() != null) {
String col = term.getColumn();
Object val = term.getValue();
if ("ts".equals(col) || "timestamp".equals(col)) {
if (val instanceof Object[]) {
Object[] range = (Object[]) val;
conditions.add("ts >= ? AND ts <= ?");
params.add(range[0]);
params.add(range[1]);
} else {
conditions.add("ts = ?");
params.add(val);
}
} else {
conditions.add(col + " = ?");
params.add(val);
}
}
});
}
if (!conditions.isEmpty()) {
sql.append(" WHERE ").append(String.join(" AND ", conditions));
}
// 排序/分页(3.7.8 兼容)
if (param.getSorts() != null && !param.getSorts().isEmpty()) {
sql.append(" ORDER BY ")
.append(param.getSorts().stream()
.map(s -> s.getName() + " " + s.getOrder())
.collect(Collectors.joining(", ")));
}
if (param.isPaging()) {
int offset = (param.getPageIndex() - 1) * param.getPageSize();
sql.append(" LIMIT ? OFFSET ?");
params.add(param.getPageSize());
params.add(offset);
}
return new QuerySql(sql.toString(), params);
});
}
/**
* 执行查询并转换为 TimeSeriesData 流(适配 TDengine 3.7.8 + 无编译/运行时异常)
* @param querySql 封装的查询SQL+参数
* @return TimeSeriesData 数据流
*/
private Flux<TimeSeriesData> executeQuery(QuerySql querySql) {
// 1. Flux.using 管理连接生命周期:创建-使用-释放
return Flux.using(
// 资源提供者:获取数据库连接(处理 SQLException 为运行时异常)
() -> {
try {
Connection conn = connFactory.getConnection();
log.debug("获取数据库连接成功,准备执行查询:{}", querySql.getSql());
return conn;
} catch (SQLException e) {
// 核心修正:检查型异常包装为运行时异常,适配 Supplier 接口
throw new RuntimeException("获取查询连接失败", e);
}
},
// 资源使用者:执行查询并转换结果
(Connection conn) -> {
try (PreparedStatement pstmt = conn.prepareStatement(querySql.getSql())) {
// 设置查询参数(处理参数绑定异常)
setParams(pstmt, querySql.getParams());
try (ResultSet rs = pstmt.executeQuery()) {
ResultSetMetaData meta = rs.getMetaData();
int colCount = meta.getColumnCount();
// 异步生成 TimeSeriesData 流
return Flux.<TimeSeriesData>create(sink -> {
// 防止 sink 重复触发
if (sink.isCancelled()) {
log.debug("查询结果流已取消,停止读取数据");
return;
}
try {
while (rs.next() && !sink.isCancelled()) {
long ts = 0;
Map<String, Object> data = new HashMap<>(colCount); // 初始化容量提升性能
for (int i = 1; i <= colCount; i++) {
String colName = meta.getColumnName(i);
Object colValue = rs.getObject(colName);
// 核心修正:处理 ts 列空值,避免 NPE
if ("ts".equalsIgnoreCase(colName)) { // 兼容大小写(TDengine 不敏感)
if (colValue != null) {
ts = rs.getTimestamp(colName).getTime();
} else {
log.warn("查询结果中 ts 列为空,默认赋值 0(列索引:{})", i);
ts = 0;
}
} else {
// 空值处理:避免 Map 中存入 null
data.put(colName, colValue == null ? "" : colValue);
}
}
// 发送数据到流
sink.next(TimeSeriesData.of(ts, data));
}
// 流完成:无更多数据
sink.complete();
log.debug("查询结果读取完成,共处理 {} 列数据", colCount);
} catch (SQLException e) {
// 异步异常传递:通过 sink.error 抛出,上层可捕获
log.error("读取查询结果集失败", e);
sink.error(new RuntimeException("读取查询数据失败", e));
}
})
// 流取消时的资源清理
.doOnCancel(() -> {
log.debug("查询结果流取消,清理结果集资源");
try {
rs.close();
} catch (SQLException e) {
log.warn("取消时关闭结果集失败", e);
}
});
} catch (SQLException e) {
log.error("执行查询SQL失败:{}", querySql.getSql(), e);
return Flux.<TimeSeriesData>error(new RuntimeException("执行查询失败", e));
}
} catch (SQLException e) {
log.error("创建查询Statement失败:{}", querySql.getSql(), e);
return Flux.<TimeSeriesData>error(new RuntimeException("创建查询Statement失败", e));
}
},
// 资源释放器:关闭连接(兜底,即使上面抛异常也会执行)
(Connection conn) -> {
try {
if (!conn.isClosed()) {
conn.close();
log.debug("查询连接已关闭");
}
} catch (SQLException e) {
log.warn("关闭查询连接失败", e);
}
}
).subscribeOn(Schedulers.boundedElastic()); // 绑定到弹性线程池,避免阻塞事件循环
}
private Mono<Integer> executeCount(QuerySql querySql) {
return Mono.fromCallable(() -> {
try (Connection conn = connFactory.getConnection();
PreparedStatement pstmt = conn.prepareStatement(querySql.sql)) {
setParams(pstmt, querySql.params);
try (ResultSet rs = pstmt.executeQuery()) {
return rs.next() ? rs.getInt(1) : 0;
}
}
}).subscribeOn(Schedulers.boundedElastic());
}
private ProcessedAggregation preProcessAggregation(AggregationQueryParam param) {
ProcessedAggregation result = new ProcessedAggregation();
for (AggregationColumn col : param.getAggColumns()) {
String statType = extractStatType(col);
if (statType != null) {
String alias = col.getAlias();
result.dbColumns.add(new AggregationColumn("COUNT(" + col.getProperty() + ")", "cnt_" + alias, Aggregation.COUNT));
result.dbColumns.add(new AggregationColumn("AVG(" + col.getProperty() + ")", "avg_" + alias, Aggregation.AVG));
result.dbColumns.add(new AggregationColumn(col.getProperty(), "sum2_" + alias, Aggregation.SUM));
ProcessedAggregation.StatisticalColumn statCol = new ProcessedAggregation.StatisticalColumn();
statCol.originalAlias = alias;
statCol.property = col.getProperty();
statCol.statType = statType;
statCol.countAlias = "cnt_" + alias;
statCol.avgAlias = "avg_" + alias;
statCol.sum2Alias = "sum2_" + alias;
result.statisticalColumns.put(alias, statCol);
} else {
result.dbColumns.add(col);
result.normalColumns.add(col);
}
}
return result;
}
private String extractStatType(AggregationColumn column) {
try {
String alias = column.getAlias().toUpperCase();
if (alias.contains("STDDEV")) {
return alias.contains("POP") ? "STDDEV_POP" : "STDDEV";
}
if (alias.contains("VARIANCE")) {
return alias.contains("POP") ? "VARIANCE_POP" : "VARIANCE";
}
} catch (Exception e) {
log.debug("提取统计类型失败", e);
}
return null;
}
private Mono<QuerySql> buildAggregationSql(AggregationQueryParam param, List<AggregationColumn> cols) {
return Mono.fromCallable(() -> {
TimeSeriesUtils.prepareAggregationQueryParam(param);
StringBuilder sql = new StringBuilder("SELECT ");
List<Object> params = new ArrayList<>();
// 聚合列(3.7.8 支持的聚合函数)
List<String> selectItems = Collections.singletonList(cols.stream()
.map(col -> convertAggFunc(col.getAggregation()) + "(" + col.getProperty() + ") AS " + col.getAlias())
.collect(Collectors.joining(", ")));
sql.append(selectItems);
// 超级表 + 时间条件
sql.append(" FROM ").append(dbName).append(".").append(metric);
sql.append(" WHERE ts >= ? AND ts <= ?");
params.add(param.getStartWithTime());
params.add(param.getEndWithTime());
// 分组(3.7.8 时间窗口聚合)
List<String> groupBy = new ArrayList<>();
for (Group g : param.getGroups()) {
if (g instanceof TimeGroup) {
String timeExpr = getTimeWindowExpr(((TimeGroup) g).getInterval().toMillis());
groupBy.add(timeExpr);
sql.append(", ").append(timeExpr).append(" AS ").append(g.getAlias());
} else {
groupBy.add(g.getProperty());
}
}
if (!groupBy.isEmpty()) {
sql.append(" GROUP BY ").append(String.join(", ", groupBy));
}
return new QuerySql(sql.toString(), params);
});
}
/**
* 执行聚合查询并转换为 AggregationData 流(修复所有编译/运行时问题)
* @param querySql 封装的聚合查询SQL+参数
* @param param 聚合查询参数
* @param processed 预处理后的聚合列配置
* @return AggregationData 数据流
*/
private Flux<AggregationData> executeAggregation(QuerySql querySql, AggregationQueryParam param, ProcessedAggregation processed) {
// Flux.using 核心:管理数据库连接的生命周期(创建-使用-释放)
return Flux.using(
// 1. 资源提供者:获取数据库连接(处理检查型异常)
() -> {
try {
Connection conn = connFactory.getConnection();
log.debug("获取聚合查询连接成功,SQL:{}", querySql.getSql());
return conn;
} catch (SQLException e) {
// 关键修正:检查型异常包装为运行时异常,适配 Supplier 接口
throw new RuntimeException("获取聚合查询连接失败", e);
}
},
// 2. 资源使用者:执行聚合查询并转换结果
(Connection conn) -> {
try (PreparedStatement pstmt = conn.prepareStatement(querySql.getSql())) {
// 设置聚合查询参数(处理参数绑定异常)
setParams(pstmt, querySql.getParams());
try (ResultSet rs = pstmt.executeQuery()) {
ResultSetMetaData meta = rs.getMetaData();
int colCount = meta.getColumnCount();
// 异步生成 AggregationData 流
return Flux.<AggregationData>create(sink -> {
// 流取消时立即停止处理,避免无效数据库操作
if (sink.isCancelled()) {
log.debug("聚合查询流已取消,停止读取结果");
return;
}
try {
while (rs.next() && !sink.isCancelled()) {
Map<String, Object> rowData = new HashMap<>(colCount); // 初始化容量提升性能
// 遍历结果集列,处理空值避免 NPE
for (int i = 1; i <= colCount; i++) {
String colName = meta.getColumnName(i);
Object colValue = rs.getObject(colName);
// 空值替换:避免 Map 中存入 null 导致后续聚合计算异常
rowData.put(colName, colValue == null ? 0 : colValue);
}
// 处理聚合行数据,发送到流
AggregationData aggData = processAggRow(rowData, param, processed);
sink.next(aggData);
}
// 流完成:无更多聚合数据
sink.complete();
log.debug("聚合查询结果读取完成,共处理 {} 列数据", colCount);
} catch (SQLException e) {
// 异步异常传递:包装为运行时异常,上层可捕获
log.error("读取聚合查询结果集失败", e);
sink.error(new RuntimeException("读取聚合数据失败", e));
}
})
// 流取消时主动清理结果集资源
.doOnCancel(() -> {
log.debug("聚合查询流取消,清理结果集资源");
try {
rs.close();
} catch (SQLException e) {
log.warn("取消时关闭聚合结果集失败", e);
}
});
} catch (SQLException e) {
log.error("执行聚合查询SQL失败:{}", querySql.getSql(), e);
return Flux.<AggregationData>error(new RuntimeException("执行聚合查询失败", e));
}
} catch (SQLException e) {
log.error("创建聚合查询Statement失败:{}", querySql.getSql(), e);
return Flux.<AggregationData>error(new RuntimeException("创建聚合Statement失败", e));
}
},
// 3. 资源释放器:兜底关闭连接(必加!原代码缺失导致语法错误)
(Connection conn) -> {
try {
if (!conn.isClosed()) {
conn.close();
log.debug("聚合查询连接已关闭");
}
} catch (SQLException e) {
log.warn("关闭聚合查询连接失败", e);
}
}
).subscribeOn(Schedulers.boundedElastic()); // 绑定到弹性线程池,避免阻塞
}
private AggregationData processAggRow(Map<String, Object> row, AggregationQueryParam param, ProcessedAggregation processed) {
Map<String, Object> result = new HashMap<>();
// 普通聚合列
processed.normalColumns.forEach(col -> {
Object val = row.get(col.getAlias());
if (val != null) {
result.put(col.getAlias(), convertNumber(val));
}
});
// 统计聚合列(标准差/方差)
processed.statisticalColumns.values().forEach(statCol -> {
Object val = calcStatValue(row, statCol);
if (val != null) {
result.put(statCol.originalAlias, val);
}
});
// 分组列
param.getGroups().forEach(g -> {
Object val = row.get(g.getAlias());
if (val != null) {
result.put(g.getAlias(), val);
}
});
return AggregationData.of(result);
}
private Object calcStatValue(Map<String, Object> row, ProcessedAggregation.StatisticalColumn statCol) {
try {
Number count = (Number) row.get(statCol.countAlias);
Number avg = (Number) row.get(statCol.avgAlias);
Number sum2 = (Number) row.get(statCol.sum2Alias);
if (count == null || avg == null || sum2 == null || count.longValue() <= 1) {
return 0.0;
}
long cnt = count.longValue();
double avgVal = avg.doubleValue();
double sum2Val = sum2.doubleValue();
double sumSq = sum2Val * sum2Val;
double varPop = (sumSq - cnt * avgVal * avgVal) / cnt;
double varSample = (sumSq - cnt * avgVal * avgVal) / (cnt - 1);
return switch (statCol.statType) {
case "STDDEV" -> Math.sqrt(Math.max(varSample, 0));
case "STDDEV_POP" -> Math.sqrt(Math.max(varPop, 0));
case "VARIANCE" -> Math.max(varSample, 0);
case "VARIANCE_POP" -> Math.max(varPop, 0);
default -> null;
};
} catch (Exception e) {
log.warn("计算统计值失败", e);
return null;
}
}
private String convertAggFunc(Aggregation agg) {
return switch (agg) {
case AVG -> "AVG";
case SUM -> "SUM";
case COUNT -> "COUNT";
case MAX -> "MAX";
case MIN -> "MIN";
case FIRST -> "FIRST_VALUE";
case LAST -> "LAST_VALUE";
default -> "AVG";
};
}
private String getTimeWindowExpr(long intervalMs) {
if (intervalMs < 1000) return "INTERVAL(ts, '1s')";
else if (intervalMs < 60000) return "INTERVAL(ts, '" + intervalMs / 1000 + "s')";
else if (intervalMs < 3600000) return "INTERVAL(ts, '" + intervalMs / 60000 + "m')";
else if (intervalMs < 86400000) return "INTERVAL(ts, '" + intervalMs / 3600000 + "h')";
else return "INTERVAL(ts, '" + intervalMs / 86400000 + "d')";
}
private Object convertNumber(Object val) {
if (val instanceof Number num) {
double d = num.doubleValue();
if (d == Math.floor(d) && !Double.isInfinite(d) && !Double.isNaN(d)) {
return d >= Integer.MIN_VALUE && d <= Integer.MAX_VALUE ? num.intValue() : num.longValue();
}
return d;
}
return val;
}
private void setParams(PreparedStatement pstmt, List<Object> params) throws SQLException {
int idx = 1;
for (Object param : params) {
if (param instanceof Integer) pstmt.setInt(idx++, (Integer) param);
else if (param instanceof Long) pstmt.setLong(idx++, (Long) param);
else if (param instanceof Double) pstmt.setDouble(idx++, (Double) param);
else if (param instanceof Timestamp) pstmt.setTimestamp(idx++, (Timestamp) param);
else pstmt.setString(idx++, param == null ? "" : param.toString());
}
}
// ========== 内部类 ==========
@Data
public static class QuerySql {
String sql;
List<Object> params;
public QuerySql(String sql, List<Object> params) {
this.sql = sql;
this.params = params;
}
}
private static class ProcessedAggregation {
List<AggregationColumn> dbColumns = new ArrayList<>();
Map<String, StatisticalColumn> statisticalColumns = new HashMap<>();
List<AggregationColumn> normalColumns = new ArrayList<>();
public List<AggregationColumn> getDbColumns() {
return dbColumns;
}
static class StatisticalColumn {
String originalAlias;
String property;
String statType;
String countAlias;
String avgAlias;
String sum2Alias;
}
}
}
工具类:
java
package cn.iot.things.tdengine.timeseries;
import org.jetlinks.community.timeseries.TimeSeriesData;
import org.jetlinks.core.metadata.DataType;
import org.jetlinks.core.metadata.types.*;
import org.springframework.util.StringUtils;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
public class TDengineHelper {
// 完整的TDengine SQL关键字集合(基于官方文档所有关键字)
private static final Set<String> TAOS_KEYWORDS = new HashSet<>();
private static final String DEFAULT_TABLE_PREFIX = "ts_";
private static final Pattern LEGAL_NAME_PATTERN = Pattern.compile("^[a-zA-Z0-9_]+$");
private static final Set<String> TAGS_NOCOLUMN=new HashSet<>();//不能是tag列
static {
TAGS_NOCOLUMN.add("threadid");
}
static {
// A区关键字
TAOS_KEYWORDS.add("ABORT");
TAOS_KEYWORDS.add("ACCOUNT");
TAOS_KEYWORDS.add("ACCOUNTS");
TAOS_KEYWORDS.add("ADD");
TAOS_KEYWORDS.add("AFTER");
TAOS_KEYWORDS.add("AGGREGATE");
TAOS_KEYWORDS.add("ALIAS");
TAOS_KEYWORDS.add("ALIVE");
TAOS_KEYWORDS.add("ALL");
TAOS_KEYWORDS.add("ALTER");
TAOS_KEYWORDS.add("ANALYZE");
TAOS_KEYWORDS.add("ANODE");
TAOS_KEYWORDS.add("ANODES");
TAOS_KEYWORDS.add("ANOMALY_WINDOW");
TAOS_KEYWORDS.add("ANTI");
TAOS_KEYWORDS.add("APPS");
TAOS_KEYWORDS.add("ARBGROUPS");
TAOS_KEYWORDS.add("ARROW");
TAOS_KEYWORDS.add("AS");
TAOS_KEYWORDS.add("ASC");
TAOS_KEYWORDS.add("ASOF");
TAOS_KEYWORDS.add("ASYNC");
TAOS_KEYWORDS.add("AT_ONCE");
TAOS_KEYWORDS.add("ATTACH");
TAOS_KEYWORDS.add("AUTO");
TAOS_KEYWORDS.add("ASSIGN");
// B区关键字
TAOS_KEYWORDS.add("BALANCE");
TAOS_KEYWORDS.add("BEFORE");
TAOS_KEYWORDS.add("BEGIN");
TAOS_KEYWORDS.add("BETWEEN");
TAOS_KEYWORDS.add("BIGINT");
TAOS_KEYWORDS.add("BIN");
TAOS_KEYWORDS.add("BINARY");
TAOS_KEYWORDS.add("BITAND");
TAOS_KEYWORDS.add("BITOR");
TAOS_KEYWORDS.add("BLOB");
TAOS_KEYWORDS.add("BLOCKS");
TAOS_KEYWORDS.add("BNODE");
TAOS_KEYWORDS.add("BNODES");
TAOS_KEYWORDS.add("BOOL");
TAOS_KEYWORDS.add("BOTH");
TAOS_KEYWORDS.add("BUFFER");
TAOS_KEYWORDS.add("BUFSIZE");
TAOS_KEYWORDS.add("BWLIMIT");
TAOS_KEYWORDS.add("BY");
// C区关键字
TAOS_KEYWORDS.add("CACHE");
TAOS_KEYWORDS.add("CACHEMODEL");
TAOS_KEYWORDS.add("CACHESIZE");
TAOS_KEYWORDS.add("CALC_NOTIFY_ONLY");
TAOS_KEYWORDS.add("CASE");
TAOS_KEYWORDS.add("CAST");
TAOS_KEYWORDS.add("CHANGE");
TAOS_KEYWORDS.add("CHILD");
TAOS_KEYWORDS.add("CLIENT_VERSION");
TAOS_KEYWORDS.add("CLUSTER");
TAOS_KEYWORDS.add("COLON");
TAOS_KEYWORDS.add("COLS");
TAOS_KEYWORDS.add("COLUMN");
TAOS_KEYWORDS.add("COMMA");
TAOS_KEYWORDS.add("COMMENT");
TAOS_KEYWORDS.add("COMP");
TAOS_KEYWORDS.add("COMPACT");
TAOS_KEYWORDS.add("COMPACTS");
TAOS_KEYWORDS.add("COMPACT_INTERVAL");
TAOS_KEYWORDS.add("COMPACT_TIME_OFFSET");
TAOS_KEYWORDS.add("COMPACT_TIME_RANGE");
TAOS_KEYWORDS.add("CONCAT");
TAOS_KEYWORDS.add("CONFLICT");
TAOS_KEYWORDS.add("CONNECTION");
TAOS_KEYWORDS.add("CONNECTIONS");
TAOS_KEYWORDS.add("CONNS");
TAOS_KEYWORDS.add("CONSUMER");
TAOS_KEYWORDS.add("CONSUMERS");
TAOS_KEYWORDS.add("CONTAINS");
TAOS_KEYWORDS.add("CONTINUOUS_WINDOW_CLOSE");
TAOS_KEYWORDS.add("COPY");
TAOS_KEYWORDS.add("COUNT");
TAOS_KEYWORDS.add("COUNT_WINDOW");
TAOS_KEYWORDS.add("CREATE");
TAOS_KEYWORDS.add("CREATEDB");
TAOS_KEYWORDS.add("CURRENT_USER");
TAOS_KEYWORDS.add("SCAN");
TAOS_KEYWORDS.add("SCANS");
// D区关键字
TAOS_KEYWORDS.add("DATABASE");
TAOS_KEYWORDS.add("DATABASES");
TAOS_KEYWORDS.add("DBS");
TAOS_KEYWORDS.add("DECIMAL");
TAOS_KEYWORDS.add("DEFERRED");
TAOS_KEYWORDS.add("DELETE");
TAOS_KEYWORDS.add("DELETE_MARK");
TAOS_KEYWORDS.add("DELETE_OUTPUT_TABLE");
TAOS_KEYWORDS.add("DELETE_RECALC");
TAOS_KEYWORDS.add("DELIMITERS");
TAOS_KEYWORDS.add("DESC");
TAOS_KEYWORDS.add("DESCRIBE");
TAOS_KEYWORDS.add("DETACH");
TAOS_KEYWORDS.add("DISK_INFO");
TAOS_KEYWORDS.add("DISTINCT");
TAOS_KEYWORDS.add("DISTRIBUTED");
TAOS_KEYWORDS.add("DIVIDE");
TAOS_KEYWORDS.add("DNODE");
TAOS_KEYWORDS.add("DNODES");
TAOS_KEYWORDS.add("DOT");
TAOS_KEYWORDS.add("DOUBLE");
TAOS_KEYWORDS.add("DROP");
TAOS_KEYWORDS.add("DURATION");
// E区关键字
TAOS_KEYWORDS.add("EACH");
TAOS_KEYWORDS.add("ELSE");
TAOS_KEYWORDS.add("ENABLE");
TAOS_KEYWORDS.add("ENCRYPT_ALGORITHM");
TAOS_KEYWORDS.add("ENCRYPT_KEY");
TAOS_KEYWORDS.add("ENCRYPTIONS");
TAOS_KEYWORDS.add("END");
TAOS_KEYWORDS.add("EQ");
TAOS_KEYWORDS.add("EVENT_TYPE");
TAOS_KEYWORDS.add("EVENT_WINDOW");
TAOS_KEYWORDS.add("EVERY");
TAOS_KEYWORDS.add("EXCEPT");
TAOS_KEYWORDS.add("EXISTS");
TAOS_KEYWORDS.add("EXPIRED");
TAOS_KEYWORDS.add("EXPIRED_TIME");
TAOS_KEYWORDS.add("EXPLAIN");
// F区关键字
TAOS_KEYWORDS.add("FAIL");
TAOS_KEYWORDS.add("FHIGH");
TAOS_KEYWORDS.add("FILE");
TAOS_KEYWORDS.add("FILL");
TAOS_KEYWORDS.add("FILL_HISTORY");
TAOS_KEYWORDS.add("FILL_HISTORY_FIRST");
TAOS_KEYWORDS.add("FIRST");
TAOS_KEYWORDS.add("FLOAT");
TAOS_KEYWORDS.add("FLOW");
TAOS_KEYWORDS.add("FLUSH");
TAOS_KEYWORDS.add("FOR");
TAOS_KEYWORDS.add("FORCE");
TAOS_KEYWORDS.add("FORCE_OUTPUT");
TAOS_KEYWORDS.add("FORCE_WINDOW_CLOSE");
TAOS_KEYWORDS.add("FROM");
TAOS_KEYWORDS.add("FROWTS");
TAOS_KEYWORDS.add("FULL");
TAOS_KEYWORDS.add("FUNCTION");
TAOS_KEYWORDS.add("FUNCTIONS");
// G区关键字
TAOS_KEYWORDS.add("GE");
TAOS_KEYWORDS.add("GEOMETRY");
TAOS_KEYWORDS.add("GLOB");
TAOS_KEYWORDS.add("GRANT");
TAOS_KEYWORDS.add("GRANTS");
TAOS_KEYWORDS.add("GROUP");
TAOS_KEYWORDS.add("GT");
// H区关键字
TAOS_KEYWORDS.add("HAVING");
TAOS_KEYWORDS.add("HEX");
TAOS_KEYWORDS.add("HOST");
// I区关键字
TAOS_KEYWORDS.add("ID");
TAOS_KEYWORDS.add("IF");
TAOS_KEYWORDS.add("IGNORE");
TAOS_KEYWORDS.add("IGNORE_DISORDER");
TAOS_KEYWORDS.add("IGNORE_NODATA_TRIGGER");
TAOS_KEYWORDS.add("ILLEGAL");
TAOS_KEYWORDS.add("IMMEDIATE");
TAOS_KEYWORDS.add("IMPORT");
TAOS_KEYWORDS.add("IN");
TAOS_KEYWORDS.add("INDEX");
TAOS_KEYWORDS.add("INDEXES");
TAOS_KEYWORDS.add("INITIALLY");
TAOS_KEYWORDS.add("INNER");
TAOS_KEYWORDS.add("INSERT");
TAOS_KEYWORDS.add("INSTEAD");
TAOS_KEYWORDS.add("INT");
TAOS_KEYWORDS.add("INTEGER");
TAOS_KEYWORDS.add("INTERSECT");
TAOS_KEYWORDS.add("INTERVAL");
TAOS_KEYWORDS.add("INTO");
TAOS_KEYWORDS.add("IPTOKEN");
TAOS_KEYWORDS.add("IROWTS");
TAOS_KEYWORDS.add("IROWTS_ORIGIN");
TAOS_KEYWORDS.add("IS");
TAOS_KEYWORDS.add("IS_IMPORT");
TAOS_KEYWORDS.add("ISFILLED");
TAOS_KEYWORDS.add("ISNULL");
// J区关键字
TAOS_KEYWORDS.add("JLIMIT");
TAOS_KEYWORDS.add("JOIN");
TAOS_KEYWORDS.add("JSON");
// K区关键字
TAOS_KEYWORDS.add("KEEP");
TAOS_KEYWORDS.add("KEEP_TIME_OFFSET");
TAOS_KEYWORDS.add("KEY");
TAOS_KEYWORDS.add("KILL");
// L区关键字
TAOS_KEYWORDS.add("LANGUAGE");
TAOS_KEYWORDS.add("LAST");
TAOS_KEYWORDS.add("LAST_ROW");
TAOS_KEYWORDS.add("LE");
TAOS_KEYWORDS.add("LEADER");
TAOS_KEYWORDS.add("LEADING");
TAOS_KEYWORDS.add("LEFT");
TAOS_KEYWORDS.add("LEVEL");
TAOS_KEYWORDS.add("LICENCES");
TAOS_KEYWORDS.add("LIKE");
TAOS_KEYWORDS.add("LIMIT");
TAOS_KEYWORDS.add("LINEAR");
TAOS_KEYWORDS.add("LOCAL");
TAOS_KEYWORDS.add("LOGS");
TAOS_KEYWORDS.add("LOW_LATENCY_CALC");
TAOS_KEYWORDS.add("LP");
TAOS_KEYWORDS.add("LSHIFT");
TAOS_KEYWORDS.add("LT");
// M区关键字
TAOS_KEYWORDS.add("MACHINES");
TAOS_KEYWORDS.add("MATCH");
TAOS_KEYWORDS.add("MAX_DELAY");
TAOS_KEYWORDS.add("MAXROWS");
TAOS_KEYWORDS.add("MEDIUMBLOB");
TAOS_KEYWORDS.add("MERGE");
TAOS_KEYWORDS.add("META");
TAOS_KEYWORDS.add("META_ONLY");
TAOS_KEYWORDS.add("MINROWS");
TAOS_KEYWORDS.add("MINUS");
TAOS_KEYWORDS.add("MNODE");
TAOS_KEYWORDS.add("MNODES");
TAOS_KEYWORDS.add("MODIFY");
TAOS_KEYWORDS.add("MODULES");
// N区关键字
TAOS_KEYWORDS.add("NCHAR");
TAOS_KEYWORDS.add("NE");
TAOS_KEYWORDS.add("NEXT");
TAOS_KEYWORDS.add("NMATCH");
TAOS_KEYWORDS.add("NONE");
TAOS_KEYWORDS.add("NORMAL");
TAOS_KEYWORDS.add("NOT");
TAOS_KEYWORDS.add("NOTIFY");
TAOS_KEYWORDS.add("NOTIFY_HISTORY");
TAOS_KEYWORDS.add("NOTIFY_OPTIONS");
TAOS_KEYWORDS.add("NOTNULL");
TAOS_KEYWORDS.add("NOW");
TAOS_KEYWORDS.add("NULL");
TAOS_KEYWORDS.add("NULL_F");
TAOS_KEYWORDS.add("NULLS");
// O区关键字
TAOS_KEYWORDS.add("OF");
TAOS_KEYWORDS.add("OFFSET");
TAOS_KEYWORDS.add("ON");
TAOS_KEYWORDS.add("ONLY");
TAOS_KEYWORDS.add("ON_FAILURE");
TAOS_KEYWORDS.add("ON_FAILURE_PAUSE");
TAOS_KEYWORDS.add("OPTIONS");
TAOS_KEYWORDS.add("OR");
TAOS_KEYWORDS.add("ORDER");
TAOS_KEYWORDS.add("OUTER");
TAOS_KEYWORDS.add("OUTPUT_SUBTABLE");
TAOS_KEYWORDS.add("OUTPUTTYPE");
// P区关键字
TAOS_KEYWORDS.add("PAGES");
TAOS_KEYWORDS.add("PAGESIZE");
TAOS_KEYWORDS.add("PARTITION");
TAOS_KEYWORDS.add("PASS");
TAOS_KEYWORDS.add("PAUSE");
TAOS_KEYWORDS.add("PERIOD");
TAOS_KEYWORDS.add("PI");
TAOS_KEYWORDS.add("PLUS");
TAOS_KEYWORDS.add("PORT");
TAOS_KEYWORDS.add("POSITION");
TAOS_KEYWORDS.add("PPS");
TAOS_KEYWORDS.add("PRE_FILTER");
TAOS_KEYWORDS.add("PRECISION");
TAOS_KEYWORDS.add("PREV");
TAOS_KEYWORDS.add("PRIMARY");
TAOS_KEYWORDS.add("PRIVILEGE");
TAOS_KEYWORDS.add("PRIVILEGES");
// Q区关键字
TAOS_KEYWORDS.add("QDURATION");
TAOS_KEYWORDS.add("QEND");
TAOS_KEYWORDS.add("QNODE");
TAOS_KEYWORDS.add("QNODES");
TAOS_KEYWORDS.add("QSTART");
TAOS_KEYWORDS.add("QTAGS");
TAOS_KEYWORDS.add("QTIME");
TAOS_KEYWORDS.add("QUERIES");
TAOS_KEYWORDS.add("QUERY");
TAOS_KEYWORDS.add("QUESTION");
// R区关键字
TAOS_KEYWORDS.add("RAISE");
TAOS_KEYWORDS.add("RAND");
TAOS_KEYWORDS.add("RANGE");
TAOS_KEYWORDS.add("RATIO");
TAOS_KEYWORDS.add("READ");
TAOS_KEYWORDS.add("RECURSIVE");
TAOS_KEYWORDS.add("REGEXP");
TAOS_KEYWORDS.add("REDISTRIBUTE");
TAOS_KEYWORDS.add("REM");
TAOS_KEYWORDS.add("REPLACE");
TAOS_KEYWORDS.add("REPLICA");
TAOS_KEYWORDS.add("RESET");
TAOS_KEYWORDS.add("RESTORE");
TAOS_KEYWORDS.add("RESTRICT");
TAOS_KEYWORDS.add("RESUME");
TAOS_KEYWORDS.add("RETENTIONS");
TAOS_KEYWORDS.add("REVOKE");
TAOS_KEYWORDS.add("RIGHT");
TAOS_KEYWORDS.add("ROLLUP");
TAOS_KEYWORDS.add("ROW");
TAOS_KEYWORDS.add("ROWTS");
TAOS_KEYWORDS.add("RP");
TAOS_KEYWORDS.add("RSHIFT");
// S区关键字
TAOS_KEYWORDS.add("SCHEMALESS");
TAOS_KEYWORDS.add("SCORES");
TAOS_KEYWORDS.add("SELECT");
TAOS_KEYWORDS.add("SEMI");
TAOS_KEYWORDS.add("SERVER_STATUS");
TAOS_KEYWORDS.add("SERVER_VERSION");
TAOS_KEYWORDS.add("SESSION");
TAOS_KEYWORDS.add("SET");
TAOS_KEYWORDS.add("SHOW");
TAOS_KEYWORDS.add("SINGLE_STABLE");
TAOS_KEYWORDS.add("SLASH");
TAOS_KEYWORDS.add("SLIDING");
TAOS_KEYWORDS.add("SLIMIT");
TAOS_KEYWORDS.add("SMA");
TAOS_KEYWORDS.add("SMALLINT");
TAOS_KEYWORDS.add("SMIGRATE");
TAOS_KEYWORDS.add("SNODE");
TAOS_KEYWORDS.add("SNODES");
TAOS_KEYWORDS.add("SOFFSET");
TAOS_KEYWORDS.add("SPLIT");
TAOS_KEYWORDS.add("SS_CHUNKPAGES");
TAOS_KEYWORDS.add("SS_COMPACT");
TAOS_KEYWORDS.add("SS_KEEPLOCAL");
TAOS_KEYWORDS.add("SSMIGRATE");
TAOS_KEYWORDS.add("STABLE");
TAOS_KEYWORDS.add("STABLES");
TAOS_KEYWORDS.add("STAR");
TAOS_KEYWORDS.add("START");
TAOS_KEYWORDS.add("STATE");
TAOS_KEYWORDS.add("STATE_WINDOW");
TAOS_KEYWORDS.add("STATEMENT");
TAOS_KEYWORDS.add("STORAGE");
TAOS_KEYWORDS.add("STREAM");
TAOS_KEYWORDS.add("STREAMS");
TAOS_KEYWORDS.add("STRICT");
TAOS_KEYWORDS.add("STRING");
TAOS_KEYWORDS.add("STT_TRIGGER");
TAOS_KEYWORDS.add("SUBSCRIBE");
TAOS_KEYWORDS.add("SUBSCRIPTIONS");
TAOS_KEYWORDS.add("SUBSTR");
TAOS_KEYWORDS.add("SUBSTRING");
TAOS_KEYWORDS.add("SUBTABLE");
TAOS_KEYWORDS.add("SYSINFO");
TAOS_KEYWORDS.add("SYSTEM");
// T区关键字
TAOS_KEYWORDS.add("TABLE");
TAOS_KEYWORDS.add("TABLE_PREFIX");
TAOS_KEYWORDS.add("TABLE_SUFFIX");
TAOS_KEYWORDS.add("TABLES");
TAOS_KEYWORDS.add("TAG");
TAOS_KEYWORDS.add("TAGS");
TAOS_KEYWORDS.add("TBNAME");
TAOS_KEYWORDS.add("THEN");
TAOS_KEYWORDS.add("TIMES");
TAOS_KEYWORDS.add("TIMESTAMP");
TAOS_KEYWORDS.add("TIMEZONE");
TAOS_KEYWORDS.add("TINYINT");
TAOS_KEYWORDS.add("TO");
TAOS_KEYWORDS.add("TODAY");
TAOS_KEYWORDS.add("TOPIC");
TAOS_KEYWORDS.add("TOPICS");
TAOS_KEYWORDS.add("TRAILING");
TAOS_KEYWORDS.add("TRANSACTION");
TAOS_KEYWORDS.add("TRANSACTIONS");
TAOS_KEYWORDS.add("TRIGGER");
TAOS_KEYWORDS.add("TRIM");
TAOS_KEYWORDS.add("TRUE_FOR");
TAOS_KEYWORDS.add("TSDB_PAGESIZE");
TAOS_KEYWORDS.add("TSERIES");
TAOS_KEYWORDS.add("TSMA");
TAOS_KEYWORDS.add("TSMAS");
TAOS_KEYWORDS.add("TTL");
// U区关键字
TAOS_KEYWORDS.add("UNION");
TAOS_KEYWORDS.add("UNSAFE");
TAOS_KEYWORDS.add("UNSIGNED");
TAOS_KEYWORDS.add("UNTREATED");
TAOS_KEYWORDS.add("UPDATE");
TAOS_KEYWORDS.add("USE");
TAOS_KEYWORDS.add("USER");
TAOS_KEYWORDS.add("USERS");
TAOS_KEYWORDS.add("USING");
// V区关键字
TAOS_KEYWORDS.add("VALUE");
TAOS_KEYWORDS.add("VALUE_F");
TAOS_KEYWORDS.add("VALUES");
TAOS_KEYWORDS.add("VARBINARY");
TAOS_KEYWORDS.add("VARCHAR");
TAOS_KEYWORDS.add("VARIABLE");
TAOS_KEYWORDS.add("VARIABLES");
TAOS_KEYWORDS.add("VERBOSE");
TAOS_KEYWORDS.add("VGROUP");
TAOS_KEYWORDS.add("VGROUPS");
TAOS_KEYWORDS.add("VIEW");
TAOS_KEYWORDS.add("VIEWS");
TAOS_KEYWORDS.add("VNODE");
TAOS_KEYWORDS.add("VNODES");
// W区关键字
TAOS_KEYWORDS.add("WAL");
TAOS_KEYWORDS.add("WAL_FSYNC_PERIOD");
TAOS_KEYWORDS.add("WAL_LEVEL");
TAOS_KEYWORDS.add("WAL_RETENTION_PERIOD");
TAOS_KEYWORDS.add("WAL_RETENTION_SIZE");
TAOS_KEYWORDS.add("WAL_ROLL_PERIOD");
TAOS_KEYWORDS.add("WAL_SEGMENT_SIZE");
TAOS_KEYWORDS.add("WATERMARK");
TAOS_KEYWORDS.add("WDURATION");
TAOS_KEYWORDS.add("WEND");
TAOS_KEYWORDS.add("WHEN");
TAOS_KEYWORDS.add("WHERE");
TAOS_KEYWORDS.add("WINDOW");
TAOS_KEYWORDS.add("WINDOW_CLOSE");
TAOS_KEYWORDS.add("WINDOW_OFFSET");
TAOS_KEYWORDS.add("WINDOW_OPEN");
TAOS_KEYWORDS.add("WITH");
TAOS_KEYWORDS.add("WRITE");
TAOS_KEYWORDS.add("WSTART");
// 下划线开头关键字
TAOS_KEYWORDS.add("_C0");
TAOS_KEYWORDS.add("_IROWTS");
TAOS_KEYWORDS.add("_QDURATION");
TAOS_KEYWORDS.add("_QEND");
TAOS_KEYWORDS.add("_QSTART");
TAOS_KEYWORDS.add("_ROWTS");
TAOS_KEYWORDS.add("_WDURATION");
TAOS_KEYWORDS.add("_WEND");
TAOS_KEYWORDS.add("_WSTART");
}
/**
* 是否排除列
* @param col
* @return
*/
public static boolean tagNo(String col) {
return TAOS_KEYWORDS.contains(col);
}
/**
* 检查传入的字符串是否为TDengine SQL关键字。
* 如果是关键字,则用反引号包裹返回;否则原样返回。
*
* @param identifier 要检查的列名、表名等标识符
* @return 处理后的安全标识符字符串
*/
public static String escapeIfKeyword(String identifier) {
if (identifier == null || identifier.trim().isEmpty()) {
return identifier;
}
// 转换为大写进行比较(TDengine关键字不区分大小写)
String upperCaseIdentifier = identifier.toUpperCase().trim();
if (TAOS_KEYWORDS.contains(upperCaseIdentifier)) {
// 如果是关键字,用反引号包裹
return "`" + identifier.trim() + "`";
}
// 不是关键字,原样返回
return identifier.trim();
}
public static String mapToTDengineType(DataType dataType, String propertyId) {
String lowerId = propertyId.toLowerCase();
if (dataType instanceof IntType) return "INT";
if (dataType instanceof LongType) return "BIGINT";
if (dataType instanceof FloatType) return "FLOAT";
if (dataType instanceof DoubleType) return "DOUBLE";
if (dataType instanceof BooleanType) return "BOOL";
if (dataType instanceof DateTimeType) return "TIMESTAMP";
if (dataType instanceof StringType) {
// if (lowerId.contains("id")) return "NCHAR(64)";
// if (lowerId.contains("name") || lowerId.contains("type")) return "NCHAR(128)";
// if (lowerId.contains("desc") || lowerId.contains("message")) return "NCHAR(256)";
// if (lowerId.contains("info") || lowerId.contains("spec")) return "NCHAR(512)";
// return "NCHAR(64)";
return "VARCHAR(255)";
}
if (dataType instanceof ObjectType || dataType instanceof ArrayType) return "JSON";
return "NCHAR(64)";
}
/**
* 修改表名称
* @param rawName
* @return
*/
public static String getSafeTableName(String rawName) {
if (!StringUtils.hasText(rawName)) {
throw new IllegalArgumentException("表名不能为空");
}
String prefixed = DEFAULT_TABLE_PREFIX + rawName;
if (LEGAL_NAME_PATTERN.matcher(prefixed).matches()) {
return prefixed;
}
String safe = prefixed.replaceAll("[^a-zA-Z0-9_]", "_");
return safe;
}
// 辅助方法:获取类的简单名称(如java.lang.String → String)
public static String getSimpleClassName(Class<?> clazz) {
if (clazz == null) return "String";
return clazz.getSimpleName();
}
/**
* 对象转类型
* @param javaTypeName
* @return
*/
public static String mapClassToTDengineType(String javaTypeName){
// 空值校验,避免NPE
if (javaTypeName == null || javaTypeName.trim().isEmpty()) {
return "VARCHAR(512)";
}
// 统一转为大写,避免大小写匹配问题(如传入 "string" 也能匹配)
String type = javaTypeName.trim().toUpperCase();
switch (type) {
// 字符串类型
case "STRING":
return "VARCHAR(512)";
// 整型
case "INT":
case "INTEGER":
return "INT";
// 长整型
case "LONG":
return "BIGINT";
// 浮点型
case "FLOAT":
return "FLOAT";
case "DOUBLE":
return "DOUBLE";
// 布尔型
case "BOOLEAN":
return "BOOL";
// 时间类型(统一映射为TIMESTAMP,TDengine以毫秒为单位)
case "DATE":
case "LOCALDATE":
case "LOCALDATETIME":
case "TIMESTAMP":
return "TIMESTAMP";
// JSON类型(仅TDengine 3.0+支持)
case "OBJECT":
return "JSON";
// 默认情况:兜底为VARCHAR(512)
default:
return "VARCHAR(512)";
}
}
}
优化批量:
java
package cn.iot.things.tdengine.timeseries;
import com.alibaba.fastjson.JSON;
import com.zaxxer.hikari.HikariDataSource;
import lombok.extern.slf4j.Slf4j;
import org.jetlinks.community.timeseries.TimeSeriesData;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.publisher.Sinks;
import java.sql.*;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.Date;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import static cn.iot.things.tdengine.timeseries.TDengineHelper.escapeIfKeyword;
@Slf4j
public class BatchProcessor {
private static final int MAX_VALUES_PER_SQL =20 ;
private final String superTable;
private final int batchSize;
private final int flushSeconds;
private final Sinks.Many<TimeSeriesData> dataSink;
private final Flux<BatchResult> batchFlux;
private final AtomicInteger bufferCount = new AtomicInteger(0);
private final AtomicLong totalProcessed = new AtomicLong(0);
private final AtomicLong totalBatches = new AtomicLong(0);
/**
* 列与数据的映射
*/
private final Map<String, String> mapTagToData = new HashMap<>();
/**
* tag
*/
private final Map<String, String> mapTags;
private final HikariDataSource hikariDs;
private String td_db="jetlinks_db";
public BatchProcessor(String superTable, int batchSize, int flushSeconds, Map<String, String> mapTage, HikariDataSource hikariDs) {
this.superTable = superTable;
this.batchSize = batchSize;
this.flushSeconds = flushSeconds;
this.mapTags = mapTage;
this.hikariDs = hikariDs;
// 创建响应式数据源
this.dataSink = Sinks.many().unicast().onBackpressureBuffer(new ArrayDeque<>(1000));
// 构建批处理流
this.batchFlux = dataSink.asFlux()
.bufferTimeout(batchSize, java.time.Duration.ofSeconds(flushSeconds))
.filter(batch -> !batch.isEmpty())
.doOnNext(batch -> {
bufferCount.addAndGet(batch.size());
log.debug("[{}] 批次准备: {}条", superTable, batch.size());
})
.flatMap(this::processBatch, 2) // 并行度2
.doOnNext(result -> {
totalBatches.incrementAndGet();
totalProcessed.addAndGet(result.getSuccessCount());
log.debug("[{}] 批次完成: 成功{}条/总共{}条",
superTable, result.getSuccessCount(), result.getProcessedCount());
})
.onErrorResume(e -> {
log.error("[{}] 批处理异常", superTable, e);
return Flux.empty();
});
// 启动处理器
start();
}
/**
* 启动处理器
*/
private void start() {
batchFlux.subscribe(
result -> {
},
error -> log.error("[{}] 处理器异常终止", superTable, error),
() -> log.info("[{}] 处理器正常结束", superTable)
);
log.info("[{}] 批处理器启动 [size={}, interval={}s]",
superTable, batchSize, flushSeconds);
}
/**
* 提交数据
*/
public boolean submit(TimeSeriesData data) {
try {
Sinks.EmitResult result = dataSink.tryEmitNext(data);
if (result.isSuccess()) {
return true;
} else {
log.warn("[{}] 提交失败: {}", superTable, result);
// 缓冲满时直接处理单条数据
return processImmediately(data);
}
} catch (Exception e) {
log.error("[{}] 提交异常", superTable, e);
return false;
}
}
/**
* 立即处理单条数据
*/
private boolean processImmediately(TimeSeriesData data) {
List<TimeSeriesData> singleBatch = Collections.singletonList(data);
processBatch(singleBatch)
.subscribe(result -> {
log.info("[{}] 缓冲满直接处理: {}条", superTable, result.getProcessedCount());
});
return true;
}
private String getChildTable(String tableName, TimeSeriesData data) {
return getTdTableNameRegex(tableName, data);
}
/**
* 处理子表同时查找映射关系
*
* @param tableName
* @param data
* @return
*/
private String getTdTableNameRegex(String tableName, TimeSeriesData data) {
Map<String, String> coloumns = mapTags;
if (coloumns == null) {
return "dev0000";
}
StringBuilder builder = new StringBuilder();
builder.append("ts");
for (String col : coloumns.keySet()) {
//查找一下
if (data.getData().containsKey(col)) {
builder.append("_").append(data.getData().get(col));
mapTagToData.put(col, col);
} else {
Map<String, Object> objectMap = data.getData();
if (mapTagToData.containsKey(col)) {
String key = mapTagToData.get(col);
builder.append("_").append(objectMap.get(key));
} else {
for (String key : objectMap.keySet()) {
if (key.toLowerCase().equals(col.toLowerCase())) {
builder.append("_").append(objectMap.get(key));
mapTagToData.put(col, key);
break;
}
}
}
}
}
// 仅保留字母、数字、下划线,其余替换为下划线
String child = builder.toString();
String cleanName = child.trim().replaceAll("[^a-zA-Z0-9_]", "_");
// 确保以字母/下划线开头(避免数字开头)
if (!cleanName.isEmpty() && Character.isDigit(cleanName.charAt(0))) {
cleanName = "t_" + cleanName;
}
return cleanName;
}
/**
* 处理批次数据
*/
private Mono<BatchResult> processBatch(List<TimeSeriesData> batch) {
if (batch.isEmpty()) {
return Mono.just(new BatchResult(0, 0, 0));
}
return Mono.fromCallable(() -> {
try {
// 按子表分组(同一子表可以批量插入)
Map<String, List<TimeSeriesData>> groupedByChildTable = new HashMap<>();
for (TimeSeriesData data : batch) {
String childTable = getChildTable(superTable, data);
groupedByChildTable
.computeIfAbsent(childTable, k -> new ArrayList<>())
.add(data);
}
int totalSuccess = 0;
int totalFailed = 0;
// 处理每个子表的数据
for (Map.Entry<String, List<TimeSeriesData>> entry : groupedByChildTable.entrySet()) {
String childTable = entry.getKey();
List<TimeSeriesData> childTableBatch = entry.getValue();
try {
// BatchResult result = insertBatch(superTable, childTable, childTableBatch);
BatchResult result =insertBatchSql(superTable, childTable, childTableBatch);
totalSuccess += result.getSuccessCount();
totalFailed += result.getFailedCount();
} catch (Exception e) {
log.error("[{}] 子表[{}]批量插入失败", superTable, childTable, e);
// 降级为逐条插入
BatchResult fallbackResult = insertOneByOne(superTable, childTable, childTableBatch);
totalSuccess += fallbackResult.getSuccessCount();
totalFailed += fallbackResult.getFailedCount();
}
}
return new BatchResult(totalSuccess, totalFailed, batch.size());
} catch (Exception e) {
log.error("[{}] 处理批次异常", superTable, e);
return new BatchResult(0, batch.size(), batch.size());
}
});
}
/**
* 批量插入到同一子表
*/
private BatchResult insertBatch(String superTable, String childTable, List<TimeSeriesData> batch) {
if (batch.isEmpty()) {
return new BatchResult(0, 0, 0);
}
// 使用第一条数据作为模板
TimeSeriesData template = batch.get(0);
Map<String, Object> tags = getTagsValues(template);
Map<String, Object> columns = getValue(template);
// 构建SQL
String sql = buildInsertSql(superTable, childTable, tags, columns);
log.info("批量构建sql:{}",sql);
// 准备参数
List<Object[]> batchParams = new ArrayList<>();
for (TimeSeriesData data : batch) {
Object[] params = prepareParams(data);
log.info("构建参数:{}",params.length);
batchParams.add(params);
}
try {
// 执行批量插入
int[] results = null;
// jdbcTemplate.batchUpdate(sql, batchParams, batchParams.get(0).length,
// (ps, params) -> {
// for (int i = 0; i < params.length; i++) {
// setPreparedStatementValue(ps, i + 1, params[i]);
// }
// });
try(Connection connection=hikariDs.getConnection()) {
PreparedStatement preparedStatement = connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);
for (int i = 0; i < batchParams.size(); i++) {
setPreparedStatementValue(preparedStatement,i+1,batchParams.get(i));
preparedStatement.addBatch();
}
results= preparedStatement.executeBatch();
}
int success = 0;
for (int result : results) {
if (result > 0) {
success++;
}
}
int failed = batch.size() - success;
return new BatchResult(success, failed, batch.size());
} catch (Exception e) {
log.error("[{}] 批量插入异常", superTable, e);
return new BatchResult(0, 0, 0);
}
}
/**
* 原生批量,参数化目前驱动不支持
* @param superTable
* @param childTable
* @param batch
* @return
*/
private BatchResult insertBatchSql(String superTable, String childTable, List<TimeSeriesData> batch){
List<String> lstBatch = buildNativeBatchSqls(superTable, childTable, batch);
int successCount = 0;
for (String sql : lstBatch) {
try(Connection connection=hikariDs.getConnection()){
Statement stmt = connection.createStatement();
int affected = stmt.executeUpdate(sql);
// TDengine返回受影响的行数
if (affected >= 0) {
successCount += batch.size(); // 假设全部成功
}
} catch (SQLException e) {
log.error("执行批量SQL失败: {}", e.getMessage());
log.info("批量:{}",sql);
throw new RuntimeException(e);
}
}
int failedCount = batch.size() - successCount;
return new BatchResult(successCount, failedCount, batch.size());
}
/**
* 构建插入SQL
*/
private String buildInsertSql(String superTable, String childTable,
Map<String, Object> tags, Map<String, Object> columns) {
StringBuilder sql = new StringBuilder();
// INSERT INTO 子表名 USING 超级表名 TAGS (标签值) (字段列) VALUES (字段值)
sql.append("INSERT INTO ? USING ").append(td_db).append(".").append(superTable).append(" TAGS (");
// 标签占位符
if (tags != null && !tags.isEmpty()) {
List<String> tagPlaceholders = new ArrayList<>();
for (int i = 0; i < tags.size(); i++) {
tagPlaceholders.add("?");
}
sql.append(String.join(",", tagPlaceholders));
} else {
sql.append("?");
}
sql.append(") (");
// 列名
if (columns != null && !columns.isEmpty()) {
List<String> lstcolumn = new ArrayList<>();
for (Map.Entry<String, Object> entry : columns.entrySet()) {
lstcolumn.add(escapeIfKeyword(entry.getKey()));
}
sql.append(String.join(",", lstcolumn));
} else {
sql.append("ts"); // 默认时间戳列
}
sql.append(") VALUES (");
// 值占位符
if (columns != null && !columns.isEmpty()) {
List<String> valuePlaceholders = new ArrayList<>();
for (int i = 0; i < columns.size(); i++) {
valuePlaceholders.add("?");
}
sql.append(String.join(",", valuePlaceholders));
} else {
sql.append("?");
}
sql.append(")");
return sql.toString();
}
private List<String> buildNativeBatchSqls(String superTable, String childTable,
List<TimeSeriesData> batch) {
List<String> sqlList = new ArrayList<>();
TimeSeriesData first = batch.get(0);
first.getData().put("ts", LocalDateTime.now());
// 1. 构建SQL头部(固定部分)
String sqlHeader = buildSqlHeader(superTable, childTable, first);
// 2. 分批构建VALUES(防止SQL过长)
List<List<TimeSeriesData>> batches = splitBatch(batch, MAX_VALUES_PER_SQL);
for (List<TimeSeriesData> subBatch : batches) {
StringBuilder sql = new StringBuilder(sqlHeader);
// 构建VALUES部分
List<String> valueStrings = new ArrayList<>();
for (TimeSeriesData data : subBatch) {
data.getData().put("ts", LocalDateTime.now());
valueStrings.add(buildValueString(data));
}
sql.append(String.join(",", valueStrings));
sqlList.add(sql.toString());
}
return sqlList;
}
/**
* 构建SQL头部
*/
private String buildSqlHeader(String superTable, String childTable, TimeSeriesData data) {
StringBuilder header = new StringBuilder();
// INSERT INTO 子表名 USING 超级表名 TAGS(标签值)
header.append("INSERT INTO ").append(childTable)
.append(" USING ").append(td_db).append(".").append(superTable)
.append(" TAGS (");
Map<String,Object> tags =getTagsValues(data);
if (!tags.isEmpty()) {
List<String> tagValues = new ArrayList<>();
for (Object value : tags.values()) {
tagValues.add(formatValue(value));
}
header.append(String.join(",", tagValues));
} else {
header.append("''");
}
header.append(") (");
// 列名
Map<String,Object> columns =getValue(data);
if (!columns.isEmpty()) {
List<String> columnNames = new ArrayList<>();
for (String column : columns.keySet()) {
columnNames.add(escapeIfKeyword(column));
}
header.append(String.join(",", columnNames));
}
header.append(") VALUES ");
return header.toString();
}
/**
* 拆分批次
*/
private List<List<TimeSeriesData>> splitBatch(List<TimeSeriesData> batch, int maxPerBatch) {
List<List<TimeSeriesData>> batches = new ArrayList<>();
for (int i = 0; i < batch.size(); i += maxPerBatch) {
int end = Math.min(i + maxPerBatch, batch.size());
batches.add(batch.subList(i, end));
}
return batches;
}
/**
* 构建单个VALUES字符串
*/
private String buildValueString(TimeSeriesData data) {
StringBuilder value = new StringBuilder("(");
Map<String,Object> columns =getValue(data);
if (!columns.isEmpty()) {
List<String> columnValues = new ArrayList<>();
for (Object valueObj :columns.values()) {
columnValues.add(formatValue(valueObj));
}
value.append(String.join(",", columnValues));
}
value.append(")");
return value.toString();
}
/**
* 格式化值(用于SQL拼接)
*/
private String formatValue(Object value) {
if (value == null) {
return "NULL";
} else if (value instanceof Number) {
return value.toString();
} else if (value instanceof Boolean) {
return ((Boolean) value) ? "true" : "false";
} else if (value instanceof LocalDateTime) {
// TDengine时间格式
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS");
return "'" + ((LocalDateTime) value).format(formatter) + "'";
} else if (value instanceof Date) {
LocalDateTime ldt = ((Date) value).toInstant()
.atZone(java.time.ZoneId.systemDefault())
.toLocalDateTime();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS");
return "'" + ldt.format(formatter) + "'";
} else if (value instanceof String) {
String str = (String) value;
// 转义单引号
str = str.replace("'", "''");
// 限制长度
if (str.length() > 16374) {
str = str.substring(0, 16374 - 3) + "...";
}
return "'" + str + "'";
} else if (value.getClass().getSimpleName().toLowerCase().equals("object")) {
return JSON.toJSONString(value);
} else if (value instanceof Optional) {
Optional<?> optional = (Optional<?>) value;
if (!optional.isPresent()) {
return "''";
}
Object innerValue = optional.get();
// 递归处理内部值
return formatValue(innerValue);
}
// 处理 Map 类型
else if (value instanceof Map) {
return "'"+JSON.toJSONString(value)+"'";
}
else {
String className = value.getClass().getSimpleName();
String str = value.toString();
str = str.replace("'", "''");
if (str.length() > 1000) {
str = str.substring(0, 1000 - 3) + "...";
}
return "'" + str + "'";
}
}
/**
* SQL 转义
*/
private static String escapeForSql(String str) {
if (str == null) return "''";
// 转义单引号
str = str.replace("'", "''");
// 限制长度
if (str.length() > 100) {
str = str.substring(0, 100 - 3) + "...";
}
return "'" + str + "'";
}
/**
* 标签列的值
*
* @param data
* @return
*/
private Map<String, Object> getTagsValues(TimeSeriesData data) {
Map<String, String> tags = mapTags;
Map<String, Object> objectMap = new HashMap<>();
for (String key : tags.keySet()) {
if (data.getData().containsKey(key)) {
objectMap.put(key, data.get(key));
} else {
if (mapTagToData.containsKey(key)) {
String col = mapTagToData.get(key);
objectMap.put(key, data.get(col));
}
}
}
return objectMap;
}
/**
* 准备参数数组
*/
private Object[] prepareParams(TimeSeriesData data) {
List<Object> params = new ArrayList<>();
// 1. 子表名
params.add(getChildTable(superTable, data));
// 2. 标签值
Map<String, Object> tagvalue = getTagsValues(data);
if (!tagvalue.isEmpty()) {
params.addAll(tagvalue.values());
} else {
params.add(""); // 空标签
}
// 3. 字段值
Map<String, Object> columnvalue = getValue(data);
if (!columnvalue.isEmpty()) {
params.addAll(columnvalue.values());
} else {
params.add(LocalDateTime.now()); // 默认时间戳
}
return params.toArray();
}
private Map<String, Object> getValue(TimeSeriesData data) {
Map<String, Object> vaue = new HashMap<>(data.getData());
Map<String, String> tags = mapTags;
mapTagToData.values().forEach(vaue::remove);
return vaue;
}
/**
* 逐条插入(降级方案)
*/
private BatchResult insertOneByOne(String superTable, String childTable, List<TimeSeriesData> batch) {
int success = 0;
int failed = 0;
for (TimeSeriesData data : batch) {
try {
boolean inserted = insertSingle(superTable, childTable, data);
if (inserted) {
success++;
} else {
failed++;
}
} catch (Exception e) {
log.error("[{}] 单条插入失败", superTable, e);
failed++;
}
}
return new BatchResult(success, failed, batch.size());
}
/**
* 插入单条数据
*/
private boolean insertSingle(String superTable, String childTable, TimeSeriesData data) {
Map<String, Object> tagvalue = getTagsValues(data);
Map<String, Object> columnvalue = getValue(data);
String sql = buildInsertSql(superTable, childTable, tagvalue,columnvalue);
Object[] params = prepareParams(data);
try {
int affected = 0;
try(Connection connection=hikariDs.getConnection()) {
PreparedStatement stm = connection.prepareStatement(sql);
for (int i = 0; i <params.length ; i++) {
setPreparedStatementValue(stm,i+1,params[i]);
}
affected= stm.executeUpdate();
stm.close();
connection.close();
}
return affected > 0;
} catch (Exception e) {
log.error("[{}] 单条SQL执行失败", superTable, e);
log.info("sql:{}",sql);
return false;
}
}
/**
* 手动刷新
*/
public Mono<BatchResult> forceFlush() {
return Mono.fromCallable(() -> {
log.info("[{}] 手动刷新", superTable);
int count = bufferCount.getAndSet(0);
return new BatchResult(0, 0, count);
});
}
/**
* 获取统计信息
*/
public Map<String, Object> getStats() {
return Map.of(
"bufferCount", bufferCount.get(),
"totalProcessed", totalProcessed.get(),
"totalBatches", totalBatches.get(),
"batchSize", batchSize,
"flushInterval", flushSeconds
);
}
/**
* 关闭处理器
*/
public void shutdown() {
log.info("[{}] 关闭批处理器", superTable);
dataSink.tryEmitComplete();
}
/**
* 设置PreparedStatement的值
*/
private void setPreparedStatementValue(PreparedStatement pstmt, int index, Object value)
throws SQLException {
if (value == null) {
pstmt.setNull(index, Types.VARCHAR);
} else if (value instanceof Integer) {
pstmt.setInt(index, (Integer) value);
} else if (value instanceof Long) {
pstmt.setLong(index, (Long) value);
} else if (value instanceof Double) {
pstmt.setDouble(index, (Double) value);
} else if (value instanceof Float) {
pstmt.setFloat(index, (Float) value);
} else if (value instanceof Boolean) {
pstmt.setBoolean(index, (Boolean) value);
} else if (value instanceof Timestamp) {
pstmt.setTimestamp(index, (Timestamp) value);
} else if (value instanceof Date) {
java.sql.Date date = new java.sql.Date(((Date) value).getTime());
pstmt.setDate(index, date);
} else if (value instanceof LocalDateTime) {
pstmt.setTimestamp(index, Timestamp.valueOf((LocalDateTime) value));
} else if (value instanceof byte[]) {
pstmt.setBytes(index, (byte[]) value);
} else if (value instanceof String) {
String str = (String) value;
if (str.length() > 512) {
str = str.substring(0, 512 - 3) + "...";
}
pstmt.setString(index, str);
}
else if (value instanceof Optional) {
Optional<?> optional = (Optional<?>) value;
if (!optional.isPresent()) {
pstmt.setString(index, null);
}
Object innerValue = optional.get();
pstmt.setString(index, JSON.toJSONString(innerValue));
}
// 处理 Map 类型
else if (value instanceof Map) {
pstmt.setString(index, JSON.toJSONString(value));
}
else {
pstmt.setString(index, value.toString());
}
}
}
批量实体:
java
package cn.iot.things.tdengine.timeseries;
import lombok.AllArgsConstructor;
import lombok.Data;
/**
* 批次结果
*/
@Data
@AllArgsConstructor
public class BatchResult {
private int successCount;
private int failedCount;
private int processedCount;
}