镜像站点 · 本页由第三方 GitHub 只读镜像提供,非 GitHub 官方站点,不接受任何登录或凭据输入。前往 github.com
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
.m2
# MacOS
.DS_Store

Expand Down
3 changes: 2 additions & 1 deletion docs/clickhouse-docs/navigation.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
"integrations/language-clients/java/client",
"integrations/language-clients/java/jdbc",
"integrations/language-clients/java/r2dbc",
"integrations/language-clients/java/date-time-guide"
"integrations/language-clients/java/date-time-guide",
"integrations/language-clients/java/spring-io-integration"
],
"root": "integrations/language-clients/java/index"
}
81 changes: 81 additions & 0 deletions docs/clickhouse-docs/spring-io-integration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
---
sidebarTitle: 'Spring integration'
keywords: ['java', 'jdbc', 'spring', 'spring boot', 'JdbcTemplate', 'RowSet', 'ZonedDateTime']
description: 'Integrating Spring applications with ClickHouse'
slug: /integrations/language-clients/java/spring-jdbc-template
title: 'Integrating Spring with ClickHouse'
doc_type: 'guide'
integration:
- support_level: 'core'
- category: 'language_client'
---

This guide covers different aspects of integrating Spring applications with ClickHouse, including configuration,
data-access APIs, type conversion, and limitations of the Spring abstractions involved.

## `JdbcTemplate` {#jdbc-template}

`JdbcTemplate` is Spring's abstraction for executing JDBC operations and managing their resources. This section focuses
on its read APIs: callbacks, row mappers, convenience query methods, and row sets. Choosing an API matters because each
one hides different logic for iterating, mapping, buffering, and returning data from the driver's `ResultSet`.

### Ways to read data {#jdbc-template-reading-data}

`JdbcTemplate` provides several fetching APIs over the same JDBC driver result:

| API | Interaction with the driver result | Specific behavior or limitation |
| --- | --- | --- |
| `RowCallbackHandler` | Receives the live `ResultSet` at the current row | Spring advances the cursor; the callback does not return a value |
| `ResultSetExtractor` | Receives the complete live `ResultSet` | The extractor controls cursor iteration and constructs the return value |
| `RowMapper` | Receives the live `ResultSet` once per row | Spring collects every mapped object into the returned `List` |
| `queryForObject`, `queryForList`, `queryForMap` | Use Spring's built-in result mapping | The application does not choose which JDBC getters perform the conversion |
| `queryForRowSet` (`SqlRowSet`) | Copies values from the driver result into a `CachedRowSet` | Reads are disconnected from the driver and use the cached Java objects |

Prefer these `query` overloads because they operate on the driver's live `ResultSet` without first copying it into a row
set:

- `RowCallbackHandler` for efficient, independent row-by-row processing.
- `ResultSetExtractor` for full control over iteration and result construction.
- `RowMapper` when returning a list is intentional. Direct access to the JDBC result makes type conversion more
predictable and portable than relying on a particular `CachedRowSet` implementation.

<Warning>
**Timestamp handling with `queryForRowSet()`**

`queryForRowSet()` is different from the other `queryFor*` methods: it reads the entire driver `ResultSet` into a
disconnected `CachedRowSet` before returning. The `SqlRowSet` returned by Spring does not support
`getObject(column, Class)`, so a call such as `getObject("event_time", ZonedDateTime.class)` fails with
`Not supported yet`.

For `DateTime` and `DateTime64`, configure
`jdbc_type_mappings=DateTime64=java.time.ZonedDateTime,DateTime=java.time.ZonedDateTime` in the connection pool's
`data-source-properties`. The cached value can then be read with the untyped
`(ZonedDateTime) rowSet.getObject("event_time")` call. This mapping applies to every `getObject(column)` call on the
connection, not only to row sets. See the
[Date/Time values guide](/integrations/language-clients/java/jdbc_date_time_guide) for details.
</Warning>

The following example uses the recommended callback API. `JdbcTemplate` advances the cursor and invokes the callback
once for each row; the callback should not call `next()`:

```java
public void processSignals(JdbcTemplate jdbcTemplate, long locationId) {
String sql = "SELECT device_id, value, event_time "
+ "FROM iot_signals "
+ "WHERE location_id = ? "
+ "ORDER BY event_time";

jdbcTemplate.query(
sql,
(ResultSet rs) -> {
String deviceId = rs.getString("device_id");
double value = rs.getDouble("value");
ZonedDateTime eventTime = rs.getObject("event_time", ZonedDateTime.class);
// process the row
},
locationId);
}
```

For the complete API and callback contracts, see the
[Spring JDBC documentation](https://docs.spring.io/spring-framework/reference/data-access/jdbc/core.html).
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,15 @@
import org.springframework.jdbc.core.JdbcTemplate;
import org.testcontainers.junit.jupiter.Testcontainers;

import java.sql.ResultSet;
import java.time.Duration;
import java.time.Instant;
import java.time.ZonedDateTime;
import java.time.temporal.ChronoUnit;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

import static org.assertj.core.api.Assertions.assertThat;

Expand Down Expand Up @@ -152,6 +155,42 @@ SELECT max(value)
assertThat(durationSamples).isNotNull().isGreaterThanOrEqualTo(1.0);
}

@Test
void selectDataViaRowSet() {
int eventCount = 100;
UUID locationId = UUID.randomUUID();
Instant start = Instant.now().minusSeconds(60).truncatedTo(ChronoUnit.MILLIS);

for (int i = 0; i < eventCount; i++) {
boolean temperature = i % 2 == 0;
postSignal(
locationId,
"rowset-device-" + (i % 5),
temperature ? "TEMPERATURE" : "HUMIDITY",
i,
temperature ? "C" : "%",
start.plusMillis(i * 10L));
}

AtomicInteger read = new AtomicInteger();
jdbc.query("""
SELECT device_id, signal_type, value, unit, event_time
FROM iot_signals
WHERE location_id = ?
ORDER BY event_time
""", new Object[] {locationId}, (ResultSet rs) -> {
int i = read.getAndIncrement();
boolean temperature = i % 2 == 0;
assertThat(rs.getString("device_id")).isEqualTo("rowset-device-" + (i % 5));
assertThat(rs.getString("signal_type")).isEqualTo(temperature ? "TEMPERATURE" : "HUMIDITY");
assertThat(rs.getDouble("value")).isEqualTo(i);
assertThat(rs.getString("unit")).isEqualTo(temperature ? "C" : "%");
assertThat(rs.getObject("event_time", ZonedDateTime.class).toInstant())
.isEqualTo(start.plusMillis(i * 10L));
});
assertThat(read.get()).isEqualTo(eventCount);
}

@Test
void acceptsReconciliationBatchAndStoresItAsynchronously() throws InterruptedException {
UUID firstId = UUID.randomUUID();
Expand Down
Loading