JDBC工具类——JdbcUtils(5)
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更新操作的封装。
浙公网安备 33010602011771号