Java 2023-12-04 328

MyBatis读取ClickHouse Array类型字段

老张

资深Java开发工程师

ClickHouse作为一款高性能的列式数据库,支持Array类型字段,但在Java中通过MyBatis读取这类字段需要特殊处理。本文将详细介绍如何实现这一功能。

为什么需要特殊处理

ClickHouse的Array类型字段在Java中读取时面临以下挑战:

  • JDBC标准中没有直接对应Array类型的处理方式
  • MyBatis默认不支持ClickHouse的Array类型转换
  • 不同基础类型(Int, Float, String等)需要不同的处理逻辑
  • 多数据源环境下需要兼容性处理

解决方案概述

我们的解决方案主要包括以下步骤:

  1. 定义ClickHouse表结构
  2. 创建Java实体类
  3. 配置MyBatis映射文件
  4. 实现自定义TypeHandler
  5. 注册TypeHandler

详细实现

1. ClickHouse表结构定义

CREATE TABLE student_scores(
    id Int32,
    cursor_name Array(String),           -- 字符串数组,存储科目名称
    cursor_scores Array(Float64),        -- 浮点数数组,存储科目成绩
    cursor_ranking Array(UInt32)         -- 整数数组,存储科目排名
) ENGINE = MergeTree()
ORDER BY id;

2. Java实体类定义

public class StudentScores {
    private Integer id;
    private ArrayList<String> courseName;
    private ArrayList<Float> courseScore;
    private ArrayList<Integer> courseRanking;

    // Getter和Setter方法
    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public ArrayList<String> getCourseName() {
        return courseName;
    }

    public void setCourseName(ArrayList<String> courseName) {
        this.courseName = courseName;
    }

    public ArrayList<Float> getCourseScore() {
        return courseScore;
    }

    public void setCourseScore(ArrayList<Float> courseScore) {
        this.courseScore = courseScore;
    }

    public ArrayList<Integer> getCourseRanking() {
        return courseRanking;
    }

    public void setCourseRanking(ArrayList<Integer> courseRanking) {
        this.courseRanking = courseRanking;
    }
}

3. MyBatis映射文件配置

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.clickhouse.mapper.StudentScoresMapper">
    <resultMap id="StudentScoresMap" type="com.demo.entity.StudentScores">
        <id property="id" column="id"/>
        <result property="courseName" column="cursor_name"
                jdbcType="ARRAY" typeHandler="com.clickhouse.handler.ClickHouseArrayStringHandler"/>
        <result property="courseScore" column="cursor_scores"
                jdbcType="ARRAY" typeHandler="com.clickhouse.handler.ClickHouseArrayFloatHandler"/>
        <result property="courseRanking" column="cursor_ranking"
                jdbcType="ARRAY" typeHandler="com.clickhouse.handler.ClickHouseArrayIntegerHandler"/>
    </resultMap>

    <select id="getStudentScoresById" resultMap="StudentScoresMap">
        SELECT id, cursor_name, cursor_scores, cursor_ranking
        FROM student_scores
        WHERE id=#{id}
    </select>
</mapper>

4. 自定义TypeHandler实现

以String数组处理为例:

public class ClickHouseArrayStringHandler extends BaseTypeHandler<List<String>> {
    @Override
    public void setNonNullParameter(PreparedStatement ps, int i,
            List<String> parameter, JdbcType jdbcType) throws SQLException {
        ps.setObject(i, parameter.toArray(new String[0]));
    }

    @Override
    public List<String> getNullableResult(ResultSet rs, String columnName)
            throws SQLException {
        return parse(rs.getObject(columnName));
    }

    @Override
    public List<String> getNullableResult(ResultSet rs, int columnIndex)
            throws SQLException {
        return parse(rs.getObject(columnIndex));
    }

    @Override
    public List<String> getNullableResult(CallableStatement cs, int columnIndex)
            throws SQLException {
        return parse(cs.getObject(columnIndex));
    }

    private List<String> parse(Object obj) {
        if (obj == null) return null;

        // 处理ClickHouseArray类型
        if (obj instanceof ClickHouseArray) {
            try {
                Object array = ((ClickHouseArray) obj).getArray();
                List<String> result = new ArrayList<>();
                for (int i = 0; i < Array.getLength(array); i++) {
                    result.add(Array.get(array, i).toString());
                }
                return result;
            } catch (SQLException e) {
                throw new RuntimeException("解析ClickHouseArray失败", e);
            }
        }

        // 处理普通数组
        if (obj instanceof Object[]) {
            return Arrays.stream((Object[]) obj)
                    .map(String::valueOf)
                    .collect(Collectors.toList());
        }

        // 处理字符串形式
        String str = obj.toString().trim();
        if (str.startsWith("[") && str.endsWith("]")) {
            str = str.substring(1, str.length() - 1);
        }
        if (str.isEmpty()) return new ArrayList<>();

        return Arrays.stream(str.split(","))
                .map(String::trim)
                .map(s -> s.replaceAll("^'|'$", ""))
                .collect(Collectors.toList());
    }
}

5. 注册TypeHandler

在application.yml中配置:

clickhouse:
  mybatis:
    mapper-locations: classpath*:/mapper/clickhouse/*.xml
    type-handlers-package: com.clickhouse.handler

测试结果

通过上述配置,我们可以正确读取ClickHouse中的Array类型字段:

{
    "id": 1,
    "courseName": ["语文", "数学", "英语"],
    "courseScore": [92.5, 88.0, 95.5],
    "courseRanking": [2, 5, 1]
}

性能优化建议

1. 批量处理

对于大量数据查询,建议使用批量查询接口:

public interface StudentScoresMapper {
    List<StudentScores> getStudentScoresByIds(@Param("ids") List<Integer> ids);
}

2. 缓存TypeHandler实例

TypeHandler可以设计为无状态,建议在Spring中配置为单例:

@Configuration
public class MyBatisConfig {
    @Bean
    public ClickHouseArrayStringHandler clickHouseArrayStringHandler() {
        return new ClickHouseArrayStringHandler();
    }
}

3. 使用连接池

配置ClickHouse连接池提高性能:

spring:
  datasource:
    clickhouse:
      url: jdbc:clickhouse://localhost:8123/default
      username: default
      password: ""
      driver-class-name: ru.yandex.clickhouse.ClickHouseDriver
      hikari:
        maximum-pool-size: 10
        minimum-idle: 5
        connection-timeout: 30000

常见问题解决

1. 类型转换异常

确保TypeHandler与字段类型匹配:

  • String数组使用ClickHouseArrayStringHandler
  • Float数组使用ClickHouseArrayFloatHandler
  • Integer数组使用ClickHouseArrayIntegerHandler

2. 空值处理

在TypeHandler中完善null值处理逻辑:

private List<String> parse(Object obj) {
    if (obj == null) return Collections.emptyList();
    // 其他处理逻辑
}

3. 多数据源配置

在多数据源环境下,需要为ClickHouse单独配置MyBatis:

@Configuration
@MapperScan(basePackages = "com.clickhouse.mapper",
        sqlSessionFactoryRef = "clickHouseSqlSessionFactory")
public class ClickHouseDataSourceConfig {

    @Bean
    @ConfigurationProperties("spring.datasource.clickhouse")
    public DataSource clickHouseDataSource() {
        return DataSourceBuilder.create().build();
    }

    @Bean
    public SqlSessionFactory clickHouseSqlSessionFactory(
            @Qualifier("clickHouseDataSource") DataSource dataSource) throws Exception {
        SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
        sessionFactory.setDataSource(dataSource);
        sessionFactory.setTypeHandlersPackage("com.clickhouse.handler");
        return sessionFactory.getObject();
    }
}

总结

本文详细介绍了通过MyBatis读取ClickHouse Array类型字段的完整方案:

  1. 定义对应的Java实体类和数据库表结构
  2. 配置MyBatis映射文件并指定TypeHandler
  3. 实现自定义TypeHandler处理不同类型数组
  4. 注册TypeHandler并配置多数据源
  5. 优化查询性能和异常处理

这套方案同样适用于其他支持Array类型的数据库,如PostgreSQL等,只需调整TypeHandler的具体实现即可。

分享: