1.10.1. 概述
时序库统一操作 API 提供了一套 跨时序数据库的统一访问与写入抽象, 用于屏蔽不同后端在连接方式、写入模型、SQL 使用方式以及查询结果处理上的差异。
当前支持的后端包括:
InfluxDB 1.x(
jdbc:influxdb:)ClickHouse(
jdbc:clickhouse:)
同时支持通过统一 SQL(Unisql)机制, 在查询与写入阶段使用一套统一的 SQL 语法, 由框架在运行时自动适配并下发至目标时序数据库。
该 API 的目标是提供一套 稳定、统一、接近 JDBC 使用体验的时序操作接口, 重点解决跨时序库使用成本高、SQL 与结果处理不一致的问题。
1.10.2. 快速开始
本节通过一个完整示例,展示典型使用流程:
创建数据源
获取客户端(类似 JDBC Connection)
使用预编译 SQL 写入数据(支持绑定变量)
查询并处理统一结果集
1.10.2.1. Maven 依赖
1.10.2.1.1. 基础依赖
统一操作 API 由 com.hundsun.lightdb:tsdb 提供,
并依赖统一 SQL 转换运行时组件:
<dependency>
<groupId>com.hundsun.lightdb</groupId>
<artifactId>tsdb</artifactId>
<version>${unisql.version}</version>
</dependency>
<dependency>
<groupId>com.hundsun.lightdb</groupId>
<artifactId>sql-convert-runtime</artifactId>
<version>${unisql.version}</version>
</dependency>
1.10.2.1.2. 原生时序库依赖(必需)
统一操作 API 不会内置具体时序库的原生客户端。 使用方必须根据实际后端 显式引入对应的原生 API 依赖, 否则在运行期将无法正常使用。
1.10.2.1.2.1. InfluxDB 1.x
<dependency>
<groupId>org.influxdb</groupId>
<artifactId>influxdb-java</artifactId>
<version>2.25</version>
</dependency>
1.10.2.1.2.2. ClickHouse
<dependency>
<groupId>com.clickhouse</groupId>
<artifactId>clickhouse-jdbc</artifactId>
<version>0.9.6</version>
</dependency>
1.10.2.2. 创建数据源
TimeSeriesDataSource 采用 接口抽象 的方式定义, 具体实现可以支持:
连接池化
统一配置管理
与 JDBC DataSource 类似的生命周期管理
当需要启用统一 SQL 时,
通过 jdbc:unisql: 前缀并结合 targetDialect 参数指定目标时序库。
TimeSeriesDataSource dataSource = new TimeSeriesManagerDataSource();
dataSource.setUrl(
"jdbc:unisql:influxdb:127.0.0.1:8086?targetDialect=influxdb1"
);
dataSource.setDatabase("example_db");
dataSource.setUsername("admin");
dataSource.setPassword("admin");
1.10.2.3. 获取客户端
获取 TimeSeriesClient 的方式与 JDBC 获取 Connection 类似,
使用完成后必须关闭,以释放底层资源。
try (TimeSeriesClient client = dataSource.getClient()) {
// 与 JDBC Connection 类似,使用完成后需要关闭
}
1.10.2.4. 使用预编译 SQL 写入时序数据
时序数据的写入通过 标准 SQL + 预编译语句 完成。
SQL 中可以使用 ? 占位符,
通过 TimeSeriesPreparedStatement 进行参数绑定。
示例:写入一条时序数据。
String insertSql =
"INSERT /*+ TAGS(region) FIELDS(temperature, humidity) TIMESTAMP(time) */ INTO weather(time, region, temperature, humidity) " +
"VALUES (?, ?, ?, ?)";
try (TimeSeriesClient client = dataSource.getClient();
TimeSeriesPreparedStatement ps = client.prepareStatement(insertSql)) {
ps.setTimestamp(1, System.currentTimeMillis(), TimeUnit.MILLISECONDS);
ps.setString(2, "east");
ps.setDouble(3, 23.5);
ps.setInt(4, 60);
ps.executeUpdate();
}
说明:
SQL 使用统一形式描述写入语义
时间戳通过
setTimestamp显式指定时间单位参数索引从
1开始,行为与 JDBC PreparedStatement 一致INSERT 语句必须有配套的 HINT ,用于指定列类型 INSERT SQL 规则
所有 ``setXxx()`` 方法均不允许传入 ``null`` 值,否则抛出 ``IllegalArgumentException``
ClickHouse 自动建表机制
ClickHouse 后端支持自动建表和添加列功能,简化用户的使用流程:
自动创建表:当表不存在时,根据 INSERT 语句和 hint 中的 ENGINE/PARTITION/ORDER_BY 信息自动创建表
自动添加列:当表已存在但缺少某些列时,通过 ALTER TABLE 自动添加缺失的列
列类型推断:根据
setXxx()调用的参数类型自动推断列类型(如setInt()推断为 Int64)NULLable 处理:TAG 和 TIMESTAMP 列自动声明为非 NULLable,FIELD 列自动声明为 NULLable
自动建表示例:
// 第一次写入,表不存在,自动创建
String insertSql =
"INSERT /*+ TAGS(region) FIELDS(temperature) TIMESTAMP(time)
ENGINE(MergeTree())
PARTITION(toDate(time))
ORDER_BY(time) */ INTO sensor_data
(time, region, temperature) VALUES (?, ?, ?)";
try (TimeSeriesClient client = dataSource.getClient();
TimeSeriesPreparedStatement ps = client.prepareStatement(insertSql)) {
ps.setTimestamp(1, System.currentTimeMillis(), TimeUnit.MILLISECONDS);
ps.setString(2, "east");
ps.setDouble(3, 25.3);
ps.executeUpdate(); // 自动创建表并写入数据
}
添加新列示例:
// 表已存在,但新增了 humidity 字段
String insertSql =
"INSERT /*+ TAGS(region) FIELDS(temperature, humidity) TIMESTAMP(time)
ENGINE(MergeTree())
PARTITION(toDate(time))
ORDER_BY(time) */ INTO sensor_data
(time, region, temperature, humidity) VALUES (?, ?, ?, ?)";
try (TimeSeriesClient client = dataSource.getClient();
TimeSeriesPreparedStatement ps = client.prepareStatement(insertSql)) {
ps.setTimestamp(1, System.currentTimeMillis(), TimeUnit.MILLISECONDS);
ps.setString(2, "east");
ps.setDouble(3, 25.3);
ps.setInt(4, 60); // humidity 是新增列
ps.executeUpdate(); // 自动添加 humidity 列并写入数据
}
注意事项:
自动建表和添加列功能仅对 ClickHouse 后端生效
ALTER TABLE 添加列可能因类型不兼容而失败,此时会抛出异常
多线程并发插入同一表时,使用表级锁保证操作的线程安全
元数据缓存的轻微不一致是可以接受的,仅影响自动 alter 的性能**
参数绑定限制示例:
// 错误示例:绑定 null 会抛出异常
ps.setString(1, null); // 抛出 IllegalArgumentException
// 正确做法:如果字段不传,直接在 INSERT 语句中不包含该列
String insertSql = "INSERT /*+ TAGS(region) FIELDS(value) TIMESTAMP(time) */ INTO table (region, value, time) VALUES (?, ?, ?)";
1.10.2.5. 时间戳处理方式
时序库统一操作 API 中的时间戳处理遵循以下规则:
统一 UTC 时间戳
所有时间戳参数(
setTimestamp)和时间戳返回值(getTimestampNanos)均使用 UTC 时区 的时间戳时间戳以纳秒(nanoseconds)为单位,从 1970-01-01T00:00:00Z 开始计算
不同后端的时区差异由框架内部自动转换,用户无需关心
写入时的时间戳
写入时使用 setTimestamp(int parameterIndex, long timestamp, TimeUnit unit) 方法:
// 使用毫秒时间戳写入
ps.setTimestamp(1, System.currentTimeMillis(), TimeUnit.MILLISECONDS);
说明:
parameterIndex:参数索引,从 1 开始timestamp:时间戳值unit:时间单位(如 TimeUnit.MILLISECONDS、TimeUnit.SECONDS)框架会根据传入的 timeUnit 自动转换为纳秒存储
查询时的时间戳
查询时使用 getTimestampNanos(String columnName) 方法:
try (TimeSeriesResultSet rs = ps.executeQuery()) {
while (rs.next()) {
long timestampNanos = rs.getTimestampNanos("time");
// 根据需要转换为毫秒或其他单位
long timestampMillis = timestampNanos / 1_000_000;
// 转换为 LocalDateTime(UTC 时区)
Instant instant = Instant.ofEpochSecond(
timestampNanos / 1_000_000_000,
timestampNanos % 1_000_000_000
);
LocalDateTime utcDateTime = LocalDateTime.ofInstant(instant, ZoneOffset.UTC);
}
}
说明:
返回值为纳秒级 UTC 时间戳
用户可根据需要进行单位转换或格式化
不同后端返回的时间戳格式由框架统一处理
时区处理建议
建议始终使用 UTC 时区进行时间戳的存储和查询
如需显示其他时区时间,可在应用层进行转换
避免在不同时区之间进行隐式转换,以免引起混淆
示例:完整的时区转换
// 写入:将本地时间转换为 UTC 时间戳后写入
LocalDateTime localTime = LocalDateTime.now();
Instant utcInstant = localTime.atZone(ZoneId.systemDefault()).toInstant();
long timestampNanos = utcInstant.getEpochSecond() * 1_000_000_000 + utcInstant.getNano();
ps.setTimestamp(1, timestampNanos, TimeUnit.NANOSECONDS);
// 查询:读取 UTC 时间戳后转换为本地时区显示
long readTimestampNanos = rs.getTimestampNanos("time");
Instant readInstant = Instant.ofEpochSecond(
readTimestampNanos / 1_000_000_000,
readTimestampNanos % 1_000_000_000
);
LocalDateTime localDisplayTime = LocalDateTime.ofInstant(readInstant, ZoneId.systemDefault());
1.10.2.6. 批量操作支持
时序数据库预编译语句支持批量插入操作,仅限 INSERT 语句使用。
通过 addBatch() executeBatch() 和 clearBatch() 方法实现批量操作。
示例:批量写入多条时序数据。
String insertSql =
"INSERT /*+ TAGS(region) FIELDS(temperature, humidity) TIMESTAMP(time) */ INTO weather(time, region, temperature, humidity) "
"VALUES (?, ?, ?, ?)";
try (TimeSeriesClient client = dataSource.getClient();
TimeSeriesPreparedStatement ps = client.prepareStatement(insertSql)) {
// 第一条记录
ps.setTimestamp(1, System.currentTimeMillis(), TimeUnit.MILLISECONDS);
ps.setString(2, "east");
ps.setDouble(3, 23.5);
ps.setInt(4, 60);
ps.addBatch(); // 添加到批量缓存
// 第二条记录
ps.setTimestamp(1, System.currentTimeMillis() + 1000, TimeUnit.MILLISECONDS);
ps.setString(2, "west");
ps.setDouble(3, 24.1);
ps.setInt(4, 55);
ps.addBatch(); // 添加到批量缓存
// 执行批量操作
int[] results = ps.executeBatch();
}
说明:
仅 INSERT 语句支持批量操作
使用
addBatch()将当前参数加入批量缓存使用
executeBatch()执行所有批量语句使用
clearBatch()可清空批量缓存返回结果为每条语句的影响行数数组
1.10.2.7. 查询与结果处理
查询同样通过 prepareStatement 执行,
查询结果以 统一结果集 TimeSeriesResultSet 返回。
示例如下:
String querySql =
"SELECT time, temperature, humidity " +
"FROM weather WHERE region = ?";
try (TimeSeriesClient client = dataSource.getClient();
TimeSeriesPreparedStatement ps = client.prepareStatement(querySql)) {
ps.setString(1, "east");
try (TimeSeriesResultSet rs = ps.executeQuery()) {
while (rs.next()) {
long time = rs.getTimestampNanos("time");
double temperature = rs.getDouble("temperature");
int humidity = rs.getInt("humidity");
// 业务处理
}
}
}
结果说明:
返回标准的 TimeSeriesResultSet 接口,接近 JDBC 规范
使用
next()遍历结果集通过列名或列索引获取字段值
使用完成后必须关闭 TimeSeriesResultSet
1.10.2.8. 完整案例
下面给出一个 完整、可运行的示例,演示:
使用统一 SQL 创建 InfluxDB 1.x 数据源
写入一条时序数据
查询并解析统一结果集
TimeSeriesDataSource dataSource = new TimeSeriesManagerDataSource();
dataSource.setUrl("jdbc:unisql:influxdb:127.0.0.1:8086?targetDialect=influxdb1");
dataSource.setDatabase("example_db");
dataSource.setUsername("admin");
dataSource.setPassword("admin");
try (TimeSeriesClient client = dataSource.getClient()) {
// =========================
// 1. 写入时序数据
// =========================
String insertSql =
"INSERT /*+ TAGS(region) FIELDS(temperature, humidity) TIMESTAMP(time) */ INTO weather(time, region, temperature, humidity) " +
"VALUES (?, ?, ?, ?)";
try (TimeSeriesPreparedStatement ps =
client.prepareStatement(insertSql)) {
ps.setTimestamp(1, System.currentTimeMillis(), TimeUnit.MILLISECONDS);
ps.setString(2, "east");
ps.setDouble(3, 25.3);
ps.setInt(4, 58);
ps.executeUpdate();
}
// =========================
// 2. 查询时序数据
// =========================
String querySql =
"SELECT time, temperature, humidity FROM weather WHERE region = ?";
try (TimeSeriesPreparedStatement ps =
client.prepareStatement(querySql)) {
ps.setString(1, "east");
try (TimeSeriesResultSet rs = ps.executeQuery()) {
while (rs.next()) {
long time = rs.getTimestampNanos("time");
double temperature = rs.getDouble("temperature");
int humidity = rs.getInt("humidity");
// 业务处理
}
}
}
}
1.10.3. 统一 SQL(Unisql)使用说明
统一 SQL(Unisql)用于在 SQL 层面 屏蔽不同后端时序库的语法差异, 使应用能够使用一套统一的 SQL 描述完成写入与查询操作。
1.10.3.1. 启用方式
统一 SQL 通过 JDBC URL 前缀 启用:
使用
jdbc:unisql:作为 URL 前缀通过
targetDialect参数指定目标时序库类型
示例:
InfluxDB 1.x
jdbc:unisql:influxdb:host:port?targetDialect=influxdb1ClickHouse
jdbc:unisql:clickhouse:host:port?targetDialect=clickhouse
1.10.3.2. 使用限制与注意事项
统一 SQL 对
prepareStatement的SELECT语句生效,其余语句不转换SQL 在执行前会根据
targetDialect自动转换返回结果统一为
TimeSeriesResultSet并非所有底层特性都具备完全等价的统一表达形式
运行期切换 database 或 retentionPolicy 的机制并不是所有后端都支持
建议将连接级别的上下文配置放置在 DataSource 初始化阶段完成。
1.10.4. 数据类型说明
本节说明 Java API 中 TimeSeriesPreparedStatement 和 TimeSeriesResultSet 支持的数据类型,
以及各后端数据库支持的具体数据类型。
1.10.4.1. Java API 方法支持的数据类型
TimeSeriesPreparedStatement setXxx() 方法
方法 |
InfluxDB 数据类型 |
ClickHouse 数据类型 |
|---|---|---|
setString(int, String) |
所有字符串类型:String, tag/field 字符串 |
String |
setInt(int, int) |
整数(在 Influx 中为 numeric field) |
Int64 |
setLong(int, long) |
64 位整数或时间戳表示 |
Int64 |
setShort(int, short) |
小范围整数 |
Int64 |
setFloat(int, float) |
浮点数(单精度) |
Float64 |
setDouble(int, double) |
浮点数(双精度) |
Float64 |
setBoolean(int, boolean) |
布尔或数值表示的布尔字段 |
Bool (底层为 UInt8) |
setTimestamp(int, long, TimeUnit) |
时间戳(以纳秒/毫秒等传入,框架转换为纳秒) |
DateTime, DateTime64 |
TimeSeriesResultSet getXxx() 方法
方法 |
InfluxDB 数据类型 |
ClickHouse 支持的数据类型 |
|---|---|---|
getString(String columnName) |
所有类型(通用方法,返回字符串表示) |
所有类型(通用方法,返回字符串表示) |
getInt(String columnName) |
整数类型(在 Influx 中 field integer) |
Int8, Int16, Int32, UInt8, UInt16, UInt32 |
getLong(String columnName) |
64 位整数类型 |
Int64, UInt64 |
getShort(String columnName) |
小范围整数 |
Int8, Int16 |
getFloat(String columnName) |
单精度浮点 |
Float32 |
getDouble(String columnName) |
双精度浮点 |
Float64 |
getBoolean(String columnName) |
布尔或整数字段(0/1) |
Bool 或数字(0=false, 非0=true) |
getTimestampNanos(String columnName) |
时间戳(以纳秒返回) |
DateTime, DateTime64 |
重要说明:
getString()是最通用的方法,可用于所有类型的数据读取整数类型(Int/UInt)只能使用对应的数值类型 getXxx() 方法读取
大整数类型(Int128/Int256, UInt128/UInt256)和 Decimal 类型必须使用
getString()读写时间戳类型(DateTime, DateTime64 等)在写入时使用
setTimestamp(),读取时使用getTimestampNanos()
1.10.4.2. 各后端数据库支持的数据类型
1.10.4.2.1. InfluxDB 1.x
InfluxDB 1.x 支持的数据类型相对简单:
类型 |
元素类型 |
描述 |
|---|---|---|
Float |
Field 值 |
默认数值类型。IEEE-754 64 位浮点数(不支持 NaN 或 +/- Inf)。示例:1, 1.0, 1.e+78, 1.E+78 |
Integer |
Field 值 |
有符号 64 位整数(-9223372036854775808 到 9223372036854775807)。在数字后加 i 指定整数。示例:1i |
String |
measurement, tag key/value, field key/value |
字符串类型,长度限制 64KB |
Boolean |
Field 值 |
存储 TRUE 或 FALSE 值 |
Timestamp |
时间戳 |
Unix 纳秒时间戳。可通过 InfluxDB API 指定其他精度。最小有效时间戳:-9223372036854775806(1677-09-21T00:12:43.145224194Z),最大有效时间戳:9223372036854775806(2262-04-11T23:47:16.854775806Z) |
类型映射:
Float →
setDouble()/getDouble()Integer →
setLong()/getLong()String →
setString()/getString()Boolean →
setBoolean()/getBoolean()Timestamp →
setTimestamp()/getTimestampNanos()
1.10.4.2.2. ClickHouse
在本项目的统一 API 中,ClickHouse 端仅保证并推荐使用下列常用字段类型(示例建表):
CREATE TABLE example (
id Int32,
int8_val Int8,
int16_val Int16,
int32_val Int32,
int64_val Int64,
uint8_val UInt8,
uint16_val UInt16,
uint32_val UInt32,
uint64_val UInt64,
float32_val Float32,
float64_val Float64,
string_val String,
fixedstring_val FixedString(20),
bool_val Bool,
datetime_val DateTime,
datetime64_val DateTime64(3),
enum8_val Enum8('ready'=1,'running'=2,'finished'=3),
enum16_val Enum16('ready'=1,'running'=2,'finished'=3),
uuid_val UUID,
ipv4_val IPv4,
ipv6_val IPv6
);
说明:
上述类型为统一 API 在 ClickHouse 端经常使用并保证支持的集合;其他 ClickHouse 特殊类型未列入清单的不保证兼容。
Date 类型暂不推荐使用(未列入受支持清单)。
Time 类型不受支持,请使用 DateTime / DateTime64 表示时间戳类字段。
1.10.5. INSERT 自动建表机制
目前 ClickHouse 后端支持自动建表和添加列功能,简化用户的使用流程。(InfluxDB 原生支持自动建表) 本节详细说明 hint 的传递方式、生成的 CREATE TABLE 语句结构,以及列类型的绑定推断机制。
Hint 传递方式
在 INSERT 语句的注释中添加 ClickHouse 专用 hint 子句:
INSERT /*+ TAGS(deviceId, region) FIELDS(temperature, humidity) TIMESTAMP(time)
ENGINE(MergeTree())
PARTITION(toDate(time))
ORDER_BY(time, deviceId)
PRIMARY_KEY(deviceId) */
INTO sensor_data
(time, deviceId, region, temperature, humidity)
VALUES (?, ?, ?, ?, ?);
可用的 hint 子句:
ENGINE(engine_type)- 指定 ClickHouse 表引擎,如MergeTree()、ReplicatedMergeTree()等PARTITION(partition_expr)- 指定分区表达式,如toDate(time)、toYYYYMM(time)或time, deviceIdORDER_BY(column_list)- 指定排序键(必需),支持单个列或多个列,如time或(time, deviceId)PRIMARY_KEY(column_list)- 指定主键(可选),如deviceId
生成的 CREATE TABLE 语句
根据上面的 INSERT 语句,框架会自动生成如下 CREATE TABLE 语句:
CREATE TABLE IF NOT EXISTS `sensor_data` (
`time` DateTime(9),
`deviceId` String,
`region` Nullable(String),
`temperature` Nullable(Float64),
`humidity` Nullable(Float64)
)
ENGINE = MergeTree()
PARTITION BY toDate(`time`)
ORDER BY (`time`, `deviceId`)
PRIMARY KEY `deviceId`
生成的规则说明:
列定义顺序与 INSERT 语句中列的顺序一致
TAG 列(
deviceId、region)和 TIMESTAMP 列(time)声明为非 NULLableFIELD 列(
temperature、humidity)声明为 Nullable如果未指定 ENGINE,默认使用
MergeTree()如果未指定 ORDER_BY,默认使用第一个列作为排序键
PARTITION 支持函数表达式(如
toDate(time))或列名PRIMARY_KEY 可选,未指定时默认与 ORDER_BY 相同
列类型映射机制
框架根据 setXxx() 方法的调用自动推断列类型:
setXxx() 方法 |
ClickHouse 列类型(非 NULLable) |
ClickHouse 列类型(NULLable) |
|---|---|---|
setString(int, String) |
String |
Nullable(String) |
setInt(int, int) |
Int64 |
Nullable(Int64) |
setLong(int, long) |
Int64 |
Nullable(Int64) |
setShort(int, short) |
Int64 |
Nullable(Int64) |
setFloat(int, float) |
Float64 |
Nullable(Float64) |
setDouble(int, double) |
Float64 |
Nullable(Float64) |
setBoolean(int, boolean) |
UInt8 |
Nullable(UInt8) |
setTimestamp(int, long, TimeUnit) |
DateTime(9) |
Nullable(DateTime(9)) |
类型推断示例:
String insertSql =
"INSERT /*+ TAGS(deviceId) FIELDS(value) TIMESTAMP(time) */ INTO sensor_data
(time, deviceId, value) VALUES (?, ?, ?)";
try (TimeSeriesClient client = dataSource.getClient();
TimeSeriesPreparedStatement ps = client.prepareStatement(insertSql)) {
ps.setTimestamp(1, System.currentTimeMillis(), TimeUnit.MILLISECONDS);
// -> time 列类型:DateTime(9)(TIMESTAMP 列,非 NULLable)
ps.setString(2, "device-001");
// -> deviceId 列类型:String(TAG 列,非 NULLable)
ps.setInt(3, 123);
// -> value 列类型:Nullable(Int64)(FIELD 列,NULLable)
ps.executeUpdate();
}
注意事项:
自动建表和添加列功能目前仅对 ClickHouse 后端生效(InfluxDB 本身自动创建)
ALTER TABLE 添加列可能因类型不兼容而失败,此时会抛出异常
多线程并发插入同一表时,使用表级锁保证操作的线程安全
元数据缓存的轻微不一致是可以接受的,仅影响自动 alter 的性能
1.10.6. 异库兼容性说明
本节总结跨不同时序数据库使用时需要注意的兼容性问题。
重要提示:由于底层时序库的实现差异,某些行为在不同数据库间可能不一致。 建议在设计阶段就避免依赖这些差异,确保业务逻辑的跨库一致性。
1.10.6.1. 连接 URL
InfluxDB 1.x:
jdbc:influxdb:host:portClickHouse:
jdbc:clickhouse:host:port
通过 URL scheme 区分后端实现。
1.10.6.2. 数据类型兼容性
不要依赖 getMetaData 中的 ColumnType
由于不同底层时序库的数据类型存储方式存在差异,getMetaData() 返回的 ColumnType 可能不一致。
尤其是布尔类型:ClickHouse 底层使用数字(1/0)存储布尔值,metadata 中只能获取到数值类型而非布尔类型。
示例:
TimeSeriesResultSetMetaData meta = rs.getMetaData();
int type = meta.getColumnType(1); // 跨库场景下,此值可能不一致
// 错误做法:依赖 ColumnType 判断字段类型
if (type == Types.BOOLEAN) { ... } // ClickHouse 中此条件可能不成立
// 正确做法:根据业务语义处理字段值,而非依赖 metadata 的类型
Object value = rs.getObject(1);
if (value instanceof Boolean) {
boolean boolValue = (Boolean) value;
} else if (value instanceof Number) {
// 兼容 ClickHouse 使用数字表示布尔值的场景
boolean boolValue = ((Number) value).intValue() == 1;
}
1.10.6.3. NULL 值行为差异
由于不同时序数据库对 NULL 值的默认行为不一致,框架无法在底层完全抹平这些差异。 强烈建议在应用层设计时避免出现 NULL 值与空字符串混合使用的场景,确保业务逻辑的一致性。
设计建议:
所有字段尽量显式设置值,避免依赖数据库的默认填充行为
字符串类型:统一使用非空字符串,不要混用空字符串和 null
数值类型:如有需要表示”无值”的语义,使用特殊的标记值而非依赖 null
如果必须使用 null,在 ClickHouse 中应将字段声明为
Nullable类型
getXxx() 方法的 NULL 返回行为
当查询结果中某列为 SQL NULL 时,各 getXxx() 方法的返回值:
方法 |
返回值 |
|---|---|
getString() |
null |
getInt() |
0 |
getLong() |
0 |
getShort() |
0 |
getFloat() |
0.0f |
getDouble() |
0.0 |
getBoolean() |
false |
getTimestampNanos() |
0 |
不同数据库的 NULL 行为差异
建议在使用时序数据库的时候,在业务设计之初就不要存在空串与 null 。
由于底层时序库的默认行为不同,对于 INSERT 时不传字段的数据,查询结果存在差异:
场景 |
InfluxDB |
ClickHouse |
|---|---|---|
TAG 不传 |
返回 |
自动填充空串 |
TAG 传入空字符串 |
返回 |
自动填充空串 |
FIELD 不传(非 Nullable) |
返回 |
自动填充默认值(字符串空串、数值 0、布尔 false), |
FIELD 不传(Nullable) |
返回 |
返回 |
FIELD 传入空字符串 |
返回空串 |
返回空串 |
1.10.6.4. API 兼容性说明
由于后端能力差异,部分 API 在不同后端的支持程度不同:
InfluxDB 1.x - 支持运行期切换 database - 支持 retentionPolicy
ClickHouse - 支持运行期切换 database - 不支持 retentionPolicy
建议根据目标后端选择合适的使用方式。
1.10.7. 接口说明
1.10.7.1. TimeSeriesDataSource
方法 |
说明 |
|---|---|
getClient() |
获取时序客户端实例(需显式关闭) |
setUrl(String) |
设置连接 URL |
setDatabase(String) |
设置默认数据库 |
setUsername(String) |
设置用户名 |
setPassword(String) |
设置密码 |
1.10.7.2. TimeSeriesClient
方法 |
说明 |
|---|---|
prepareStatement(String) |
创建预编译 SQL 语句 |
setConsistency(ConsistencyLevel) |
设置一致性级别(部分后端支持) |
setRetentionPolicy(String) |
设置保留策略(部分后端支持) |
setDatabase(String) |
设置当前数据库(部分后端不支持) |
close() |
关闭客户端并释放资源 |
1.10.7.3. TimeSeriesPreparedStatement
方法 |
说明 |
|---|---|
setString(int, String) |
绑定字符串参数(不允许传入 null,传入空字符串需注意各库行为差异) |
setInt(int, int) |
绑定整数参数(索引从 1 开始) |
setLong(int, long) |
绑定长整数参数 |
setShort(int, short) |
绑定短整数参数 |
setFloat(int, float) |
绑定浮点数参数 |
setDouble(int, double) |
绑定双精度浮点数参数 |
setBoolean(int, boolean) |
绑定布尔参数 |
setTimestamp(int, long, TimeUnit) |
绑定时间戳参数(按 UTC 时区处理) |
executeUpdate() |
执行写入类 SQL |
executeQuery() |
执行查询并返回统一结果集 |
addBatch() |
将当前参数加入批量缓存(仅 INSERT 语句支持) |
executeBatch() |
执行批量缓存中的所有语句,返回每条语句的影响行数数组(为1) |
clearBatch() |
清空批量缓存 |
close() |
关闭预编译语句并释放资源 |
重要注意事项:
setString 参数限制: - 不允许传入
null值,否则抛出IllegalArgumentException- 传入空字符串需注意:InfluxDB 查询结果为 null,ClickHouse 查询结果为空字符串 - 详见 异库兼容性说明参数绑定完整性: - SQL 中的每个占位符(
?)都必须进行参数绑定 - 缺少绑定会在执行时抛出IllegalStateException
1.10.7.4. TimeSeriesResultSet
方法 |
说明 |
|---|---|
next() |
将光标移动到下一行 |
getString(String columnName) |
以字符串形式获取指定列的值 |
getInt(String columnName) |
以整数形式获取指定列的值 |
getLong(String columnName) |
以长整数形式获取指定列的值 |
getShort(String columnName) |
以短整数形式获取指定列的值 |
getFloat(String columnName) |
以浮点数形式获取指定列的值 |
getDouble(String columnName) |
以双精度浮点数形式获取指定列的值 |
getBoolean(String columnName) |
以布尔值形式获取指定列的值 |
getTimestampNanos(String columnName) |
以纳秒时间戳形式获取指定列的值(按 UTC 时区返回) |
getMetaData() |
获取结果集的元数据信息 |
wasNull() |
上一个 get 方法返回的列值是否为 null |
close() |
关闭结果集并释放相关资源 |
1.10.7.5. TimeSeriesResultSetMetaData
方法 |
说明 |
|---|---|
getColumnCount() |
获取结果集中的列数 |
getColumnType(int column) |
获取指定列的 SQL 类型(对应 java.sql.Types 中的常量) |
getColumnTypeName(int column) |
获取指定列的数据库特定类型名称 |
getColumnName(int column) |
获取指定列的名称 |
1.10.8. INSERT SQL 规则
所有写入操作必须使用 INSERT SQL 语句
SQL 中必须包含 hint 注释,用于声明 tag / field 元信息
hint 必须位于 INSERT 与 INTO 之间,采用 MySQL 类似的注释风格:
INSERT /*+ TAGS(region) FIELDS(temperature, humidity) TIMESTAMP(time) */ INTO weather (time, region, temperature, humidity) VALUES (?, ?, ?, ?);
hint 用于指导框架正确地写入时序数据库
tag / field 声明内容必须与
TimeSeriesPreparedStatement中绑定的列保持一致未包含正确 hint 的 INSERT 将被视为非法,框架会抛出异常
ClickHouse 专用 hint 子句
对于 ClickHouse 后端,hint 中还支持以下子句,用于指定表结构和分区策略:
子句 |
说明 |
|---|---|
ENGINE(engine_type) |
指定 ClickHouse 的表引擎类型,如 |
PARTITION(partition_expr) |
指定表分区表达式,如 |
ORDER_BY(column_list) |
指定排序键(必需),支持单个列或多个列,如 |
PRIMARY_KEY(column_list) |
指定主键(可选),如 |
示例:完整的 ClickHouse INSERT SQL
INSERT /*+ TAGS(deviceId, region) FIELDS(temperature, humidity) TIMESTAMP(time)
ENGINE(MergeTree())
PARTITION(toDate(time))
ORDER_BY(time, deviceId)
PRIMARY_KEY(deviceId) */
INTO sensor_data
(time, deviceId, region, temperature, humidity)
VALUES (?, ?, ?, ?, ?, ?);
说明:
ENGINE、PARTITION、ORDER_BY、PRIMARY_KEY 子句在 InfluxDB 后端会被忽略
ORDER_BY 是 ClickHouse 必需的子句,如未指定,默认使用时间戳列
PARTITION 建议设置为
toDate(timestamp)或类似函数,按天分区PRIMARY_KEY 可选,未指定时默认与 ORDER_BY 相同