手写数据库查询框架ORM

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

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

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

这里复习下JDBC的流程

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

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

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

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

/**
	 * 基础查询,需要处理结果集映射
	 * @param 
	 * @param conn
	 * @param sql
	 * @param params
	 * @param resultSetHandler
	 * @return
	 * @throws SQLException
	 */
	public static  T find(Connection conn, String sql, Map params, ResultSetHandler resultSetHandler) throws SQLException {
		//解析sql/HQL
		Map result=new HashMap();
		String processSQL = processSQL(sql, result);
		ResultSet rs=null;
		PreparedStatement pst = null;
		try {
			//连接获取预编译对象
			pst = conn.prepareStatement(processSQL);
			//设置查询参数params
			setPreparedStatementParams(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 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 result, Map params) throws SQLException {
		for (Entry 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的查询结果集映射,我们开始创建以下方法(有的方法未实现,有红色底线提示,不要紧,还是老话,我们要的是抽象编程)

/**
	 * 高级查询支持javaBean自动映射,支持复杂参数
	 */
	public static  List findForList(Connection conn, String sql, Object bean, final Class clazz) throws SQLException {
        //解析Bean参数转换为前者基础查询的Map参数
		Map paramConvert = paramConvert(bean);
		//调用基础查询方法
		List find = find(conn, sql, paramConvert, new ResultSetHandler>() {
			@SuppressWarnings({ "unchecked" })
			@Override
			public List doHandler(ResultSet rs) throws SQLException {
				List list = new ArrayList();
				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 paramConvert(Object params) {
		if (params == null) {
			params = new HashMap();
		}
		boolean isMap = false;
		Class 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) params;
	}

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

private static  T getObjectFromResultSet(ResultSet rs, Class 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.StringUtils
			sb.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 bean=new HashMap();
		bean.put("userName", "姓名1");
        bean.put("password", "123");
		List 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 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 
	 * @param inClazz 接口类型
	 * @return
	 */
	@SuppressWarnings("unchecked")
	public static  T processDaoInterface(Class inClazz,final DataSource dataSource){
		//用来存储接口每个方法的sql,缓存高效性能,所以mybatis修改xml或者注解需要重新启动工程加载(或自定义实现插件)
		final Map metaData=new HashMap();
		
		Method[] declaredMethods = inClazz.getDeclaredMethods();
		for (Method method : declaredMethods) {
			String mName = method.getName();
			//这里仅仅实现Select注解,其他注解 就留给同学们了
			Select annotation = method.getAnnotation(Select.class);
			Annotation[][] parameterAnnotations = method.getParameterAnnotations();
			Map paramIndexMap=new HashMap();//记录方法参数的位置与名称
			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() {
			
			@Override
			public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
				
				String name = method.getName();
				Object[] metaDataA = metaData.get(name);
				String sql=(String) metaDataA[0];//根据方法名称拿到SQL
				Map paramIndexMap =(Map) metaDataA[1];//参数的位置
				
				//组长基查询参数
				Map findParam=new HashMap();
				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)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 bean = new HashMap();
		bean.put("userName", "姓名1");
		bean.put("password", "123");
		List findForList = findForList(conn, sql, bean, String.class);

		UserDao processDaoInterface = processDaoInterface(UserDao.class, new DataSource() {

			@Override
			public  T unwrap(Class iface) throws SQLException {
				return null;
			}

			@Override
			public boolean isWrapperFor(Class iface) throws SQLException {
				return false;
			}

			@Override
			public void setLoginTimeout(int seconds) throws SQLException {

			}

			@Override
			public void setLogWriter(PrintWriter out) throws SQLException {

			}

			@Override
			public int getLoginTimeout() throws SQLException {
				return 0;
			}

			@Override
			public PrintWriter getLogWriter() throws SQLException {
				return null;
			}

			@Override
			public Connection getConnection(String username, String password) throws SQLException {
				// TODO Auto-generated method stub
				return null;
			}

			@Override
			public Connection getConnection() throws SQLException {
				return conn;
			}
		});

		List 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

 

你可能感兴趣的:(教你手写各种Java框架,java,后端,mybatis,mysql,spring)