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
66 changes: 53 additions & 13 deletions examples/demo-service/README.md
Original file line number Diff line number Diff line change
@@ -1,29 +1,69 @@
# ClickHouse-Java Demo Service

## About
This is an example of a Spring Boot application using different ClickHouse-Java clients and features.
This is an example of a Spring Boot service using ClickHouse client directly and via JPA.
Example is an application that requires ClickHouse DB running externally. It can be a Docker or
ClickHouse Cloud instance.

## How to Run

## Usage
### Initialize DB
Set up a ClickHouse instance.

This example requires an instance of ClickHouse running locally on in remote server.
Application uses `system.numbers` table to generate dataset of any size. It is very convenient because:
- data is already there
- server generates data as if it was a real table
- no need to change schema or create tables
Example of running with Docker:
```shell
docker run -d --name demo-service-db -e CLICKHOUSE_USER=default -e CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT=1 -e CLICKHOUSE_PASSWORD=secret\
-p 8123:8123 clickhouse/clickhouse-server

To run
docker ps
# output should be like:
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
2abfefc40a84 clickhouse/clickhouse-server "/entrypoint.sh" 4 seconds ago Up 2 seconds 9000/tcp, 0.0.0.0:8123->8123/tcp, :::8123->8123/tcp, 9009/tcp demo-service-db
```

Example how to run client:
```shell
docker exec -it demo-service-db clickhouse-client
```

Create table (needed for JPA example):
```
CREATE TABLE ui_events
(
`id` String,
`timestamp` DateTime64,
`event_name` String,
`tags` Array(String)
)
ENGINE = MergeTree
ORDER BY timestamp
```

### Run Demo-Service

```bash
```shell
./gradlew bootRun
```

To test
### Interact with API

#### Direct Client

```bash
To read `limit` number of rows from `system.numbers`:
```shell
curl http://localhost:8080/direct/dataset/0?limit=100000
```

## Features
#### JPA

To insert some data:
```shell
curl -v -X POST -H "Content-Type: application/json" \
-d '{"id": "4NAD7B8HH1", "timestamp": "2025-04-07T14:30:00.000Z", "eventName": "Login", "tags": ["security", "activity"]}'\
http://localhost:8080/events/ui_events
```

- [x] Client V2 New Implementation
To fetch inserted data:
```shell
curl -v -X GET http://localhost:8080/events/ui_events
```
22 changes: 13 additions & 9 deletions examples/demo-service/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
plugins {
java
id("org.springframework.boot") version "3.2.8"
id("io.spring.dependency-management") version "1.1.6"
id("org.springframework.boot") version "3.4.4"
id("io.spring.dependency-management") version "1.1.7"
}

group = "com.clickhouse"
Expand Down Expand Up @@ -29,22 +29,26 @@ val ch_java_client_version: String by extra

dependencies {

// -- clickhouse dependencies
// Main dependency
implementation("com.clickhouse:client-v2:${ch_java_client_version}-SNAPSHOT:all") // local or nightly build
// Add this if working with client directly (not JDBC)
// implementation("com.clickhouse:client-v2:${ch_java_client_version}-SNAPSHOT:all") // local or nightly build
// implementation("com.clickhouse:client-v2:${ch_java_client_version}:all") // release version

// -- application dependencies
// OR this if working with JDBC (or both)
implementation("com.clickhouse:clickhouse-jdbc:${ch_java_client_version}-SNAPSHOT:all") // local or nightly build
// implementation("com.clickhouse:clickhouse-jdbc:${ch_java_client_version}:all") // release version

// Other dependencies
implementation("org.springframework.boot:spring-boot-starter-actuator")
implementation("org.springframework.boot:spring-boot-starter-web")

// To enable JPA
implementation("org.springframework.boot:spring-boot-starter-data-jpa")

compileOnly("org.projectlombok:lombok")
annotationProcessor("org.projectlombok:lombok")

implementation("io.micrometer:micrometer-core:1.14.3")




// -- test dependencies
testImplementation("org.springframework.boot:spring-boot-starter-test")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,20 @@


import com.clickhouse.client.api.Client;
import io.micrometer.core.instrument.logging.LoggingMeterRegistry;
import io.micrometer.core.instrument.MeterRegistry;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.transaction.annotation.EnableTransactionManagement;

@Configuration
@EnableJpaRepositories
@EnableTransactionManagement
public class DbConfiguration {

@Bean
public Client chDirectClient(LoggingMeterRegistry loggingMeterRegistry, @Value("${db.url}") String dbUrl, @Value("${db.user}") String dbUser,
public Client chDirectClient(MeterRegistry meterRegistry, @Value("${db.url}") String dbUrl, @Value("${db.user}") String dbUser,
@Value("${db.pass}") String dbPassword) {
return new Client.Builder()
.addEndpoint(dbUrl)
Expand All @@ -27,7 +31,9 @@ public Client chDirectClient(LoggingMeterRegistry loggingMeterRegistry, @Value("
.setSocketSndbuf(500_000)
.setClientNetworkBufferSize(500_000)
.allowBinaryReaderToReuseBuffers(true) // using buffer pool for binary reader
.registerClientMetrics(loggingMeterRegistry, "clickhouse-client-metrics")
.registerClientMetrics(meterRegistry, "clickhouse-client-metrics")
.build();
}

// JPA configured via application.properties
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package com.clickhouse.demo_service;

import com.clickhouse.demo_service.data.UIEvent;
import com.clickhouse.demo_service.jpa.UIEventsDbRepository;
import jakarta.annotation.PostConstruct;
import jakarta.persistence.EntityManager;
import jakarta.transaction.Transactional;
import lombok.extern.java.Log;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.support.TransactionTemplate;
import org.springframework.web.bind.annotation.*;

import java.util.Collection;

/**
* Class demonstrates usage of ClickHouse JDBC driver with JPA
*
*/
@RestController
@RequestMapping("/events/")
@Log
public class JPAInsertController {

@Autowired
private UIEventsDbRepository uiEvents;

@Autowired
private PlatformTransactionManager transactionManager;

@Autowired
private EntityManager entityManager;

private TransactionTemplate transactionTemplate;

@PostConstruct
public void setUp() {
transactionTemplate = new TransactionTemplate(transactionManager);
}

@PostMapping("/ui_events")
@Transactional(Transactional.TxType.NOT_SUPPORTED)
public void addUIEvent(@RequestBody UIEvent event) {
// do input validation and conversion
transactionTemplate.executeWithoutResult(transactionStatus -> {
entityManager.persist(event);
});
}

@GetMapping("/ui_events")
public Collection<UIEvent> lastUIEvents() {
return uiEvents.findAll();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@
import io.micrometer.core.instrument.Clock;
import io.micrometer.core.instrument.logging.LoggingMeterRegistry;
import io.micrometer.core.instrument.logging.LoggingRegistryConfig;
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;

import java.time.Duration;
Expand Down Expand Up @@ -32,10 +37,19 @@ public Duration step() {

// Create the LoggingMeterRegistry bean.
@Bean
@ConditionalOnProperty("app.log_metrics")
public LoggingMeterRegistry loggingMeterRegistry(LoggingRegistryConfig config) {
LoggingMeterRegistry registry = new LoggingMeterRegistry(config, Clock.SYSTEM);
// Start the registry’s internal scheduler so that metrics are published periodically.
registry.start();
return registry;
}

@Bean
@ConditionalOnProperty(value = "app.log_metrics", havingValue = "false")
public SimpleMeterRegistry simpleMeterRegistry() {
SimpleMeterRegistry registry = new SimpleMeterRegistry();

return registry;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,7 @@
import jakarta.annotation.PostConstruct;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.java.Log;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.*;

import java.util.ArrayList;
import java.util.LinkedList;
Expand All @@ -31,23 +27,22 @@
import java.util.stream.Collectors;

/**
* Dataset API:
* - /direct/dataset/0/?limit=N - uses client v2 directly to fetch N rows from a virtual dataset.
*
* <p>Example: {@code curl -v http://localhost:8080/direct/dataset/0?limit=10}</p>
* Class demonstrates using ClickHouse client directly from a service.
* It avoids JDBC overhead and much easier to use.
* Data may be streamed from database directly to the service response.
*/
@RestController
@RequestMapping("/")
@RequestMapping("/dataset")
@Log
public class DatasetController {
public class QueryController {

private final Client chDirectClient;

private static final int MAX_LIMIT = 100_000;

private BasicObjectsPool<ObjectsPreparedCollection<VirtualDatasetRecord>> pool;

public DatasetController(Client chDirectClient) {
public QueryController(Client chDirectClient) {
this.chDirectClient = chDirectClient;
}

Expand Down Expand Up @@ -95,7 +90,7 @@ VirtualDatasetRecord create() {
* @param limit
* @return
*/
@GetMapping("/dataset/reader")
@GetMapping("/reader")
public List<VirtualDatasetRecord> directDatasetFetch(@RequestParam(name = "limit", required = false) Integer limit) {
limit = limit == null ? 100 : limit;

Expand Down Expand Up @@ -140,7 +135,7 @@ public List<VirtualDatasetRecord> directDatasetFetch(@RequestParam(name = "limit
* @param httpResp
* @param limit
*/
@GetMapping("/dataset/json_each_row_in_and_out")
@GetMapping("/json_each_row_in_and_out")
@ResponseBody
public void directDataFetchJSONEachRow(HttpServletResponse httpResp, @RequestParam(name = "limit", required = false) Integer limit) {
limit = limit == null ? 100 : limit;
Expand Down Expand Up @@ -184,7 +179,7 @@ public void directDataFetchJSONEachRow(HttpServletResponse httpResp, @RequestPar
* @param limit
* @return
*/
@GetMapping("/dataset/read_to_pojo")
@GetMapping("/read_to_pojo")
public CalculationResult directDatasetReadToPojo(@RequestParam(name = "limit", required = false) Integer limit,
@RequestParam(name = "cache", required = false) Boolean cache) {
limit = limit == null ? 100 : limit;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.clickhouse.demo_service.data;

import com.clickhouse.demo_service.jpa.ClickHouseStringArrayType;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.Data;
import org.hibernate.annotations.Array;
import org.hibernate.annotations.JdbcType;
import org.hibernate.annotations.Type;
import org.hibernate.type.descriptor.jdbc.ArrayJdbcType;

import java.sql.JDBCType;
import java.sql.Timestamp;
import java.util.Collection;
import java.util.List;

@Entity
@Data
@Table(name = "ui_events")
public class UIEvent {

@Id
private String id;

private Timestamp timestamp;

private String eventName;

@JdbcType(ArrayJdbcType.class)
@Type(ClickHouseStringArrayType.class)
private Collection<String> tags;
}
Loading
Loading