1. ​前言

Clickhouse数据库支持定义字段类型为Array,Array中可以定义基本类型为String,int等等,那么我们如何读取它呢,特别是我们在使用Java读取数据时,有写程序里面既有Mysql数据源,又有Clickhouse数据源,也有可能还有Redis,MangoDB等等,这种情况下,可能使用的连接串都是类似的,都是通过JDBC来连接这些数据源的。比如我们数据源连接配置如下:

2.Clickhouse中表数据结构定义

假设我们在clickhouse中有一张表,定义表结构如下(仅仅举例说明Array类型数据如何读取,请不要深究逻辑):

CREATE TABLE student_scores(
    id Int32,
    cursor_name Array(String),           -- 字符串数组,用来存储科目,如 ['a', 'b', 'c']
    cursor_scores Array(Float64),        -- 浮点数数组,用来存储成绩,如 [95.5, 88.0, 91.2]
    cursor_ranking(UInt32)               -- 整数数组,用来代表单科分数排名,如 [1, 2, 3]
) ENGINE = MergeTree()
ORDER BY id;
3.Java代码中实体定义

我们在Java代码中就会有一个对应的实体类 StudentScores:

public class StudentScores {

    private Integer id;

    private ArrayList<String> courseName;

    private ArrayList<Float> courseScore;

    private ArrayList<Integer> courseRanking;

    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;
    }
}
4. Mybatis中mapper.xml配置

我们如何用mybatis像读取这几个array字段呢?我们定义mapper.xml如下:

<?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="getUserTagsByUserId" resultMap="AdsUserTagMap">
        select id, cursor_name, language_list, cursor_scores,cursor_ranking from student_scores where id=#{id}
    </select>
</mapper>

这时候细心的朋友就会发现,我们在定义resultMap的时候,针对数据类型为Array的字段,我们在字段mapping时除了定义了jdbcType,还指定了typeHandler,为什么呢?应为Array不是基础数据类型,它可以是Array<String>也可以是Array<Integer>说白了就是一个容器,你指定哪种数据类型,它就往里面装指定的数据类型。如果你不告诉它里面到底是什么类型,那么对不起,不认识,只能报错给你看了

5.自定义typeHandler来解析clickhouse数据库定义表中的Array字段

那么这个typeHandler是什么,为什么需要它?其实很简单,我们默认的Handler里面好像没有能处理jdbcType为Array类型的而且这个Array里面还装的不一样的数据。所以需要我们自己定义,其实也不难,实现以下对应的接口就可以了。话不多说,直接开干,上菜(上代码),我们就贴一个Array<String>来看看,其它的其实大同小异,只要在parse方法把object对象转换为对应的数据类型就可以了:

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

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

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

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

    private List<String> parse(Object obj) {
        if (obj == null) return null;
        if (obj instanceof Object[]) {
            return Stream.of((Object[]) obj).map(String::valueOf).collect(Collectors.toList());
        }
        if(obj instanceof ClickHouseArray){
            try {
                Object real = ((ClickHouseArray) obj).getArray();
                List<String> out = new ArrayList<>();
                for (int i = 0; i < Array.getLength(real); i++) {
                    out.add(Array.get(real, i).toString());
                }
                return out;
            }catch (SQLException e){
                log.error("ClickHouseArrayStringHandler parse ClickHouseArray error", e);
            }
        }
        String s = obj.toString().trim();
        if (s.startsWith("[") && s.endsWith("]")) s = s.substring(1, s.length() - 1);
        if (s.isEmpty()) return new ArrayList<>();
        return Arrays.stream(s.split(","))
                .map(String::trim)
                .map(x -> x.replaceAll("^'|'$", "")) // 去除可能的引号
                .collect(Collectors.toList());
    }
}
6. 在程序中配置注册handler

要想使用自定义的typeHandler,还得在application配置文件中注册自定义的handler(只需要填写包名即可,不需要写具体的handler的类,这样可以扫描整个包下面的handler),大概的代码如下:

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

7. 测试结果

至此,读取clickhouse里面的Array字段的代码就基本完成了,我们随意写个controller,然后访问一下,可以得到结果如下:

{
    "id": 1,
    "cursorName": ["语文", "英语", "数学"],
    "cursorScore": [92.5,98,86.5],
    "cursorRanking": [2,4,11]
}

更多推荐