一、模式介绍
1. 基本介绍
我们都知道在 Java
的 JDBC
中定义了一套标准规范,例如 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
, rowCount
与 rowContainer
,对应信息参考下表:
属性 | 作用 |
---|---|
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()
才能保证通过序号访问的确定性。
因为 List
与 LinkedHashMap
皆为有序容器,因此 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;
}
}