JDBC工具类——JdbcUtils(5)
前言
本系列文章介绍JDBC工具类——JdbcUtils
的封装,部分实现参考了Spring框架的JdbcTemplate
。
完整项目地址:https://github.com/byx2000/JdbcUtils
回顾
到目前为止,JDBC的查询操作已经封装得差不多了,结果集转换器和行转换器这两个抽象让查询操作使用起来非常方便,用户不仅可以直接使用预定义的转换器,还可以自定义转换器,基本能实现各种常见的查询需求。但是还是存在一些不完美的地方。
ResultSet
暴露引发的问题
在ResultSetMapper<T>
接口和RowMapper<T>
接口的map
方法的定义中,向用户直接暴露了ResultSet
参数:
T map(ResultSet rs) throws Exception;
这导致用户在实现自定义转换器的过程中,可能无意或有意地调用了ResultSet
中的一些关键方法,从而导致程序崩溃。
举个例子,假如某个用户在实现ResultSetMapper<T>
的过程中,“自作聪明”地在map
方法的最后调用了ResultSet
的close
方法:
public class UserListResultSetMapper implements ResultSetMapper<List<User>>
{
@Override
public List<User> map(ResultSet rs) throws Exception
{
...
rs.close();
return XXX;
}
}
这将会引发程序崩溃,因为在query
函数中也会对结果集进行关闭,导致ResultSet
被关闭了两次。
另一个问题,就是用户在实现RowMapper<T>
时,可能在map
方法中调用ResultSet
的next
函数,这会导致结果集向前移动一行:
public class UserRowMapper implements ResultSetMapper<User>
{
@Override
public User map(ResultSet rs) throws Exception
{
...
rs.next();
...
return XXX;
}
}
当然,我们可以在文档中加上说明,提醒用户实现这两个接口时的注意事项,但是并不是每个用户使用前都会仔细阅读文档。
封装ResultSet
为了解决这个问题,我们不能把ResultSet
暴露给用户,所以需要把ResultSet
封装起来。我们把封装后的结果集分离成Record
和Row
两个接口:
public interface Record
{
Row getCurrentRow(); // 获取当前行
boolean next(); // 移动到下一行
}
public interface Row
{
Object getObject(String columnLabel);
Object getObject(int columnIndex);
int getInt(String columnLabel);
int getInt(int columnIndex);
String getString(String columnLabel);
String getString(int columnIndex);
double getDouble(String columnLabel);
double getDouble(int columnIndex);
int getColumnCount(); // 获取列数
String getColumnLabel(int index); // 获取列标签
}
Record
表示整个结果集,它是对数据库查询操作返回结果的封装。一个结果集由若干个数据行组成。初始时,结果集的当前行指向第一行之前,调用next
方法可以让当前行向前移动一行。若当前已到达最后一行,则next
调用返回false
,否则返回true
。
Row
表示数据行,封装了结果集的一行数据。数据行由若干列组成,每列都是一个值。Row中包含了一系列getXXX
方法用于获取列中的值。
Record
中的getCurrentRow
方法用于获取当前行。
重构ResultSetMapper<T>
和RowMapper<T>
有了Record
和Row
,就可以对之前的两个转换器接口做重构:
public interface RecordMapper<T>
{
T map(Record record);
}
public interface RowMapper<T>
{
T map(Row row);
}
注意,这里把ResultSetMapper<T>
重命名成了RecordMapper<T>
。
此时Record
和Row
接口中已经没有像close
一样危险的方法,所以再也不用担心用户在map
方法中搞破坏了。
实现Record
和Row
接口
接下来,需要使用适配器模式将ResultSet
转换成Record
和Row
:
public class RecordAdapterForResultSet implements Record, Row
{
private final ResultSet rs;
public RecordAdapterForResultSet(ResultSet resultSet)
{
this.rs = resultSet;
}
@Override
public Object getObject(String columnLabel)
{
try
{
return rs.getObject(columnLabel);
}
catch (SQLException e)
{
throw new RuntimeException(e.getMessage(), e);
}
}
@Override
public Object getObject(int columnIndex)
{
try
{
return rs.getObject(columnIndex);
}
catch (SQLException e)
{
throw new RuntimeException(e.getMessage(), e);
}
}
// 其它getXXX方法...
@Override
public int getColumnCount()
{
try
{
ResultSetMetaData metaData = rs.getMetaData();
return metaData.getColumnCount();
}
catch (SQLException e)
{
throw new RuntimeException(e.getMessage(), e);
}
}
@Override
public String getColumnLabel(int index)
{
try
{
ResultSetMetaData metaData = rs.getMetaData();
return metaData.getColumnLabel(index);
}
catch (SQLException e)
{
throw new RuntimeException(e.getMessage(), e);
}
}
@Override
public Row getCurrentRow()
{
return this;
}
@Override
public boolean next()
{
try
{
return rs.next();
}
catch (SQLException e)
{
throw new RuntimeException(e.getMessage(), e);
}
}
}
RecordAdapterForResultSet
类将ResultSet
包装成了Record
和Row
,同时封装了异常处理。
修改query
方法
query
方法做出的修改如下:
public class JdbcUtils
{
...
public static <T> T query(String sql, RecordMapper<T> recordMapper, Object... params)
{
ResultSet rs = null;
PreparedStatement stmt = null;
Connection conn = null;
try
{
conn = getConnection();
stmt = createPreparedStatement(conn, sql, params);
rs = stmt.executeQuery();
return recordMapper.map(new RecordAdapterForResultSet(rs));
}
catch (Exception e)
{
throw new RuntimeException(e.getMessage(), e);
}
finally
{
close(rs, stmt, conn);
}
}
...
}
其中,try
块中最后一行由
return resultSetMapper.map(rs);
改成了
return recordMapper.map(new RecordAdapterForResultSet(rs));
总结
到这里,对JDBC查询操作的封装就结束了。下一篇文章将介绍JDBC更新操作的封装。