Mybatis操作Clickhouse數(shù)組的最佳實(shí)踐分享
ClickHouse Array 類(lèi)型概述
ClickHouse 的 Array(T) 數(shù)據(jù)類(lèi)型支持任意有效數(shù)據(jù)類(lèi)型作為元素,包括基本類(lèi)型、嵌套數(shù)組和可空類(lèi)型,關(guān)鍵特性包括:
- 索引從1開(kāi)始:區(qū)別于多數(shù)編程語(yǔ)言的 0 索引機(jī)制
- 自動(dòng)類(lèi)型推斷:選擇最窄兼容類(lèi)型以?xún)?yōu)化存儲(chǔ)
- 嚴(yán)格類(lèi)型檢查:混合不兼容類(lèi)型將導(dǎo)致異常
- NULL值處理:包含 NULL 值時(shí)自動(dòng)轉(zhuǎn)換為 Nullable 類(lèi)型
-- 有效用法
SELECT array(1, 2, 3); -- Array(UInt8)
SELECT array('a', 'b', 'c'); -- Array(String)
SELECT array([1, 2], [3, 4]); -- Array(Array(UInt8))
-- 無(wú)效用法:類(lèi)型不兼容
SELECT array(1, 'a');
-- Error: There is no supertype for types UInt8, String
MyBatis 寫(xiě)入 ClickHouse 數(shù)組的兩種方法比較
JPA 與 Mybatis 是常見(jiàn)的兩種 ORM 框架。JPA 主要為 OLTP 設(shè)計(jì),ClickHouse 是 OLAP 數(shù)據(jù)庫(kù),JPQL 難以表達(dá) ClickHouse 的復(fù)雜分析查詢(xún),而這正好可以發(fā)揮 Mybaits 靈活控制 SQL 的特性。對(duì)于 Clickhouse 的數(shù)組類(lèi)型寫(xiě)入一般有兩種方法:
方法一:使用 ClickHouse array() 函數(shù) + ${} 參數(shù)替換
Mybatis XML 代碼:
insert into xxx_base
(array1)
values (array(${array1Value}))
Java 代碼:
// 手動(dòng)格式化數(shù)組
List<String> userIds = Arrays.asList("user1", "user2", "user3");
String userIdsStr = userIds.stream()
.map(s -> "'" + s.replace("'", "\\'") + "'") // 轉(zhuǎn)義單引號(hào)
.collect(Collectors.joining(","));
// userIdsStr = "'user1','user2','user3'"
// 處理空值
String deviceIdsStr = deviceIds.isEmpty() ? "" :
deviceIds.stream()
.map(s -> "'" + s.replace("'", "\\'") + "'")
.collect(Collectors.joining(","));
方法二:使用自定義 TypeHandler + #{} 參數(shù)綁定
Mybatis XML 代碼:
insert into xxx_base
(array1)
values (#{array1Value,typeHandler=com.test.clickhousemybatisdemo.typehandler.ClickHouseArrayTypeHandler})
Java 代碼:
// 直接使用List對(duì)象
List<String> userIds = Arrays.asList("user1", "user2", "user3");
List<String> deviceIds = new ArrayList<>(); // 空列表也可以直接使用
// TypeHandler會(huì)自動(dòng)處理轉(zhuǎn)換和空值情況
方案對(duì)比分析
| 維度 | array() + ${} | TypeHandler + #{} |
|---|---|---|
| 安全性 | ? SQL注入風(fēng)險(xiǎn) | ? 預(yù)編譯安全,類(lèi)型安全 |
| 可讀性 | ? 需要格式化處理 | ? 直接使用List |
| 可維護(hù)性 | ? 邏輯分散 | ? 邏輯集中 |
| 性能 | ?? 字符串拼接開(kāi)銷(xiāo) | ? 預(yù)編譯緩存 |
得到的結(jié)論是推薦方法二:
- 安全性差異:
${}參數(shù)替換存在 SQL 注入漏洞,#{}預(yù)編譯機(jī)制提供安全保障 - 開(kāi)發(fā)復(fù)雜度:字符串拼接方案需要復(fù)雜的格式化與轉(zhuǎn)義處理,TypeHandler 方案支持直接對(duì)象操作
自定義 TypeHandler 的優(yōu)勢(shì)
MyBatis 內(nèi)置的 TypeHandler 主要針對(duì)關(guān)系型數(shù)據(jù)庫(kù)的標(biāo)準(zhǔn) SQL 類(lèi)型設(shè)計(jì),對(duì)于 ClickHouse 這樣的分析型數(shù)據(jù)庫(kù)的特殊數(shù)據(jù)類(lèi)型支持有限,Java 的List<T>與 ClickHouse 的Array(T)之間缺少直接的類(lèi)型轉(zhuǎn)換機(jī)制。而如果使用 Java String 來(lái)處理又會(huì)帶來(lái)數(shù)據(jù)類(lèi)型頻繁轉(zhuǎn)換的工程問(wèn)題,代碼可讀性與可維護(hù)性都會(huì)受到影響。
ClickHouse JDBC 驅(qū)動(dòng)對(duì)數(shù)組類(lèi)型的處理與傳統(tǒng)關(guān)系型數(shù)據(jù)庫(kù)存在差異:
// 傳統(tǒng)數(shù)據(jù)庫(kù)的數(shù)組處理(如PostgreSQL)
Array sqlArray = connection.createArrayOf("varchar", stringArray);
// ClickHouse需要特殊的類(lèi)型名稱(chēng)映射
Array sqlArray = connection.createArrayOf("String", stringArray); // 注意:"String"而非"varchar"
于是,擴(kuò)展 TypeHandler 實(shí)現(xiàn)處理數(shù)組問(wèn)題就變得有必要:
- 雙向轉(zhuǎn)換:實(shí)現(xiàn) Java
List<T>↔ ClickHouseArray(T)的無(wú)縫轉(zhuǎn)換 - 類(lèi)型安全:確保編譯期和運(yùn)行期的類(lèi)型一致性
- 空值處理:正確處理 null 值和空數(shù)組的邊界情況
- 性能優(yōu)化:避免不必要的字符串拼接和解析開(kāi)銷(xiāo)
擴(kuò)展 TypeHandler 支持 Clickhouse Array
TypeHandler 接口設(shè)計(jì)
Mybatis TypeHandler 的設(shè)計(jì)就是為了緩解 JDBC 與 Java 數(shù)據(jù)類(lèi)型不匹配的問(wèn)題,通過(guò)擴(kuò)展 TypeHandler 可以對(duì)各種數(shù)據(jù)庫(kù)的各種數(shù)據(jù)類(lèi)型予以支持。
TypeHandler 接口很簡(jiǎn)潔,一個(gè)是 setParameter 方法通過(guò) PreparedStatement 為 SQL 語(yǔ)句綁定參數(shù),實(shí)現(xiàn) JDBC 到 Java 的數(shù)據(jù)類(lèi)型轉(zhuǎn)換;另外三個(gè) getResult 重載方法通過(guò) ResultSet 獲取數(shù)據(jù)時(shí),將 Java 轉(zhuǎn)換成 JDBC 數(shù)據(jù)類(lèi)型。
public interface TypeHandler<T> {
void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException;
T getResult(ResultSet rs, String columnName) throws SQLException;
T getResult(ResultSet rs, int columnIndex) throws SQLException;
T getResult(CallableStatement cs, int columnIndex) throws SQLException;
}
BaseTypeHandler 抽象類(lèi)設(shè)計(jì)
BaseTypeHandler 實(shí)現(xiàn) TypeHandler 接口抽象成基類(lèi),setParameter 提取出參數(shù)是否為空的條件判斷,如果為空就會(huì)調(diào)用 PreparedStatement#setNull,如果不為空就會(huì)調(diào)用 setNonNullParameter。前者會(huì)委托給具體的數(shù)據(jù)庫(kù)驅(qū)動(dòng),這里引入的 clickhouse-jdbc 就會(huì)實(shí)現(xiàn) setNull 方法;后者則會(huì)交給具體的 TypeHandler 實(shí)現(xiàn)類(lèi)。
類(lèi)似的,BaseTypeHandler 實(shí)現(xiàn)了 TypeHandler#getResult 后抽象出了 getNullableResult 方法,委托給具體的 TypeHandler 實(shí)現(xiàn)。
public abstract class BaseTypeHandler<T> extends TypeReference<T> implements TypeHandler<T> {
@Override
public void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException {
if (parameter == null) {
if (jdbcType == null) {
...
ps.setNull(i, jdbcType.TYPE_CODE);
...
} else {
...
setNonNullParameter(ps, i, parameter, jdbcType);
...
}
}
@Override
public T getResult(ResultSet rs, String columnName) throws SQLException {
...
return getNullableResult(rs, columnIndex);
...
}
@Override
public T getResult(ResultSet rs, int columnIndex) throws SQLException {
...
return getNullableResult(rs, columnIndex);
...
}
@Override
public T getResult(CallableStatement cs, int columnIndex) throws SQLException {
...
return getNullableResult(rs, columnIndex);
...
}
public abstract void setNonNullParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType)
throws SQLException;
public abstract T getNullableResult(ResultSet rs, String columnName) throws SQLException;
public abstract T getNullableResult(ResultSet rs, int columnIndex) throws SQLException;
public abstract T getNullableResult(CallableStatement cs, int columnIndex) throws SQLException;
}
要想擴(kuò)展簡(jiǎn)單的 TypeHandler 就可以繼承 BaseTypeHandler,實(shí)現(xiàn)設(shè)置非空 Java 數(shù)據(jù)類(lèi)型和獲取非空 JDBC 數(shù)據(jù)類(lèi)型的 4 個(gè)抽象方法。
Mybatis 內(nèi)置了一些常用的 BaseTypeHandler 實(shí)現(xiàn)類(lèi),比如 ArrayTypeHandler(當(dāng)然,這個(gè)指的是 Java 中的基礎(chǔ)類(lèi)型 Array 而不是 List)、ClobTypeHandler、LocalDateTimeTypeHandler 等。
實(shí)現(xiàn) BaseTypeHandler<List<String>>
參考這些內(nèi)置的 BaseTypeHandler,容易實(shí)現(xiàn)支持 Java 的 List 與 ClickHouse 的 Array 的類(lèi)型綁定,首先支持 List<String> 與 Array(String) 的類(lèi)型綁定。
/**
* ClickHouse Array(String) 類(lèi)型處理器
* 處理 Java List<String> 和 ClickHouse Array(String) 之間的轉(zhuǎn)換
*/
@MappedTypes(List.class)
@MappedJdbcTypes(JdbcType.ARRAY)
public class ClickHouseArrayTypeHandler extends BaseTypeHandler<List<String>> {
@Override
public void setNonNullParameter(PreparedStatement ps, int i, List<String> parameter, JdbcType jdbcType) throws SQLException {
if (parameter == null || parameter.isEmpty()) {
ps.setArray(i, null);
} else {
// 將 List<String> 轉(zhuǎn)換為數(shù)組
String[] array = parameter.toArray(new String[0]);
Array sqlArray = ps.getConnection().createArrayOf("String", array);
ps.setArray(i, sqlArray);
}
}
@Override
public List<String> getNullableResult(ResultSet rs, String columnName) throws SQLException {
Array array = rs.getArray(columnName);
return convertArrayToList(array);
}
@Override
public List<String> getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
Array array = rs.getArray(columnIndex);
return convertArrayToList(array);
}
@Override
public List<String> getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
Array array = cs.getArray(columnIndex);
return convertArrayToList(array);
}
/**
* 將 SQL Array 轉(zhuǎn)換為 List<String>
*/
private List<String> convertArrayToList(Array sqlArray) throws SQLException {
if (sqlArray == null) {
return new ArrayList<>();
}
Object[] array = (Object[]) sqlArray.getArray();
if (array == null || array.length == 0) {
return new ArrayList<>();
}
List<String> result = new ArrayList<>();
for (Object item : array) {
if (item != null) {
result.add(item.toString());
}
}
return result;
}
}
實(shí)現(xiàn) BaseTypeHandler<List<T>>
進(jìn)一步的,可以將 Array(String) 擴(kuò)展為支持 Array(T),這樣可以處理 Clickhouse 通用數(shù)組類(lèi)型。
通過(guò)動(dòng)態(tài)類(lèi)型檢測(cè)實(shí)現(xiàn)多數(shù)據(jù)類(lèi)型支持,getSqlTypeName 方法提供 Java 類(lèi)型到 ClickHouse 類(lèi)型的映射機(jī)制:
/**
* ClickHouse Array(T) 類(lèi)型處理器
* 處理 Java List<T> 和 ClickHouse Array(T) 之間的轉(zhuǎn)換
* 支持 String, Integer, Long, Double, Float, Boolean 等基本類(lèi)型
*/
@MappedTypes(List.class)
@MappedJdbcTypes(JdbcType.ARRAY)
public class ClickHouseArrayTypeHandler<T> extends BaseTypeHandler<List<T>> {
@Override
public void setNonNullParameter(PreparedStatement ps, int i, List<T> parameter, JdbcType jdbcType) throws SQLException {
if (parameter == null || parameter.isEmpty()) {
ps.setArray(i, null);
} else {
// 檢測(cè)元素類(lèi)型并創(chuàng)建相應(yīng)的數(shù)組
Object firstElement = parameter.get(0);
String sqlTypeName = getSqlTypeName(firstElement);
Object[] array = parameter.toArray();
Array sqlArray = ps.getConnection().createArrayOf(sqlTypeName, array);
ps.setArray(i, sqlArray);
}
}
@Override
public List<T> getNullableResult(ResultSet rs, String columnName) throws SQLException {
Array array = rs.getArray(columnName);
return convertArrayToList(array);
}
@Override
public List<T> getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
Array array = rs.getArray(columnIndex);
return convertArrayToList(array);
}
@Override
public List<T> getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
Array array = cs.getArray(columnIndex);
return convertArrayToList(array);
}
/**
* 將 SQL Array 轉(zhuǎn)換為 List<T>
*/
@SuppressWarnings("unchecked")
private List<T> convertArrayToList(Array sqlArray) throws SQLException {
if (sqlArray == null) {
return new ArrayList<>();
}
Object[] array = (Object[]) sqlArray.getArray();
if (array == null || array.length == 0) {
return new ArrayList<>();
}
List<T> result = new ArrayList<>();
for (Object item : array) {
if (item != null) {
result.add((T) convertToTargetType(item));
}
}
return result;
}
/**
* 根據(jù) Java 對(duì)象類(lèi)型獲取對(duì)應(yīng)的 SQL 類(lèi)型名稱(chēng)
*/
private String getSqlTypeName(Object obj) {
if (obj instanceof String) {
return "String";
} else if (obj instanceof Integer) {
return "Int32";
} else if (obj instanceof Long) {
return "Int64";
} else if (obj instanceof Double) {
return "Float64";
} else if (obj instanceof Float) {
return "Float32";
} else if (obj instanceof Boolean) {
return "UInt8";
} else {
// 默認(rèn)轉(zhuǎn)換為字符串
return "String";
}
}
/**
* 將對(duì)象轉(zhuǎn)換為目標(biāo)類(lèi)型
*/
private Object convertToTargetType(Object obj) {
// 對(duì)于基本類(lèi)型,直接返回
if (obj instanceof String || obj instanceof Integer || obj instanceof Long ||
obj instanceof Double || obj instanceof Float || obj instanceof Boolean) {
return obj;
}
// 對(duì)于其他類(lèi)型,轉(zhuǎn)換為字符串
return obj.toString();
}
}
Docker 容器化測(cè)試
這里引入 TestContainers 實(shí)現(xiàn)自動(dòng)化測(cè)試,避免安裝 Clickhouse 的繁瑣、避免使用替身數(shù)據(jù)庫(kù)無(wú)法還原真實(shí)依賴(lài),確保 MyBatis 與ClickHouse 數(shù)組操作的可靠性。
環(huán)境配置
Docker Compose 配置(docker-compose.yml):
version: '3.8'
services:
clickhouse:
image: clickhouse/clickhouse-server:latest
ports:
- "8123:8123"
- "9000:9000"
environment:
CLICKHOUSE_DB: test_db
CLICKHOUSE_USER: test_user
CLICKHOUSE_PASSWORD: test_password
healthcheck:
test: ["CMD", "wget", "--spider", "http://localhost:8123/ping"]
interval: 30s
timeout: 10s
Maven 依賴(lài):
<dependencies>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
集成測(cè)試實(shí)現(xiàn)
測(cè)試類(lèi)核心實(shí)現(xiàn):
@SpringBootTest
@ActiveProfiles("test")
@Testcontainers
class ClickHouseIntegrationTest {
@Container
static GenericContainer<?> clickhouseContainer = new GenericContainer<>(
DockerImageName.parse("clickhouse/clickhouse-server:latest"))
.withExposedPorts(8123, 9000)
.withEnv("CLICKHOUSE_DB", "test_db")
.withEnv("CLICKHOUSE_USER", "test_user")
.withEnv("CLICKHOUSE_PASSWORD", "test_password")
.waitingFor(Wait.forHttp("/ping").forPort(8123));
@Autowired
private xxxMapper xxxMapper;
@Autowired
private JdbcTemplate jdbcTemplate;
@DynamicPropertySource
static void configureProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", () ->
"jdbc:clickhouse://localhost:" + clickhouseContainer.getMappedPort(8123) + "/test_db");
registry.add("spring.datasource.username", () -> "test_user");
registry.add("spring.datasource.password", () -> "test_password");
}
@BeforeEach
void initializeDatabase() {
jdbcTemplate.execute("CREATE DATABASE IF NOT EXISTS test_db");
jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS xxx_base (" +
"uuid String, name String, xxx_type Array(String), userIds Array(String), " +
"deviceIds Array(String)) ENGINE = MergeTree() ORDER BY uuid");
}
}
核心測(cè)試用例
容器狀態(tài)驗(yàn)證:
@Test
void testContainerIsRunning() {
assertTrue(clickhouseContainer.isRunning());
}
數(shù)組 CRUD 操作測(cè)試:
@Test
void testArrayInsertAndQuery() {
int result = xxxMapper.insert(testxxx);
assertEquals(1, result);
List<xxxBase> list = xxxMapper.selectByPage(0, 10);
assertNotNull(list);
xxxBase found = list.stream()
.filter(a -> test.getUuid().equals(a.getUuid()))
.findFirst().orElse(null);
assertNotNull(found.getUserIds());
}
執(zhí)行測(cè)試
命令行執(zhí)行:
# 使用TestContainers自動(dòng)化測(cè)試 mvn test -Dtest=ClickHouseIntegrationTest # 或使用Docker Compose手動(dòng)環(huán)境 docker-compose up -d && mvn test
測(cè)試配置(application-test.properties):
mybatis.type-handlers-package=com.test.clickhousemybatisdemo.typehandler logging.level.com.test.clickhousemybatisdemo=DEBUG
驗(yàn)證結(jié)果
容器化讀寫(xiě)測(cè)試驗(yàn)證 TypeHandler 實(shí)現(xiàn)了 Java List<T> 與 Clickhouse Array(T) 的映射關(guān)系。

以上就是Mybatis操作Clickhouse數(shù)組的最佳實(shí)踐分享的詳細(xì)內(nèi)容,更多關(guān)于Mybatis操作Clickhouse數(shù)組的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Java責(zé)任鏈模式的實(shí)現(xiàn)方法詳解
責(zé)任鏈模式是一種設(shè)計(jì)模式,在責(zé)任鏈模式里,很多對(duì)象由每一個(gè)對(duì)象對(duì)其下家的引用而連接起來(lái)形成一條鏈,這篇文章主要介紹了Java責(zé)任鏈模式實(shí)現(xiàn)方法的相關(guān)資料,需要的朋友可以參考下2025-07-07
使用SpringBoot Actuator監(jiān)控應(yīng)用示例
Actuator是Spring Boot提供的對(duì)應(yīng)用系統(tǒng)的自省和監(jiān)控的集成功能,可以對(duì)應(yīng)用系統(tǒng)進(jìn)行配置查看、相關(guān)功能統(tǒng)計(jì)等。這篇文章主要介紹了使用SpringBoot Actuator監(jiān)控應(yīng),有興趣的可以了解一下2018-05-05
Java使用線(xiàn)程池批量處理數(shù)據(jù)操作具體流程
這篇文章主要給大家介紹了關(guān)于Java使用線(xiàn)程池批量處理數(shù)據(jù)操作的相關(guān)資料,Java多線(xiàn)程編程中線(xiàn)程池是一個(gè)非常重要的概念,線(xiàn)程池可以提高線(xiàn)程的復(fù)用率和任務(wù)調(diào)度的效率,尤其是當(dāng)需要查詢(xún)大批量數(shù)據(jù)時(shí),需要的朋友可以參考下2023-06-06
springboot實(shí)現(xiàn)瀏覽器截屏并添加文字
大家好,本篇文章主要講的是springboot實(shí)現(xiàn)瀏覽器截屏并添加文字,感興趣的同學(xué)趕快來(lái)看一看吧,對(duì)你有幫助的話(huà)記得收藏一下2022-02-02
基于Java并發(fā)容器ConcurrentHashMap#put方法解析
下面小編就為大家?guī)?lái)一篇基于Java并發(fā)容器ConcurrentHashMap#put方法解析。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-06-06
Java多個(gè)線(xiàn)程同時(shí)執(zhí)行的方法
這篇文章主要介紹了Java多線(xiàn)程處理文件詳解與代碼示例,通過(guò)本文的介紹和代碼示例,我們了解了如何使用Java多線(xiàn)程來(lái)處理文件,使用多線(xiàn)程技術(shù)可以顯著提高文件處理的效率,特別是對(duì)于大量文件的處理任務(wù),需要的朋友可以參考下2024-12-12
Spring?Boot3整合OAuth2實(shí)現(xiàn)第三方登錄功能詳細(xì)示例
OAuth是一個(gè)關(guān)于授權(quán)的開(kāi)放網(wǎng)絡(luò)標(biāo)準(zhǔn),在全世界得到廣泛應(yīng)用,目前的版本是2.0版,這篇文章主要介紹了Spring?Boot3整合OAuth2實(shí)現(xiàn)第三方登錄功能的相關(guān)資料,需要的朋友可以參考下2025-06-06
Java多態(tài)中動(dòng)態(tài)綁定原理解析
這篇文章主要介紹了Java多態(tài)中動(dòng)態(tài)綁定原理解析,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-12-12
Spring學(xué)習(xí)筆記3之消息隊(duì)列(rabbitmq)發(fā)送郵件功能
這篇文章主要介紹了Spring學(xué)習(xí)筆記3之消息隊(duì)列(rabbitmq)發(fā)送郵件功能的相關(guān)資料,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友可以參考下2016-07-07

