我对JDBC的封装思考

想法的诞生

当我学习三层Dao的封装的时候发现,在数据持久化的过程中,对数据的查询操作语句是非常麻烦的,如果我们的实体类Bean属性过多(数据库表字段多)的时候,对表的查询填充到实体类时会有许多冗余的代码,在编写代码时会发现效率并不高(我说的是写代码的效率),比如对以下实体类填充的过程:

/**
 * 此方法为持久层的实现(实现了查询dog表的数据)
 * 根据宠物名字查询宠物全部信息
 */
public List<Pet> findByName(String name) {
    
    
		//sql语句
		String strSql = "select * from dog where name = ?";
		//该数组存储需要替换?的值
		Object[] param = {
    
    name};
		//数据表的一条记录(一行)为一个实体对象,因此需要集合存储
		List<Pet> pList = new ArrayList<Pet>();
		//声明结果集
		ResultSet rs = null;
		try {
    
    
			//此方法已封装preparedStatement预编译过程
			rs = this.excuteQuery(strSql, param);
			//在此处取出数据库每行的数据集
			while(rs.next()) {
    
    
				Pet pet = new Pet();//每一条记录对应的对象实例化
				//以下是对实体类的填充
				pet.setId(rs.getInt("id"));
				pet.setName(rs.getString("name"));
				pet.setHealth(rs.getInt("health"));
				pet.setLove(rs.getInt("love"));
				pet.setStatus(rs.getString("strain"));
				//将已经填充好的实体类添加到集合中
				pList.add(pet);
			}
		} catch (SQLException e) {
    
    
			e.printStackTrace();
		} finally {
    
    
			//释放三大对象资源
			this.closeAll(conn, pstmt, rs);
		}
		//返回实体类集合
		return pList;
	}

从上面可以发现如果数据表的字段有十条,那么这里要写很多重复的代码,而且三大对象不能在BaseDao中释放

第一次想法与第一次的尝试(在不知道反射的前提下)

从上面的代码可以看出在对实体类填充的循环过程中,rs对对象的设值可以统一为rs.getObject(),由于不知道实体类会有多少个属性,会有什么封装方法。所以我把BaseDao的executeQuery的实现方式改了一下,将它的返回值改为List< Object >, 然后再将数据表所有数据先存储到List< Object >中,具体实现如下:

	/**
	 * 查询到数据表所有数据
	 * @param strSql
	 * @param param
	 * @return 数据集合
	 */
	public List<Object> executeQuery(String strSql, Object[] param) {
    
    
		//获取连接对象
		conn = getConnection();
		//存储所有数据集的集合
		List<Object> pList = new ArrayList<Object>();
		
		try {
    
    
			//获取预编译对象
			pstmt = conn.prepareStatement(strSql);
			//获取成功不为null
			if(pstmt != null) {
    
    
				//根据参数param的值来确定sql有无可设参数
				for(int i = 0; i < (param == null ? 0 : param.length); i++) {
    
    
					//预编译设值
					pstmt.setObject(i+1, param[i]);
				}
				//获取结果集
				rs = pstmt.executeQuery();
				//获取数据的过程
				while(rs.next()) {
    
    
					//获取每行的列,再把每列值添加到集合中,次循环的条件为当前行的列数
					for(int i = 0; i < rs.getMetaData().getColumnCount(); i++) {
    
    
						//添加到结果集中
						pList.add(rs.getObject(i+1));
					}
				}
				//为了方便外部填充至实体类,此处将表列数存储到集合最后,外部可通过
				//pList.get(pList.size()-1)获取每列列数
				pList.add(rs.getMetaData().getColumnCount());
				//返回数据集合
				return pList;
			}
		} catch (SQLException e) {
    
    
			e.printStackTrace();
		} finally {
    
    
			closeAll(this.rs, this.pstmt, this.conn);
		}
		//以上失败返回空
		return null;
	}

当这个方法写完时,我又头疼了,我该如何把这个Object集合的每个值填充到对应的实体类呢?
冥冥之中我便发现所有实体类封装的set方法都是可以统一起来的,那如何将他们统一封装呢,这又是一个问题,java又没有指向函数的指针,我想如果有类似的指针的话那就可以定义一个指向函数的指针类型的数组(用c++表示:void (*fun_array [3]) (Object) = {setName,setId,setLove}),这样的话只要用一个数组就可以循环调用set方法 例如 : fun_array[0] ("name");
在网上查找一番资料后,发现java还有一个Lambda表达式可以解决以上问题,但是"指向"那个函数的那个"指针"要替换为接口,而且接口要有抽象一个方法,而Lambda表达式则是重写了接口中的抽象方法,这样的话我就可以声明一个接口类型的数组,用这个接口类型的数组来存放Lambda表达式,每一个Lambda表达式都是对该接口方法的实现,如下:

/**
 *该方法的实现放在单独的类中,并实现一个接口,接口很简单就一个方法
 */
public class ExpandToolDaoImpl implements ExpandToolDao {
    
    

	/**
	 * 该方法只需要传入相应的实体类对象即可,该方法会根据实体类类型对实体类填充设值
	 */
	public Function[] newArrayFunc(Object stu) {
    
    
		Function[] arrayFun = null;
		//判断该实体类stu的类型
		if(stu instanceof NewsUsers) {
    
    
			//对相应属性设值,此处相当于一个数组存放了三个方法
			arrayFun = new Function[]{
    
    
				(a)->((NewsUsers)stu).setUid((int)a),
				(a)->((NewsUsers)stu).setUname((String)a),
				(a)->((NewsUsers)stu).setUpwd((String)a)
			};
		} else if(stu instanceof Student) {
    
    
		//对相应属性设值,此处相当于一个数组存放了六个方法
			arrayFun = new Function[] {
    
    
				(a)->((News)stu).setNid((int)a),
				(a)->((News)stu).setNtid((int)a),
				(a)->((News)stu).setNtitle((String)a),
				(a)->((News)stu).setNauthor((String)a),
				(a)->((News)stu).setNcreateDate((Date)a),
				(a)->((News)stu).setNsummary((String)a)
			};
		} else if(stu instanceof Comments) {
    
    
		//对相应属性设值,此处相当于一个数组存放了六个方法
			arrayFun = new Function[] {
    
    
				(a)->((Comments)stu).setCid((int)a),
				(a)->((Comments)stu).setCnid((int)a),
				(a)->((Comments)stu).setCcontent((String)a), 
				(a)->((Comments)stu).setCdate((Date)a),
				(a)->((Comments)stu).setCip((String)a), 
				(a)->((Comments)stu).setCauthor((String)a)
			};
		} else if(stu instanceof Topic) {
    
    
		//对相应属性设值,此处相当于一个数组存放了两个个方法
			arrayFun = new Function[] {
    
    
				(a)->((Topic)stu).setTid((int)a),
				(a)->((Topic)stu).setTname((String)a)
			};
		}
		return arrayFun;
	}
}
public interface Function {
    
    
	/**
	 * 为实体类字段设置值的方法
	 * @param obj
	 */
	public void func(Object obj);
}

public interface ExpandToolDao {
    
    
	/**
	 * 返回存储设值方法的方法集合
	 * @param acc
	 * @return
	 */
	public Function[] newArrayFunc(Object stu);
}

这样的话以后需要填充实体类只需提前向该类写好各个属性对应的set方法存储到数组中,之后当我们需要对一个实体类设值只需要这样调用即可:newArrayFunc(实体类对象)[0].func(value)
可以看到以后在dao的实现类中则看不到对实体类的set操作了:

public List<Student> getStudent() {
    
    
		//查询student表所有数据
		String strSql = "select * from student";
		//存储设值方法的实现类
		ExpandToolDaoImpl met = new ExpandToolDaoImpl();
		//获取表所有数据存储到集合中
		List<Object> newsterList = this.executeQuery(strSql, null);
		//将从newsterList集合中把数据转换为该集合的一个个对象
		List<Student> newslis = new ArrayList<>();
		//由于newsterList中的数据是连续的,不应该写在循环中,否则会被重置导致读取到重复数据所以k为全局
		int k = 0;
		//1.循环次数必须为数据表行数(一行为一个对象)
		//2.循环次数由列数决定,在上文中我将列数存储在集合的最后,所以会有
		//(int)newsterList.get(newsterList.size()-1)取值操作
		for(int i = 0; i < newsterList.size()/(int)newsterList.get(newsterList.size()-1); i++) {
    
    
			//读取一行的开始,先new一个实体对象
			Studnet stu= new Studnet();
			//传入实体类对象,将实体类对象的set方法存储到接口数组中
			Function[] arrayFun = met.newFuncArray(stu);
			//现在arrayFun存储的都是方法的重写,arrayFun.length为该实体类set方法数量
			for(int j = 0; j < arrayFun.length; j++) {
    
    
				//调用arrayFun的第j个方法func
				//newsterList将第k个element传入func方可对stu对象进行属性填充
				arrayFun[j].func(newsterList.get(k++));
			}
			newslis.add(news);//每一个对象填充完后添加到集合中
		}
		//返回数据集合
		return newslis;
	}

以上就完成了大概的整个查询数据库数据到实体类的整个封装过程,写过一两次后会发现这样并没有完全封装好,而且没有对设值逻辑隐藏,每次设值还要写两个循环,这样好像并不人性化,于是我决定把该逻辑直接封装到BaseDao类。
那么问题来了,如何封装呢,这个代码片段看起来并不友好,像外层循环的第一条语句的实例化,如何把它抽出来作为公共实例化的工厂呢?(在不用反射之前只能用接口来实现了) 当我们需要对一件事情做多种处理的时候,我们可以利用到java面向对象的特性了,像多态就能很完美的解决这个问题,比如我们可以让所有实体类实现一个接口,在接口中定义一个创建对象的抽象方法让实现该接口的类重写该方法,这样就可以做到利用多态创建对象了,接下来的集合对象都可以利用泛型做到。

public <T>List<T> getEntity(EntityClass<T> ec, Object[] param, String sql) {
    
    
		//返回存储设值方法的方法集合操作对象(et)
		ExpandToolDao et = new ExpandToolDaoImpl();
		//用来存储返回的实体类集合
		List<T> listInfo = new ArrayList<T>();
		//执行sql查询语句的关键代码
		List<Object> objList = this.executeQuery(sql, param);
		//由于objList中的数据是连续的,不应该写在循环中,否则会被重置导致读取到重复数据所以k为全局
		int k = 0;
		//外层循环次数为数据库记录的总行数,从数据库得到的实体类集合的尾端存储数据表的列数
		for(int i = 0; i < objList.size()/(int)objList.get(objList.size()-1); i++) {
    
    
			//利用多态的特性获取指定的实体类对象,EntityClass的泛型可指定获取何种类型的对象
			Object obj = ec.getNew();
			//传入实体类对象,将实体类对象的set方法存储到接口数组中
			Function[] arrayFun = et.newArrayFunc((T)obj);
			//现在arrayFun存储的都是func方法的重写,arrayFun.length为该实体类set方法数量
			for(int j = 0; j < arrayFun.length; j++) {
    
    
				//将objList从数据库取出的数据通过被重写的func方法设置到实体类中
				arrayFun[j].func((T)objList.get(k));
				//k为下一个下标做准备
				k++;
			}
			//将已填充的实体类对象存储到集合之中
			listInfo.add((T)obj);
		}
		//返回集合对象
		return listInfo;
	}

这样全部封装已完成,现在只要在dao实现类中调用该方法即可方便完成对数据的读取了。

public List<StudentResultInfo> getStudentInfo(String subjectName) {
    
    
		String strSql = "SELECT s.studentName,s.studentno,r.`studentResult`,sub.`subjectName` FROM student AS s INNER JOIN result AS r ON s.`studentNo` = r.`studentNo` INNER JOIN `subject` AS sub ON sub.`subjectNo` = r.`subjectNo`\r\n" + 
				"WHERE s.`studentNo` IN (\r\n" + 
				"	SELECT studentNo FROM result WHERE subjectNo = (\r\n" + 
				"		SELECT subjectNo FROM `subject` WHERE subjectName = ?\r\n" + 
				"	) AND studentResult >= 60\r\n" + 
				")";
		Object[] param = {
    
    subjectName};
		return this.<StudentResultInfo>getEntity(new StudentResultInfo(), param, strSql);
	}

看是不是一行就能解决问题了呢。
那么以上就是我对此封装的全部思路与关键代码,在这里我仅分享我的思路与想法,我想这并不是一个很好解决方案,但是我认为出发点是很好的,大家可以借鉴一下其中的封装目的,在这个基础上可以进一步发展,进一步思考如何才能达到高效率的封装,和可读性更高的代码。
接下来我会在下一篇文章中利用Java反射机制来优化这次的封装。

猜你喜欢

转载自blog.csdn.net/qq_43538697/article/details/107969770
今日推荐