DButils的通用数据库方法有两种,一种是用于增删改的Update方法,一种是用于查询的Query方法。
根据其通用性的思想,对update方法和query方法的底层实现进行解析。
根据DButils的两个方法思考,思路是:
1.传入带占位符(?)的数据库语句,如:insert into bank values(null,?,?)。
2.传入通用型的参数,并且传入的参数个数可变。
Update(增删改):
//通用的增删改
/*
* @param sql 需要操作的sql语句
* @param args 可变参数,...表示有几个占位符就传入几个参数,args相当于一个可变数组
* */
public void update(String sql,Object ... args) throws SQLException {
Connection conn = null;
PreparedStatement ps = null;
try {
conn = JDBCutil02.getConn();
ps = conn.prepareStatement(sql);
for (int i=0;i<args.length;i++){
ps.setObject(i+1,args[i]);
}
ps.executeUpdate();
} finally {
JDBCutil02.release(ps,conn);
}
}
通过(Object ... name)(可变参数)的方式,可以传入任意个数的参数,并且Object类型可以接受任意类型的参数。
然后通过for循环,遍历args 数组,通过PrepareStatement的setObject方法,一个个为占位符赋值。
但是这样有隐患,那就是如果输入的参数个数与占位符不一样,遍历次数就会有问题,将会出错。
所以为了解决这个隐患,使用参数元数据 ParameterMetaData 来获取参数SQL中有多少个问号,通过计算占位符的数量,从而消除遍历次数不正确的问题。
优化后的Update:
/*
元数据:描述数据的数据
* 优化版的update语句,防止可变参数数量传错,以问号个数为准
* 通过 参数元数据ParameterMetaData 来获取参数中有多少个问号
* */
public void update02(String sql,Object ... args) throws SQLException {
Connection conn = null;
PreparedStatement ps = null;
try {
conn = JDBCutil02.getConn();
ps = conn.prepareStatement(sql);
//元数据
//获取到有几个问号,占位符
ParameterMetaData metaData = ps.getParameterMetaData();
int count = metaData.getParameterCount();
for (int i=0;i<count;i++){//以问号的数量来遍历
ps.setObject(i+1,args[i]);
}
ps.executeUpdate();
} finally {
JDBCutil02.release(ps,conn);
}
}
Query的底层实现稍微上有点复杂,前两个问题与update一样,占位符个数和参数,稍有不同的时候,查询语句会返回结果集,并且数据库返回的结果集需要进行封装,还需要将封装的结果集返回,所以query的重点就在:
数据获取与数据封装
解决方法:由于我们不清楚具体要封装成什么对象,所以选择将结果集交给调用者自行去封装。
- 首先,先声明一个泛型接口(ResultSetHandler<T>),里面定义一个返回值为泛型(T),参数为ResultSet 的方法(T Handle(ResultSet rs)),也即是泛型方法。并且将query也定义为泛型方法,返回值为T。
- 然后在 query泛型方法中添加一个 ResultSetHandler 接口参数,即:(public <T> T query(String sql,ResultSetHandler<T> handler,Object ... args))
- 然后在 query 中调用参数 handler 的 handle 方法封装数据,并且获取封装好的数据(T t = (T) handler.Handle(rs);),最后返回封装好的数据。
使用方法:
- 通过匿名实现类的方式,实例化 query 中的ResultSetHandler 接口参数(指向实现类的引用,相当于接口的向上转型),并且实现其中的handle方法,在handle方法中对结果集(rs)进行封装,而封装的数据对象类型由调用者实例化接口时传入。如(new ResultSetHandler<Bank>(){}),最后返回封装好的数据对象类型。
具体代码:
ResultSetHandler接口:
import java.sql.ResultSet;
import java.sql.SQLException;
public interface ResultSetHandler<T> {
/*
* 定义了数据封装的规则。规范
* */
T Handle(ResultSet rs);
}
Query 方法:
/*查询
可能出现的问题
select * from aa
select * from aa where id =?
select * from aa where name=? and gender=?
问题一:数据获取,以及数据封装成什么对象返回。因为调用的地方需要的数据不一样,数据封装问题
解决方法:将结果集交给用户自行去封装,定义一个ResultSetHandler的接口,里面声明一个handle方法。
然后query方法中添加一个ResultSetHandler接口参数,然后通过匿名实现类的方式实现handle方法,然后对其中的结果集数据进行封装。
*/
public <T> T query(String sql,ResultSetHandler<T> handler,Object ... args){
Connection conn = null;
PreparedStatement ps = null;
try {
conn = JDBCutil02.getConn();
ps = conn.prepareStatement(sql);
//元数据
//获取到有几个问号,占位符
ParameterMetaData metaData = ps.getParameterMetaData();
int count = metaData.getParameterCount();
for (int i=0;i<count;i++){//以问号的数量来遍历
ps.setObject(i+1,args[i]);
}
//执行查询工作,然后得到结果集
ResultSet rs = ps.executeQuery();
//将结果集给调用者,让它去封装数据,并且返回封装好的数据
T t = (T) handler.Handle(rs);
return t;
}
catch (Exception e){
e.printStackTrace();
}finally {
JDBCutil02.release(ps,conn);
}
return null;
}
Query 方法的调用:
/*
* ResultSetHandler参数其实等同于
* ResultSetHandler handle = new A();A是具体实现类,这里使用了匿名实现类的方式,指向实现类的引用,相当于接口的向上转型。
* */
@Test
public void testQuery(){
Bank bank = query("select * from bank where id = ?", new ResultSetHandler<Bank>() {
@Override
public Bank Handle(ResultSet rs) {
try {
Bank bank = new Bank();
while (rs.next()){
String name = rs.getString("name");
double money = rs.getDouble("money");
bank.setName(name);
bank.setMoney(money);
}
return bank;
} catch (SQLException e) {
e.printStackTrace();
}
return null;
}
},3);
System.out.println(bank);
}
总结
其上基本上就是DButils框架的update和query方法的解析,其中update方法比较简单,因为只需要执行即可。
query方法的重点主要就是在数据的封装方面,如何做到可以通用型的封装数据以及返回通用型的数据。
通过泛型类型的方式,让调用者指定封装数据的对象类型,并且将数据封装的工作交给调用者来进行,而无论使用匿名实现类的方式在调用query方法的时候再定义封装方法,还是提前写好继承接口的类作为参数传入。
核心思想都是将我们无法确定的数据封装,数据对象类型,交给调用者来处理,从而达成定义通用方法的目的。