自定义 ResultSet 结果集


一、模式介绍

1. 基本介绍

我们都知道在 JavaJDBC 中定义了一套标准规范,例如 ResultSet 用于接口语句的结果集,如果点开 ResultSet 内你可以看到其中定义了一系列标准接口,仅接口代码行数即达到了四千多行,当然其中一大半是注释文档,但数量依然客观。
ResultSet接口类

如果你想要定义自己的查询结构集,实现也相当简单,自定义你的结果集类并实现 ResultSet 接口,并按照按照标准重写其中接口即可。

但通过实现 ResultSet 接口会带来一个问题,你需要重写其中的上百个标准接口,当然你可以仅定义一个空实现,但仍然会造成大量无用代码。当然这是基于仅需要 ResultSet 部分功能的情况下,如果你要设计完善的数据规范,还是需要通过实现 ResultSet 接口来实现。

假如我们只想要实现一个类似 ResultSet 中通过游标逐行读取的方式,最简单的方式的方式即自定义一个结果集规范,只定义你所需的接口,从而提高代码的简洁度。

本文的重点教你如何实现一个自己的 ResultSet 规范。

2. 接口规范

新建接口类 AbstractResultSet 用于定义我们自己的规范。

首先根据需求确定接口中所需要的定义的方法,如我想要与 ResultSet 中一样能够通过 next() 逐行读取数据,同时通过 getString() 读取属性字段,那么我的接口类则只需要定义下述三个方法。

这里定义的三个接口方法与 ResultSet 中的接口方法是一致的,只是截取过来而已。

public interface AbstractResultSet extends AutoCloseable {

    boolean next() throws SQLException;

    // 通过下标访问
    String getString(int columnIndex) throws SQLException;

    // 通过名称访问
    String getString(String columnLabel) throws SQLException;

}

通过上述方式定义使得代码更为简洁,但也存在弊端即 AbstractResultSet 并不继承于 ResultSet 则也就无法通过其定义对象实例类型。如需要保证该特性,可以将 AbstractResultSet 同时继承于 ResultSet 并在接口类中将不涉及的方法同 default 重写并标记为 UnsupportedOperationException 如下述 updateNull 定义所示:

public interface AbstractResultSet extends ResultSet, AutoCloseable {

    void fill(List<LinkedHashMap<String, Object>> theData);
    
    default void updateNull(int columnIndex) throws SQLException {
        throw new UnsupportedOperationException();
    }
    
    // 其它方法同上,忽略具体内容
}

通过此定义方式,后续在创建 AbstractResultSet 实现类时即可根据需要自定义选择重写接口,而不用一股脑全局重写,从而提高代码的简洁性。

二、模式实现

1. 接口实现

定义完接口之后就简单了,创建实现类 DataResultSet 实现具体的细节。

具体设计参考了 com.sun.rowset.CachedRowSetImpl 类中的实现思路,将数据通过 Map 塞到内存中,再通过游标逐行读取内容,当然这种情况就没法针对大数据量,容易造成内存溢出的风险。

这里定义三个成员变量 cursorRow, rowCountrowContainer,对应信息参考下表:

属性 作用
cursorRow 行游标,用于标记当前读到第几行。
rowCount 行数,用于记录当前结果集中的总行数。
rowContainer Map容器,用于存储目标数据内容,key 存对应行下表,value存该行数据。
public class DataResultSet implements AbstractResultSet {

    private int cursorRow;
    private int rowCount;
    private Map<Integer, LinkedHashMap<String, Object>> rowContainer;

    public DataResultSet() {
        rowContainer = new ConcurrentHashMap<>();
        cursorRow = 0;
        rowCount = 0;
    }

    // 略去具体接口实现
}

2. 数据填充

在上面已经提到了是通过 Map 容器用于接收目标数据用于后续读取,因此在 DataResultSet 类中新增 fill() 方法用于接收数据。

方法入参为 List 容器,容器中的每个元素代表一行数据,注意这里容器中需要指定为 LinkedHashMap,这样后续通过 getString() 才能保证通过序号访问的确定性。

因为 ListLinkedHashMap 皆为有序容器,因此 fill() 只需简单遍历目标数据按照 List 存放顺序存入 rowContainer 即可。

public void fill(List<LinkedHashMap<String, Object>> theData) {
    if (Objects.isNull(theData)) {
        throw new IllegalArgumentException("Data can't be null");
    }
    rowCount = theData.size();
    for (int i = 1; i <= rowCount; i++) {
        rowContainer.put(i, theData.get(i - 1));
    }
}

3. 数据验证

在定义具体的接口实现之前这里定义了几个数据验证,用于验证读取的行标与字段下标的合法性。

private void checkCursor() {
    if (cursorRow < 1 || cursorRow > rowCount) {
        throw new IllegalArgumentException("Cursor is illegal");
    }
}

private void checkIndex(int index) {
    int colCount = rowContainer.get(cursorRow).size();
    if (index < 1 || index > colCount) {
        throw new IllegalArgumentException("Index is illegal");
    }
}

private void checkColLabel(String name) {
    Set<String> keySet = rowContainer.get(cursorRow).keySet();
    if (!keySet.contains(name)) {
        throw new IllegalArgumentException("Column " + name + " didn't existed");
    }
}

4. 具体实现

(Ⅰ) next() 实现

next() 实现的较为简单,即每次执行 next() 时判断当前行标 (cursorRow) 是否已经超过行数 (rowCount),若否则行标加一。

public boolean next() throws SQLException {
    boolean hasRemain = cursorRow < rowCount;
    if (hasRemain) {
        // Move row cursor
        cursorRow++;
    }
    return hasRemain;
}

(Ⅱ) getString(int index) 实现

getString(int) 即通过下标获取属性值,因为在执行 next() 时行标已经为对应的目标行,因此通过 rowContainer.get(cursorRow) 即可获取行标所对应的该行数据。

获取到对应行后即可将转为数组并通过下标获取,注意这里的 columnIndex 是按照 JDBC 标准从 1 开始,因此转为数组读取时需要减一。

@Override
public String getString(int columnIndex) throws SQLException {
    // check row cursor is valid
    this.checkCursor();
    // check index is valid for column cursor
    this.checkIndex(columnIndex);

    LinkedHashMap<String, Object> row = rowContainer.get(cursorRow);
    return row.values().toArray()[columnIndex - 1].toString();
}

(Ⅲ) getString(String name) 实现

与之对应的 getString(String) 即通过属性名进行查询。

这里参考了 CachedRowSetImpl#getString(String) 的实现思路,根据 columnLabel 获取对应属性的下标,即可复用上述的 getString(int) 方法

@Override
public String getString(String columnLabel) throws SQLException {
    // check row cursor is valid
    this.checkCursor();
    // check column label
    this.checkColLabel(columnLabel);

    // get value by column index
    return getString(getColIdxByName(columnLabel));
}

// 根据 name 获取下标
private int getColIdxByName(String name) {
    LinkedHashMap<String, Object> record = rowContainer.get(cursorRow);
    Map<String, Integer> orderMap = new HashMap<>();
    int ch = 1;
    for (Map.Entry<String, Object> entry : record.entrySet()) {
        orderMap.put(entry.getKey(), ch);
        ch++;
    }
    return orderMap.get(name);
}

(Ⅳ) close() 实现

因为继承了 AutoCloseable 类,所以需要重写 close() 方法,内容也相对简单,将成员遍历重置即可。

@Override
public void close() throws Exception {
    rowContainer = null;
    cursorRow = 0;
    rowCount = 0;
}

三、演示示例

1. 基本使用

完成上述步骤之后基本就完成了所有工作,对应的测试实例如下。

这里通过 generateMockData() 生成了一个五行四列的测试数据,在通过 fill() 填充数据之后利用 next()getString() 模拟数据读取。

public class ResultTest {

    @Test
    public void demo() throws Exception {
        // Mock data
        List<LinkedHashMap<String, Object>> list = generateMockData(5, 4);

        try (DataResultSet resultSet = new DataResultSet()) {
            resultSet.fill(list);
            while (resultSet.next()) {
                System.out.println(resultSet.getString("column-1"));
            }
        } catch (Throwable e) {
            throw e;
        }
    }

    public List<LinkedHashMap<String, Object>> generateMockData(int rNum, int colNum) {
        List<LinkedHashMap<String, Object>> list = new ArrayList<>();
        for (int i = 0; i < rNum; i++) {
            LinkedHashMap<String, Object> map = new LinkedHashMap<>();
            for (int j = 1; j <= colNum; j++) {
                map.put("column-" + j, new Random().nextInt(50));
            }
            list.add(map);
        }
        return list;
    }
}

文章作者: 烽火戏诸诸诸侯
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 烽火戏诸诸诸侯 !
  目录