编写自定义的JDBC框架与策略模式

时间:2023-12-16 18:41:44

  本篇根据上一篇利用数据库的几种元数据来仿造Apache公司的开源DbUtils工具类集合来编写自己的JDBC框架。也就是说在本篇中很大程度上的代码都和DbUtils中相似,学完本篇后即更容易了解DbUtils是如何使用的。

  我们在使用JDBC对数据库进行操作时,基本都是在dao层,也就是说在dao层封装了最基本的增删改查(CRUD)的方法,但是我们以前都是怎么写的呢?

 public class UserDao {
//添加用户
public void add(User user) throws SQLException {
Connection conn = null;
PreparedStatement st = null;
ResultSet rs = null;
try{
conn = JdbcUtils.getConnection();
String sql = "insert into user(id,name,age) values(?,?,?)";
st = conn.prepareStatement(sql);
st.setInt(1, user.getId());
st.setString(2, user.getName());
st.setInt(3, user.getAge());
st.executeUpdate(); }finally{
JdbcUtils.release(conn, st, rs);
}
}
//删除用户
public void delete(User user) throws SQLException {
Connection conn = null;
PreparedStatement st = null;
ResultSet rs = null;
try{
conn = JdbcUtils.getConnection();
String sql = "delete from user where id=?";
st = conn.prepareStatement(sql);
st.setInt(1, user.getId());
st.executeUpdate(); }finally{
JdbcUtils.release(conn, st, rs);
}
}
。。。//其他代码,这里省略
}

  看看上面的代码,很熟悉吧,这里面就是我们以前经常写的“模板”代码,可以看出像在dao中对数据库的增加和删除方法,有大部分的代码都是重复的,需要变得只是其中一点点,我们说过对于重复的代码都要进行提取,简化开发,这就是本篇我们自定义框架要做的事,同时也是DbUtils的好处。

  对于一个数据库的增删改查(CRUD)来说,其中增加、删除、修改这三种方式在我们的JDBC中代码都是类似的,因为都是调用PreparedStatement对象的executeUpdate()无参方法,只是SQL命令语句和占位符参数不同,并且方法返回的是代表影响数据库中数据行数的整型。而对于数据库的查询来说,除了调用的是PreparedStatement对象的executeQuery()无参方法这点不同,还有一个最关键的是该方法返回的是结果集ResultSet对象,因此要另外处理。

  通过上面的分析,明白了如果要对以前dao层的增删改查进行简化,那么就要分两步走,一步简化增加、删除和修改;另一步简化查询。

  接下来我们将以一个案例来简化一个dao层的JDBC增删改查方法。所有的简化方法都可以放在JDBC的工具类JdbcUtils中,这样我们在dao层编写CRUD方法只要从工具类调用即可,可能这么说有点抽象,那么就往下看好了。

  注:对于增删改查方法分成两种方法,分别以update方法和query方法为名,这里我先从工具类JdbcUtils中提取出来,这样能重点讲解,但别忘了这两个方法我们都是放在工具类JdbcUtils中的。

  需求:自定义的JDBC框架能满足向驱动管理器中注册驱动、获取连接、释放资源、封装了增删改查(CRUD)的简化方法等。

一、  在数据库中创建一个user表

  SQL脚本内容如下:

create table user(
id int primary key,
name varchar(40),
age int
);

二、根据数据库表的实体创建相应的JavaBean对象

 public class User {
private int id;
private String name;
private int age; 。。。//此处省略各个属性的getter和setter方法 }

三、编写数据库工具类JdbcUtils

  我们知道数据库工具类要帮我们向Java驱动管理器注册数据库驱动并获取连接等操作,我们也学过各种开源的工具如DBCP和C3P0等。但是注意,我们要编写的是一个JDBC框架,这个框架是要给别人用的,所以应该尽量避免使用第三方开源工具,不然别人用我们的框架还得要导入另外的工具Jar包这就有点麻烦了。

因此我们可以使用《JDBC操作数据库的学习(2)》中的工具类(不推荐,因为没有使用连接池),或者《数据库连接池》中的JdbcPool工具类。这里我们使用后面一种方式,代码如下:

 public class JdbcUtils implements DataSource {

     private static LinkedList<Connection> connectionList = new LinkedList<>();   //以集合作为连接池
private static Properties config = new Properties(); static{
InputStream in = JdbcPool.class.getClassLoader().getResourceAsStream("database.properties");
try {
config.load(in);
Class.forName(config.getProperty("driver")); //注册驱动
String url = config.getProperty("url");
String username = config.getProperty("username");
String password = config.getProperty("password");
for(int i=0;i<10;i++) { //获取十个连接
Connection conn = DriverManager.getConnection(url, username, password);
connectionList.addLast(conn); //将每个连接都添加到集合(池)中
} } catch (Exception e) {
throw new ExceptionInInitializerError(e);
}
} @Override
public Connection getConnection() throws SQLException {
if(connectionList.size()<1) {
throw new RuntimeException("数据库连接忙");
}
Connection conn = connectionList.removeFirst();
MyConnection myConn = new MyConnection(conn); //从池中取出连接并使用包装类增强close方法 return myConn;
} class MyConnection implements Connection{ //包装设计模式的类
private Connection conn;
public MyConnection(Connection conn) {
this.conn = conn;
} @Override
public void close() throws SQLException {
connectionList.addFirst(this.conn); //调用close方法时只是将连接重新返回池中,而不会销毁
} @Override
public Statement createStatement() throws SQLException {
this.conn.createStatement(); //对于不增强的方法则调用目标对象的方法即可,在MyConnection包装类中其他方法都是这样的
return null;
}
。。。 //以下省略Connection接口中覆写的其他方法,对于不想增强的方法都如上(createStatement方法)所示
} // MyConnection包装类完成 @Override
public Connection getConnection(String username, String password)
throws SQLException { //实现DataSource接口的其他方法,在本案例中对我们来说并没有什么作用
return null;
}
。。。//以下省略DataSource接口中覆写的其他方法,这些方法对我们来说暂时没有作用
}

  当然自建数据库连接池麻烦的一点就是要自己动手增强从数据库直接获取的Connection对象,覆写其close方法,这里建议采用动态代理。

四、在工具类JdbcUtils中编写简化的增加、修改和删除的update方法

  前面我们分析过,增加、删除、修改这三种方式在我们的JDBC中代码都是类似的,因为都是调用PreparedStatement对象的executeUpdate()无参方法,只是SQL命令语句和占位符参数不同。因此,对于SQL命令语句和占位符参数,我们就可以以方法参数的形式进行传入,而方法内都是以前冗余重复的代码。

 //增加、删除、修改的统一方法
public static void update(String sql,Object[] params) throws SQLException {
Connection conn = null;
PreparedStatement st = null;
ResultSet rs = null;
try{
conn = JdbcUtils.getConnection();
st = conn.prepareStatement(sql);
for(int i=0;i<params.length;i++) {
st.setObject(i+1, params[i]);
}
st.executeUpdate();
}finally{
JdbcUtils.release(conn, st, rs);
}
}

五、在工具类JdbcUtils中编写简化的查询方法(重点)

  前面分析过,查询和其他方法如增加最大不同的一点就在于查询会返回结果集对象ResultSet,只有数据库的编程人员才知道如何使用结果集对象,但如果有开发人员没学过JDBC那么他们就无法使用该框架了,因此查询方法不仅仅只是查询,还应该包括对查询结果的处理,例如将查询到的每个数据封装到某个实体对象或者集合中。

  这就有一个问题了,到底如何详细地处理结果集框架开发人员是不知道的,鬼知道用户想拿查询结果干什么,通过对方立勋老师的视频,这里我们可以采用策略模式,即将这个处理交给用户来操作,我们只要提供给他们一个接口即可。

  也就是说我们会编写一个接口,这个接口是给用户来实现,或者我们也来写一些实现类给用户用,如果我们的实现类功能不够,用户还是可以通过该接口自己编写想要的功能。这就是策略模式。

  其实我们早就使用过策略模式,比如对于TreeSet集合,本身这个集合不知道如何对存入的对象进行排序操作,而是由我们在构建的同时传入实现Comparable接口或者Comparator接口,那么TreeSet集合就会根据我们所传入的比较器对象将集合中的元素进行排序,这也就是策略模式,将策略以接口抛出给用户来实现。

  现在作为框架的设计者我们就给用户暴露一个接口,这个接口用来给用户编写自己如何处理查询的结果集对象:

public interface ResultSetHandler {
//这个方法用户处理结果集对象
public Object handle(ResultSet rs);
}

  现在有了处理结果集的接口,那么我们在查询方法的参数不仅要传入SQL语句和占位符参数数组,还要传入这个接口,至于在用户使用时传入的是该接口的什么实现类我们并不在乎,这就是多态的好处:

 //查询的简化方法
public static Object query(String sql,Object[] params,ResultSetHandler handler) throws SQLException {
Connection conn = null;
PreparedStatement st = null;
ResultSet rs = null;
try{
conn = JdbcUtils.getConnection();
st = conn.prepareStatement(sql);
if(params!=null) {
for(int i=0;i<params.length;i++) {
st.setObject(i+1, params[i]);
}
}
rs = st.executeQuery();
return handler.handle(rs); //将结果集对象交给接口处理 }finally{
JdbcUtils.release(conn, st, rs);
}
}

  总体上做完这一查询的简化方法,我们就已经完成了对JDBC的增删改查的简化,我们在写dao层的代码就不要再一次次将“模板”代码在每个方法中重复的写,只要使用工具类中上面定义的update方法和query方法即可,我们的代码会简化很多。

  事实上我们已经结束了一个非常简单的框架的开发,但是作为框架的设计者,如果能帮用户多考虑一点是非常人性化的,比如上面的那个处理结果集的接口ResultSetHandler,我们可以在向用户暴露这个接口的同时也给出一些该接口的实现类,至于用户用不用那我们管不着,如果用户觉得功能不够可以自己写个类来实现该接口,而我们就以平常最可能使用到的功能给用户提供这接口的实现类。

  对结果集的处理就不免要使用到结果集的元数据了,关于结果集的元数据已经在上一篇博客中详细讲解了,接下来我们就来看看如何使用元数据来进行我们对结果集的处理,当然这里面还需要反射或内省来实现对属性的赋值。

5.1将单个查询结果封装进JavaBean对象中的结果集处理器类

 public class BeanHandler implements ResultSetHandler {

     private Class<?> clazz;

     public BeanHandler(Class<?> clazz) {
this.clazz = clazz; //接收JavaBean的类型
}
@Override
public Object handle(ResultSet rs) {
//覆写ResultSetHandler接口的未实现方法
try {
if (!rs.next()) {
return null;
} Object bean = this.clazz.newInstance();
ResultSetMetaData resultMeta = rs.getMetaData();
//将每一列数据的值都根据列名称反射进Bean对象的属性中
for (int i = 0; i < resultMeta.getColumnCount(); i++) {
String columnName = resultMeta.getColumnName(i + 1);
Field field = bean.getClass().getDeclaredField(columnName);
field.setAccessible(true);
Object value = rs.getObject(columnName);
field.set(bean, value);
}
return bean;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}

  在上面的代码中,需要我们在创建这个实现类对象的时候在构造器中传入一个JavaBean的类,例如对于这篇来说就是User.class,为什么要传入类而不是对象,这样做的好处是避免了在使用该实现类的时候还要创建一个JavaBean的对象,不够优雅!!!根据结果集的元数据获取这个数据共有几列,将每一列的数据通过结果集获取并使用反射将值赋予给Bean对象的属性。

5.2将多个查询结果封装进List集合中的结果集处理器类

 public class BeanListHandler implements ResultSetHandler {

     private Class<?> clazz;

     public BeanListHandler(Class<?> clazz) {
this.clazz = clazz;
}
@Override
public Object handle(ResultSet rs) {
List<Object> list = new ArrayList<>();
try{
ResultSetMetaData resultMeta = rs.getMetaData();
while(rs.next()) {
Object bean = this.clazz.newInstance();
for(int i=0;i<resultMeta.getColumnCount();i++) {
String columnName = resultMeta.getColumnName(i+1);
Object value = rs.getObject(columnName); Field field = bean.getClass().getDeclaredField(columnName);
field.setAccessible(true);
field.set(bean, value);
}
list.add(bean);
}
return list.size()>0?list:null;
}catch (Exception e) {
throw new RuntimeException(e);
}
}
}

  上面的代码只是对BeanHandler这个类进行了一点改动,内部维护一个List集合,将每个JavaBean对象从结果集中赋值属性后再依次添加到List集合中去,这也算是平常对结果集处理的一种常用的功能吧,例如列出所有的用户。

  对结果集还可以进行很多的处理来实现不同的方法,这里就介绍上面两种功能,至于其他的实现可以通过实现ResultSetHandler接口,做出自己想要的操作。

  最后我们已经完成了这个简单的框架,可以实现对dao层增删改查的简化,代码冗余做到了最小,那么我们就在dao层利用上面所做的所有的框架来简化CRUD的方法吧:

 public class UserDao {
//添加用户
public void add(User user) throws SQLException {
String sql = "insert into user(id,name,age) values(?,?,?)";
Object[] params = {user.getId(),user.getName(),user.getAge()};
JdbcUtils.update(sql, params);
} //删除用户
public void delete(int id) throws SQLException {
String sql = "delete from user where id=?";
Object[] params = {id};
JdbcUtils.update(sql, params);
} //修改用户
public void update(User user) throws SQLException {
String sql = "update user set name=?,age=? where id=?";
Object[] params = {user.getName(),user.getAge(),user.getId()};
JdbcUtils.update(sql, params);
} //查找某个用户
public User find(int id) throws SQLException {
String sql = "select * from user where id=?";
Object[] params = {id};
User user = (User) JdbcUtils.query(sql, params, new BeanHandler(User.class));
return user;
} //列出所有用户
public List<User> getAllUser() throws SQLException {
String sql = "select * from user";
List<User> list = (List<User>) JdbcUtils.query(sql, null, new BeanListHandler(User.class));
return list;
}
}

  可以看出通过简化我们的增删改查的冗余代码,在dao层对CRUD的操作只要几行即可,将以前的“模板”代码都在工具类中封装起来,同时对于查询获得的结果集处理也更加灵活。