原生JDBC简单实现Mybatis核心功能

程序浅谈 后端 2024-09-24

原生JDBC简单实现Mybatis核心功能

前言

之前在Vertx项目中使用Tdengine,但Vertx没有Tdengine的异步JDBC驱动。所以只能使用Tdengine提供的同步JDBC驱动配合vertx.executeBlocking实现异步数据库查询。
原生的JDBC在SQL参数绑定和返回数据映射时很不方便。但当时第一印象是Mybatis和Spring结合太紧密了(实际是可以的哈),所以自己写了一个简单的SQL解析和返回数据映射的简单JDBC工具。但不能像Mybaits那样支持复杂的嵌套的类参数绑定和映射,但已经够用了。

首先要实现的功能:

  • 实现SQL参数绑定,支持实体类和MAP绑定到SQL
  • 实现返回值映射到实体类

实现效果:java

代码解读
复制代码
public class PropertyMapper extends BaseMapper { public PropertyMapper(DataSource dataSource) { super(dataSource); } public Integer add(PropertyEntity propertyEntity) { // 实体类参数绑定到SQL return update("INSERT INTO property VALUES (#{ts}, #{value});", propertyEntity); } public List<PropertyEntity> page(Long startTime, Long endTime, Long start, Long size) { Map<String, Object> params = new MapBuilder().put("startTime", startTime) .put("endTime", endTime) .put("start", start) .put("size", size) .build(); // MAP参数绑定到SQL return selectList("SELECT ts, value, property_code FROM property WHERE ts >= #{startTime} AND ts <= #{endTime} ORDER BY ts DESC LIMIT #{size} OFFSET #{start}", params, PropertyEntity.class); } public class PropertyEntity { /** * 时间戳, 精确到ms */ @TableField(typeHandler = OffsetDateTimeToLongHandler.class, value = "ts") private Long ts; /** * 值, 不同产品属性定义的超级表值类型不同 */ private Object value; /** * 属性编码 */ private String propertyCode; }

对比下原生实现,原生的很多重复代码,同时预编译SQL中参数占位符号都是?区分度差了,每次写的时候还要将参数和?的序列对应起来(JDBC中?index从1开始)可能导致绑定参数错误,java

代码解读
复制代码
PreparedStatementpsmt = null; ResultSetrs = null; try { Connectionconn = null; conn =JdbcUtils.getConnection(); Stringsql = "SELECT ts, value, property_code FROM property WHERE ts >= ? AND ts <= ? ORDER BY ts DESC LIMIT ? OFFSET ?"; psmt =conn.prepareStatement(sql); //绑定参数到预编译SQL psmt.setLong(1,startTime); psmt.setLong(1,endTime); psmt.setLong(1,start); psmt.setLong(1,size); rs =psmt.executeQuery(); List<PropertyEntity> result = new ArrayList<>(); while(rs.next()){ //解析返回值 Long ts = rs.getLong("ts"); Object value = rs.getObject("value"); String propertyCode = rs.getObject("property_code"); PropertyEntity property = new PropertyEntity(); property.setTs(ts); property.setValue(value); property.setPropertyCode(propertyCode); result.add(property); } return result; } catch(Exception e) { throw new RuntimeException(e); } finally{ JdbcUtils.closeResource(conn,psmt, rs); }

实现SQL参数绑定,支持实体类和MAP绑定到SQLjava

代码解读
复制代码
INSERT INTO property VALUES (#{ts}, #{value}); SELECT ts, value, property_code FROM property WHERE ts >= #{startTime} AND ts <= #{endTime} ORDER BY ts DESC LIMIT #{size} OFFSET #{start}
  1. 第一步将SQL中类似#{ts}替换成?,同时获取到参数占位标识符ts
  2. 解析传进来的参数,如果是实体类就转换成Map,Key是属性名称,Value是属性值。如果是Map就进行第三步
  3. 第1步获取的参数占位符ts,从第二步解析到的参数Map中获取到参数值存储到顺序List中
  4. 填充预编译SQL参数值java
代码解读
复制代码
/** * obj根据属性名映射到sql 占位符 #{ts} */ public static SqlAndParamDTO objMapToSqlParam(String sql, Object po) { return toSqlParam(sql, poToMap(po, false)); } /** * map参数更加key映射到sql */ public static SqlAndParamDTO toSqlParam(String sql, Map<String, Object> paramMap) { if (paramMap == null || paramMap.isEmpty()) { return SqlAndParamDTO.builder().paramList(Collections.emptyList()).sql(sql).build(); } List<Object> params = new ArrayList<>(); String regex = "#\{\s*([^\{\}\s]*)\s*\}"; Pattern pattern = Pattern.compile(regex); Matcher matcher = pattern.matcher(sql); StringBuilder sb = new StringBuilder(); String placeholder = "?"; while (matcher.find()) { //占位符 String poParamName = matcher.group(1); Object paramValue = paramMap.get(poParamName); matcher.appendReplacement(sb, placeholder); params.add(paramValue); } matcher.appendTail(sb); return SqlAndParamDTO.builder().paramList(params).sql(sql).build(); } /** * 将po转Map * 属性名称--->属性值 * * @param po 需要转换的对象 * @param underScore 是否取下划线即注解值 */ public static Map<String, Object> poToMap(Object po, Boolean underScore) { if (po == null) { return Collections.emptyMap(); } Map<String, Object> paramMap = new HashMap<>(16); // getField只能够获取类的共有属性字段 也就是public修饰的 Field[] fields = FieldUtil.getAllFields(po.getClass()); for (Field field : fields) { field.setAccessible(true); TableField tableField = field.getDeclaredAnnotation(TableField.class); String fieldName = underScore ? tableField.value() : field.getName(); try { paramMap.put(fieldName, field.get(po)); } catch (IllegalAccessException e) { throw new RuntimeException("参数绑定出错"); } } return paramMap; } public static PreparedStatement getPreparedStatement(Connection connection, String sql, List params) throws SQLException { PreparedStatement preparedStatement = connection.prepareStatement(sql); for (int i = 1; i <= params.size(); i++) { //jdbc预编译参数从1起步 preparedStatement.setObject(i, params.get(i - 1)); } return preparedStatement; } @Data @Builder public static class SqlAndParamDTO { /** * 生成的预编译sql */ public String sql; /** * 生成对应预编译参数位置的数组 */ private List paramList; }

这里没做集合类型支持,比如当参数类型为List时可以生成对应数量的?,golang中的gorm也是这样实现的。

核心BaseMapper
很神奇只需要写两个方法就可以解决所有SQL查询了
SQL只有两类更新SQL和查询SQL,分别对应JDBC的 preparedStatement.executeQuery(),reparedStatement.executeUpdate()。对应我们的也就两个函数:selectList和updatejava

代码解读
复制代码
public class BaseMapper { private DataSource dataSource; /** * getOne之类查询 * * @param sql sql * @param valueMap 参数, 因为查询一般参数都是零散的没有提供Obj参数绑定方法要写页很简单 * @param rClass 返回值类型 */ public <R> R selectOne(String sql, Map<String, Object> valueMap, Class<R> rClass) { List<R> result = selectList(sql, valueMap, rClass); if (result == null || result.isEmpty()) { return null; } if (result.size() == 1) { return result.get(0); } throw new RuntimeException("except one but more than one"); } /** * 查询list * * @param sql sql * @param valueMap 参数 * @param rClass 返回值类型 */ public <R> List<R> selectList(String sql, Map<String, Object> valueMap, Class<R> rClass) { ParamUtil.SqlAndParamDTO sqlAndParamDTO = ParamUtil.toSqlParam(sql, valueMap); Connection connection = null; PreparedStatement preparedStatement = null; ResultSet resultSet = null; try { connection = dataSource.getConnection(); preparedStatement = getPreparedStatement(connection, sqlAndParamDTO.getSql(), sqlAndParamDTO.getParamList()); resultSet = preparedStatement.executeQuery(); return ResultConvertUtil.listFromResultSet(resultSet, rClass); } catch (Exception e) { throw new RuntimeException(e); } finally { close(resultSet, preparedStatement, connection); } } /** * 更新sql: insert, delete, update * * @param sql sql * @param paramObj Obj参数 * @return {@link Future}<{@link Integer}> */ public Integer update(String sql, Object paramObj) { //将Object转换为Map return update(sql, ParamUtil.poToMap(paramObj, false)); } /** * 更新sql: insert, delete, update * * @param sql sql * @param paramMap map类型参数 * @return {@link Future}<{@link Integer}> */ public Integer update(String sql, Map<String, Object> paramMap) { ParamUtil.SqlAndParamDTO sqlAndParamDTO = ParamUtil.toSqlParam(sql, paramMap); Connection connection = null; PreparedStatement preparedStatement = null; try { connection = dataSource.getConnection(); preparedStatement = getPreparedStatement(connection, sqlAndParamDTO.getSql(), sqlAndParamDTO.getParamList()); return preparedStatement.executeUpdate(); } catch (Exception e) { throw new RuntimeException(e); } finally { //关闭资源让线程池回收 close(null, preparedStatement, connection); } } }

实现返回值映射到实体类Tdengine返回的是ResultSet和其他JDBC驱动有些不同,Postgres返回的是RowSet rowSet但实现是一样的。主要步骤就是:

  1. 从返回的数据行中解析出列名称-列值
  2. 反射出需要返回的对象,根据属性值和列名称对应起来给属性赋值java
代码解读
复制代码
/** * 从resultSet获取结果 * @param resultSet * @param targetClass * @return {@link List}<{@link R}> * @throws SQLException */ public static <R> List<R> listFromResultSet(ResultSet resultSet, Class<R> targetClass) throws SQLException { if (resultSet == null) { return Collections.emptyList(); } List<R> resultList = new ArrayList<>(20); //如果是基本类型或者String直接取第一个返回 if (CommonUtil.isPrimitive(targetClass) || String.class == targetClass) { while (resultSet.next()) { if (String.class == targetClass) { resultList.add((R) resultSet.getString(1)); } else { resultList.add((R) resultSet.getObject(1)); } } return resultList; } //如果是对象映射结果返回 while (resultSet.next()) { Map<String, Object> sqlRowToValueMap = new HashMap<>(8); ResultSetMetaData rowMetaData = resultSet.getMetaData(); int columnCount = rowMetaData.getColumnCount(); for (int i = 1; i <= columnCount; i++) { sqlRowToValueMap.put(rowMetaData.getColumnLabel(i), resultSet.getObject(i)); } resultList.add(mapToObj(sqlRowToValueMap, targetClass)); } return resultList; } /** * map值映射Obj * * @param valueMap * @param rClass * @return {@link R} */ public static <R> R mapToObj(Map<String, Object> valueMap, Class<R> rClass) { try { //反射出对象 R r = rClass.getDeclaredConstructor().newInstance(); Field[] fields = FieldUtil.getAllFields(rClass); for (Field field : fields) { field.setAccessible(true); TableField tableField = field.getDeclaredAnnotation(TableField.class); TypeHandler<?> typeHandler = null; if (tableField != null && tableField.typeHandler() != null) { typeHandler = tableField.typeHandler().getDeclaredConstructor().newInstance(); } //如果有注解用注解没有用字段名改为下划线,支持注解定义别名。同时避免数据库short和java int long转换异常提供了变量提升 String realFieldName = tableField == null ? poParamNameToDb(field.getName()) : tableField.value(); field.set(r, typeHandler == null || typeHandler instanceof UnknownTypeHandler ? varLift(valueMap.get(realFieldName), field.getType()) : typeHandler.getResult(valueMap.get(realFieldName))); } return r; } catch (Exception e) { throw new RuntimeException("sql result to Object error:", e); } } /** * 变量提升 * * @param oldVar * @param targetClass * @return {@link Object} */ public static Object varLift(Object oldVar, Class<?> targetClass) { if (oldVar == null) { return null; } if (oldVar instanceof Integer && targetClass.equals(Long.class)) { return ((Integer) oldVar).longValue(); } if (oldVar instanceof Short && targetClass.equals(Long.class)) { return ((Short) oldVar).longValue(); } if (oldVar instanceof Short && targetClass.equals(Integer.class)) { return ((Short) oldVar).intValue(); } if (targetClass.equals(String.class) && oldVar instanceof byte[]) { return new String((byte[]) oldVar, StandardCharsets.UTF_8); } return oldVar; }

总结

基本实现了Mybatis核心功能,其实不复杂,但够用了。但是Mybatis支持的太多啦,加上需要整合SpringBoot所以比较复杂。

转载来源:https://juejin.cn/post/7408849436431073316

Apipost 私有化火热进行中

评论