我们不重复制造轮子,这里主要写的是如何封装JDBC,实现将数据库查询直接映射成javaBean,实现数据与对象的查询与映射。进阶可以思考开源框架hibernate,mybatis、JPA的底层是如何实现的。

直接正题,需要具备的基础知识如下:

  1. Java内省机制,反射,泛型,注解技术
  2. 设计模式-策略模式
  3. 动态代理技术
  4. JDBC基础

这里复习下JDBC的流程

  1. 获取数据库连接
  2. 连接获取预编译查询对象
  3. 设置sql查询参数
  4. 查询对象执行sql
  5. 得到结果集ResultSet
  6. 将结果集转换成对应的数据结构链表List<Bean>

那么我们开始封装JDBC流程,将可变结果集转换行为使用策略模式进行抽象,可以创建接口:

public interface ResultSetHandler<T> {/**结果集转换*/<T> T doHandler(ResultSet rs) throws SQLException;
}

封装JDBC查询方法如下(没有具体实现的方法,在ide中可能会有红色底线,不要紧,我们需要的是抽象编程,虽然该实现使用的是Java语言,学会抽象该ORM框架换个语言也可以实现):

/*** 基础查询,需要处理结果集映射* @param <T>* @param conn* @param sql* @param params* @param resultSetHandler* @return* @throws SQLException*/public static <T> T find(Connection conn, String sql, Map<Object, Object> params, ResultSetHandler<T> resultSetHandler) throws SQLException {//解析sql/HQLMap<String, Integer> result=new HashMap<String, Integer>();String processSQL = processSQL(sql, result);ResultSet rs=null;PreparedStatement pst = null;try {//连接获取预编译对象pst = conn.prepareStatement(processSQL);//设置查询参数paramssetPreparedStatementParams(pst,result,params);//执行查询得到结果集rs = pst.executeQuery();//结果集转换成数据链表return resultSetHandler.doHandler(rs);}finally {if (rs != null) {rs.close();}if (pst != null) {pst.close();}}}

我们开始实现processSQL解析方法sql:select * from user where user_name=  #{userName} ,因为解析器是复杂的,需要逐个切割字符串将 #{userName} 替换为 '?' 号,然后在将params对应key为 userName的值记录后面设置查询参数的位置(第几个问号),可以自行切割字符串然后解析#{}符号切割解析,或者直接参考hibernate的SQLQueryParser解析器,或参考mybatis的源码,这里我们就使用简单正则表达式进行解析(可能存在bug,仅写此博客使用过),一个我们的目的不是为了真正可以运行在生产环境,而是这ORM框架sql解析的一种抽象策略,我们的目的还是为了手写框架(不限制语言),解析器代码简单如下:

/*** 解析sql的一种策略抽象* @param sql 类似这样的HQL:select * from user where user_name=  #{userName} and password=#{password}* @param result 解析每个参数的位子,key参数名,value位置* @return 最终可执行的预编译sql带问号*/private static String processSQL(String sql, Map<String,Integer> result) {Matcher m = Pattern.compile("\\#\\{.*?\\}").matcher(sql);StringBuffer sb = new StringBuffer();int i=0;while (m.find()) {String param = m.group();String key = param.replaceAll("\\#|\\{|\\}", "").trim();result.put(key, i++);//记录每个参数的位置m.appendReplacement(sb, "?");//将每个参数 替换为 问号}m.appendTail(sb);return sb.toString();}

实现setPreparedStatementParams,为每个‘?’问号的参数设置值,逻辑就是前者解析sql得到每个问号的位置与每个参数的字段名称,给相应设值,这里简单扩展了下查询支持时间数据类型,兴趣的同学可以基于此自行扩展支持IN参数等复杂参数查询

/*** @param pst 查询对象* @param result 参数的位置* @param params 查询参数* @throws SQLException */private static void setPreparedStatementParams(PreparedStatement pst, Map<String, Integer> result, Map<Object, Object> params) throws SQLException {for (Entry<String, Integer> entry : result.entrySet()) {String key = entry.getKey();//参数名Integer index = entry.getValue();//问号位置Object param = params.get(key);//查询参数,if(param instanceof Date){Date time=(Date)param;//支持时间参数,我们为了兼容可以传入复杂参数,如时间使用Object,pst.setTimestamp(index + 1, new Timestamp(time.getTime()));}else{pst.setObject(index+ 1, param);}//后期可扩展IN参数,支持传入数组,支持select * from user where user_id in(#{userId})   这样的,但需要修改sql解析器}}

此时我们已经实现了基础的sql查询得到结果集,现在开始增强该查询,支持JavaBean的参数与,返回List<Bean>的查询结果集映射,我们开始创建以下方法(有的方法未实现,有红色底线提示,不要紧,还是老话,我们要的是抽象编程)

/*** 高级查询支持javaBean自动映射,支持复杂参数*/public static <T> List<T> findForList(Connection conn, String sql, Object bean, final Class<T> clazz) throws SQLException {//解析Bean参数转换为前者基础查询的Map参数Map<Object, Object> paramConvert = paramConvert(bean);//调用基础查询方法List<T> find = find(conn, sql, paramConvert, new ResultSetHandler<List<T>>() {@SuppressWarnings({ "unchecked" })@Overridepublic List<T> doHandler(ResultSet rs) throws SQLException {List<T> list = new ArrayList<T>();while (rs.next()) {T objectFromResultSet;try {objectFromResultSet = getObjectFromResultSet(rs, clazz);} catch (Exception e) {throw new SQLException(e);}list.add(objectFromResultSet);}return list;}});return find;}

开始实现paramConvert,这里我偷懒了直接使用org.apache.commons.beanutils.PropertyUtils的工具类了,其实使用原本的反射也是可以的,因为反射需要些很多代码,我偷懒了,这里简单说话实现逻辑,利用类反射,遍历obj的所有属性,将属性名作为hashMap的key,属性值put到hashMap中。

/*** 将参数全部转换为Map参数*/@SuppressWarnings("unchecked")private static Map<Object, Object> paramConvert(Object params) {if (params == null) {params = new HashMap();}boolean isMap = false;Class<? extends Object> clazz = params.getClass();Class<?>[] interfaces = clazz.getInterfaces();for (Class<?> inf : interfaces) {if (inf == Map.class) {isMap = true;break;}}if (!isMap) {try {//这里直接使用org.apache.commons.beanutils.PropertyUtils的工具类了,具体实现也很简单,使用java的内省机制,将javaBean的每个属性都转到Map中return PropertyUtils.describe(params);} catch (Exception e) {throw new RuntimeException("查询参数转换异常", e);}}return (Map<Object, Object>) params;}

接下来实现getObjectFromResultSet对单条的结果集进行映射转换,这里写死小驼峰转换,其中我又偷懒了,使用了org.apache.commons.lang.StringUtils的工具类,但是这不要紧,重要的是我们将每个过程都策略抽象了

private static <T> T getObjectFromResultSet(ResultSet rs, Class<T> clazz) throws SQLException, InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException, IOException {Object resultBean = null;if (clazz == String.class) {return (T) rs.getString(1);} else if (clazz == Integer.class || clazz == int.class) {return (T) new Integer(rs.getInt(1));} else if (clazz == Boolean.class || clazz == boolean.class) {return (T) new Boolean(rs.getBoolean(1));} else if (clazz == Float.class || clazz == float.class) {return (T) new Float(rs.getFloat(1));} else if (clazz == short.class || clazz == Short.class) {return (T) new Short(rs.getShort(1));} else if (clazz == Double.class || clazz == double.class) {return (T) new Double(rs.getDouble(1));} else if (clazz == Long.class || clazz == long.class) {return (T) new Long(rs.getLong(1));} else {resultBean = clazz.newInstance();ResultSetMetaData rmd = rs.getMetaData();for (int i = 1; i <= rmd.getColumnCount(); i++) {String colName = rmd.getColumnLabel(i).toLowerCase();// 转换属性名称String colNameConverted = convertColName(colName);String setName = "set" + colNameConverted;Method method = getMethod(setName, clazz);if (method == null) {continue;}Class<?>[] paramtypes = method.getParameterTypes();Class<?> paramType = paramtypes[0];if (paramType.getName().equals("java.lang.String")) {String val = "";val = rs.getString(i);method.invoke(resultBean, new Object[] { val });} else if (paramType.getName().equals(Integer.class.getName()) || paramType.getName().equals("int")) {method.invoke(resultBean, new Object[] { rs.getInt(i) });} else if (paramType.getName().equals(Long.class.getName()) || paramType.getName().equals("long")) {method.invoke(resultBean, new Object[] { rs.getLong(i) });} else if (paramType.getName().equals(Long.class.getName()) || paramType.getName().equals("float")) {method.invoke(resultBean, new Object[] { rs.getFloat(i) });} else if (paramType.getName().equals(Double.class.getName()) || paramType.getName().equals("double")) {method.invoke(resultBean, new Object[] { rs.getDouble(i) });} else if (paramType.getName().equals(BigDecimal.class.getName())) {method.invoke(resultBean, new Object[] { rs.getBigDecimal(i) });}else if(paramType.getName().equals(Date.class.getName())){Timestamp timestamp = rs.getTimestamp(i);if(timestamp!=null){Date jDate=new Date(timestamp.getTime());method.invoke(resultBean, new Object[] {jDate});}}}}return (T) resultBean;}/**小驼峰*/private static String convertColName(String colName) {String[] tmp = colName.toLowerCase().split("_");StringBuilder sb = new StringBuilder();for (String col : tmp) {//这里又偷懒了,使用org.apache.commons.lang.StringUtilssb.append(StringUtils.capitalize(col));}return sb.toString();}/*** 从类中获取某方法对象* * @param name* @param cla* @return*/private static Method getMethod(String name, Class<?> cla) {Method[] mtds = cla.getMethods();for (int i = 0; i < mtds.length; i++) {if (name.equals(mtds[i].getName())) {return mtds[i];}}return null;}

到这里我们就实现了基础的查询功能了

public static void main(String[] args) throws SQLException, ClassNotFoundException {String sql="select * from (select '姓名1' user_name ,'123' password from  dual UNION select '姓名2' user_name ,'123' password from  dual  ) where user_name = #{userName} and password=#{password} ";Connection conn=getConnection();//该方法请自行实现Map<String,String> bean=new HashMap<String, String>();bean.put("userName", "姓名1");bean.put("password", "123");List<String> findForList = findForList(conn, sql, bean, String.class);conn.close();System.out.println();}

到这里我们已经实现了基础查询, 更新修改sql就留给同学们了, 接下来我们来思考hibernate,mybatis、JPA他们是如何实现的

hibernate使用配置xml文件或注解绑定对象进行查询与映射,使用HQL,其实就是在我们解析processSQL的方法抽象的另外一种实现,在结果集映射getObjectFromResultSet也换成另外一种映射实现,那么就是将这些方法都进行策略抽象即可。

mybatis使用xml或注解与hibernate同理,只是mybatis的xml配置文件可以使用OGNL(对象导航图语言(Object Graph Navigation Language))这个有兴趣的同学可以关注留言,改天也可以带大家手写实现这个OGNL(简单实现),在接口方法名与配置的sql进行绑定,使用类方射调用。

同理JPA使用的是接口方法名,按照一定的规则生成sql执行

回到正题,接下来开始手写mybatis的注解查询 ,新建注解Select与Param

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Select {String[] value();
}@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface Param {String value();
}

新建测试查询相关类

interface UserDao {@Select("select * from (select '姓名1' user_name ,'123' password from  dual UNION select '姓名2' user_name ,'123' password from  dual  ) where user_name = #{userName} and password=#{password} ")User user(@Param("userName")String userName,@Param("password") String password);@Select("select * from (select '姓名1' user_name ,'123' password from  dual UNION select '姓名2' user_name ,'123' password from  dual  ) ")List<User> users();}public static class User{String userName;String password;public String getUserName() {return userName;}public void setUserName(String userName) {this.userName = userName;}public String getPassword() {return password;}public void setPassword(String password) {this.password = password;}}

这里的类容都是利用类反射加泛型加注解的灵活运用,外加class扫描(此文不实现扫描),感觉有点手写SpringMVC框架时候controller的方法解析的味道

解析接口,将接口的所有方法查询解析返回得到Dao接口类

/*** 解析接口生成可以调用的接口实现* @param <T>* @param inClazz 接口类型* @return*/@SuppressWarnings("unchecked")public static <T> T processDaoInterface(Class<T> inClazz,final DataSource dataSource){//用来存储接口每个方法的sql,缓存高效性能,所以mybatis修改xml或者注解需要重新启动工程加载(或自定义实现插件)final Map<String,Object[]> metaData=new HashMap<String,Object[]>();Method[] declaredMethods = inClazz.getDeclaredMethods();for (Method method : declaredMethods) {String mName = method.getName();//这里仅仅实现Select注解,其他注解 就留给同学们了Select annotation = method.getAnnotation(Select.class);Annotation[][] parameterAnnotations = method.getParameterAnnotations();Map<Integer,String> paramIndexMap=new HashMap<Integer,String>();//记录方法参数的位置与名称int paramIndex=0;for (Annotation[] pA : parameterAnnotations) {//这里偷懒默认就只认为参数只要Param注解if(pA[0].annotationType()==Param.class) {String paramName = ((Param)pA[0]).value();//参数名paramIndexMap.put(paramIndex++,paramName);}}metaData.put(mName, new Object[] {annotation.value()[0],paramIndexMap});}return (T) Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), new Class<?>[] {inClazz}, new InvocationHandler() {@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {String name = method.getName();Object[] metaDataA = metaData.get(name);String sql=(String) metaDataA[0];//根据方法名称拿到SQLMap<Integer,String> paramIndexMap =(Map<Integer, String>) metaDataA[1];//参数的位置//组长基查询参数Map<Object,Object> findParam=new HashMap<Object, Object>();for (int i = 0; args!=null && i < args.length; i++) {findParam.put(paramIndexMap.get(i), args[i]);}//返回值类型Type returnType = method.getGenericReturnType();Type rawType=returnType;if (returnType instanceof ParameterizedType) {//根据接口方法返回值获取 返回值类型 与 列表的泛型ParameterizedType returnTypeP=  ((ParameterizedType) returnType);Type[] actualTypeArguments = returnTypeP.getActualTypeArguments();//泛型returnType=actualTypeArguments[0];rawType = returnTypeP.getRawType();//List类型}//策略抽象获取连接,后面可扩展事务管理,Connection conn = dataSource.getConnection();List<?> findForList = findForList(conn, sql, findParam, (Class<T>)returnType);if(rawType==List.class) {return findForList;}else {return  findForList.isEmpty()? null:findForList.get(0);}}});}

接下来就是主函数测试调用,processDaoInterface方法能根据接口类型与数据源得到一个接口实现对象(是一个动态代理类),这里可以我们思考下mybatis在与spring整合时候需要配置指向一个数据源(数据库连接池)其实就是这个,然后将所有Dao保存到IOC容器中,这里仅是一个主函数进行测试,如下:

 public static void main(String[] args) throws SQLException, ClassNotFoundException {final Connection conn = getConnection();// 该方法请自行实现String sql = "select * from (select '姓名1' user_name ,'123' password from  dual UNION select '姓名2' user_name ,'123' password from  dual  ) where user_name = #{userName} and password=#{password} ";Map<String, String> bean = new HashMap<String, String>();bean.put("userName", "姓名1");bean.put("password", "123");List<String> findForList = findForList(conn, sql, bean, String.class);UserDao processDaoInterface = processDaoInterface(UserDao.class, new DataSource() {@Overridepublic <T> T unwrap(Class<T> iface) throws SQLException {return null;}@Overridepublic boolean isWrapperFor(Class<?> iface) throws SQLException {return false;}@Overridepublic void setLoginTimeout(int seconds) throws SQLException {}@Overridepublic void setLogWriter(PrintWriter out) throws SQLException {}@Overridepublic int getLoginTimeout() throws SQLException {return 0;}@Overridepublic PrintWriter getLogWriter() throws SQLException {return null;}@Overridepublic Connection getConnection(String username, String password) throws SQLException {// TODO Auto-generated method stubreturn null;}@Overridepublic Connection getConnection() throws SQLException {return conn;}});List<User> findUser = processDaoInterface.users();User user = processDaoInterface.user("姓名1", "123");System.out.println(findUser);System.out.println(user);}

执行测试结果如下:

到这里就已经实现了mybatis使用接口上的注解进行数据库Dao查询操作,同理其他原理一致,本文中的代码并不与原开源框架相同,只是框架功能的简单实现,框架只是对API的封装,使用了设计模式可对原本的框架进行插件扩展,多个层切面可随意替换,但总体流程是缺少不了的。

下篇将介绍数据库查询跨Dao的事务管理器是什么,spring的事务管理器,事务传播是如何实现的,进阶分布式事务管理器要如何实现。

最后,其实这些代码就是我现场手写一行,然后写一行博客内容(大家只要将代码拷贝到一个类文件中就可以执行的),其实花时间的(我偷懒了,也并无经过健壮性测试,是存在bug的,仅限于学习使用),至于前面提到的OGNL(对象导航图语言(Object Graph Navigation Language))这块相当于一门解释行语言,这块的实现还是有点意思的,提示下,例如需要数据结构"栈"的灵活运用,解析逻辑标签等,这块的原理相当于实现一门解释语言的味道。

看了该文章,如果喜欢的,请给我关注加评论支持下,谢谢!

另外该文的源码文件下载地址 https://download.csdn.net/download/qq_18497293/13755283

手写数据库查询框架ORM相关推荐

  1. 博主熬夜手写个SpringMVC框架

    博主熬夜手写个SpringMVC框架 前言: 在spring全家桶流行的当下,只要你做的是Java技术栈基本上95%以上都使用的spring或springboot框架,剩下的5%基本上是一些老项目,政 ...

  2. 认识Vue源码 (2)-- 手写类Vue框架:Zue

    一.手写类Vue框架:zue class Zue{constructor(options){//构造函数:this永远指向实例} } 1.在zue实例下创建$el,并指向挂载点 this.$el = ...

  3. 【RPC框架、RPC框架必会的基本知识、手写一个RPC框架案例、优秀的RPC框架Dubbo、Dubbo和SpringCloud框架比较】

    一.RPC框架必会的基本知识 1.1 什么是RPC? RPC(Remote Procedure Call --远程过程调用),它是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络的技术. ...

  4. 05. 手写Spring核心框架

    目录 05 手写Spring核心框架 Pt1 手写IoC/DI Pt1.1 流程设计 Pt1.2 基础配置 application.properties pom.xml web.xml Pt1.3 注 ...

  5. 自己手写一个Mybatis框架(简化)

    继上一篇手写SpringMVC之后,我最近趁热打铁,研究了一下Mybatis.MyBatis框架的核心功能其实不难,无非就是动态代理和jdbc的操作,难的是写出来可扩展,高内聚,低耦合的规范的代码.本 ...

  6. 手写一个RPC框架,理解更透彻(附源码)

    作者:烟味i www.cnblogs.com/2YSP/p/13545217.html 一.前言 前段时间看到一篇不错的文章<看了这篇你就会手写RPC框架了>,于是便来了兴趣对着实现了一遍 ...

  7. 我居然手写了Spring框架

    手写完了 刚参加工作那会接触java还是用的struct的时代,后面在SSH火爆时代的时候我转战.net,多年之后公司转java技术栈已经是Spring的天下,源码嚼了很多遍于是很想尝试把这套东西用在 ...

  8. 手写图片缓存框架 ImageLoader

    图片缓存是App开发中最常见的,本篇博文给大家带来自己手写的图片缓存框,大致的思路很简单,首先从内存中获取图片,如果内存中没有,就从手机本地进行获取,如果还没有,就从网络访问进行获取. 所以,我们在I ...

  9. 手写迷你SpringMVC框架

    前言 本文总结自慕课网同名课程. 学习如何使用Spring,SpringMVC是很快的,但是在往后使用的过程中难免会想探究一下框架背后的原理是什么,本文将通过讲解如何手写一个简单版的springMVC ...

最新文章

  1. 员工信息管理系统java6_职工信息管理系统java源代码【可修改】.doc
  2. css03层次选择器
  3. vue+Mint-ui实现登录注册
  4. 使用组策略中的首选项更改域中计算机注册表
  5. Java 实现基于 UDP 的简单 socket 通信
  6. php fpm 调试模式,调试 – nginx php-fpm xdebug netbeans只能启动一个调试会话
  7. 论文解读二十七:文本行识别模型的再思考
  8. 条件测试 linux,Linux的条件测试
  9. gbk编码在线转换工具_珍藏的4个PDF格式转换网站「在线工具,无需下载,还免费哦。」...
  10. linux双机热备软件Rose,Linux Rose HA 双机热备软件原理
  11. 笔者分享:关于Win7 XPS查看器的详细介绍【386w】
  12. 两平面间8参数变换参数求解简单原理解析(已更新间接平差算法)
  13. java玫瑰花代码_给爱人的玫瑰花表白程序代码–Java版 | 学步园
  14. 计算机时钟同步的原理,华为以太网时钟同步原理介绍(二)
  15. Self-supervised Heterogeneous Graph Neural Network with Co-contrastive Learning 论文阅读
  16. 计算机一级幻灯片版式,ppt2010官方基础教程:添加不同版式幻灯片-powerpoint技巧-电脑技巧收藏家...
  17. 中科大计算机学院推免拟录取,中科大2019年录取177所高校推免生2109人,外校985生源不足三成...
  18. 程序员的焦虑!承认吧!你不是不行,你是不敢
  19. java简历项目经验描述,2021大厂面试合集
  20. Codeforces.1139D.Steps to One(DP 莫比乌斯反演)

热门文章

  1. Android Instant App调研报告
  2. 场景中配置阴影(个人笔记)
  3. hanlp自然语言处理包的使用
  4. IMDb Large Movie Review-数据集
  5. 王爽 汇编语言第二版 课程设计2
  6. ubuntu下修复U盘并格式化
  7. 百度AI攻略:银行卡识别
  8. 【笔记】学习CSS布局17——column
  9. 《学术研究,你的成功之道》
  10. 台式计算机读取不了移动硬盘,移动硬盘插入win7电脑一直无法识别的几种原因和解决方法...