Spring JDBC的优雅设计 - 数据转换
Posted 蘑菇君520
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Spring JDBC的优雅设计 - 数据转换相关的知识,希望对你有一定的参考价值。
原生JDBC之痛
在上一篇中分析了Java里的JDBC规范,规范里面的抽象设计的确实很优雅。但是规范这种东西嘛,比较形而上,实际在项目中使用起来,还是挺繁琐。
下面是使用JDBC查询课程表的代码:
/**
* 查询课程
* course表 列id, name, type, score, desc
*/
public List<Course> findCourseList()
String sql = "select * from course order by course_id";
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
//创建一个集合对象用来存放查询到的数据
List<Course> courseList = new ArrayList<>();
try
conn = DbUtil.getConnection();
pstmt = (PreparedStatement) conn.prepareStatement(sql);
rs = (ResultSet) pstmt.executeQuery();
while (rs.next())
int courseId = rs.getInt("id");
String courseName = rs.getString("name");
//每个记录对应一个对象
Course course = new Course();
course.setId(courseId);
course.setName(courseName);
// 省略...
//将对象放到集合中
courseList.add(course);
catch (SQLException e)
// TODO: handle exception
e.printStackTrace();
finally
DbUtil.close(pstmt);
DbUtil.close(conn); //必须关闭
return courseList;
从上面的代码,我们可以看出不方便的几处:
- 要自己处理结果集,将每行数据封装成实体对象Course
- 要自己处理各种异常。如果报错信息要精确,比如网络超时,查询条件有问题等等,还要自己处理
SQLException,根据里面的状态码来获得更详细的报错信息 - 要自己管理数据库资源,比如连接,Statement。每次都能去获取一个连接,用完再手动关闭。
Spring框架对JDBC做了一层封装,提供了一套更加简洁优雅的调用方式,帮我们免去了上面那些繁琐的操作。在欣赏Spring JDBC之前呢,老规矩,先看看蘑菇君会怎么去解决这些问题(~ ̄▽ ̄)~ 。好,又到了见证翻车的时刻了~
这一篇先分析数据转化,也就是上面的第一点,如何优雅的处理结果集,将每行数据封装成实体对象。
蘑菇君翻车时刻
在上面的代码中,从ResultSet中读取出一行数据,我们是手动创建一个Course的实体对象,然后将行数据的每一列跟Course类中的属性映射起来。
细细品味这句话,将数据库每行的数据跟对象的属性映射起来。蘑菇君首先想到的是,这个行为不就可以抽象成一个接口么?具体怎么个映射法,那就是这个接口的实现类们该干的事儿。那这个接口的输入输出是啥呢?接着分析哪句话,不难想到,输入就是一张表的每行数据,输出就是一个映射好的对象。
那怎么拿到每行数据呢?ResultSet
接口可以通过next()
方法移动游标,遍历查找。那么我们就可以通过ResultSet
加上行的index来表示某一行。
而输出的映射对象,其实是不确定的。我们可以用泛型T来表示。所以接口设计如下:
public interface RowMapper<T>
T mapRow(ResultSet rs, int rowIndex);
接口出来了,想想该怎么实现这个映射过程。蘑菇君想到的一个实现是,将每行数据按每列的列名,找到映射类的相应属性,然后通过反射机制赋值。伪代码如下:
public class ObjectPropertyRowMapper<T> implements RowMapper<T>
// 通过Class类来反射
private Class<T> mappedClass;
public T mapRow(ResultSet rs, int rowIndex)
// 1. 遍历该行的所有列
int columnCount = rs.getMetaData().getColumnCount();
for (int index = 1; index <= columnCount; index++)
// 2. 获取列名和列值
String column = getColumnName(rs, index);
Object value = getColumnValue(rs, index);
// 3. 通过每列的列名找到映射类的对应属性
String propertyName = findPropertyNameByColumn();
// 4. 通过反射机制,拿到构造器,创建映射对象
T mappedObject = initObject(mappedClass);
// 5. 将该列的值赋给该属性
setObjectPropertyValue(mappedObject, propertyName, value);
嘿嘿嘿,伪代码写起来还是很容易滴,过程也很清晰。但是,其中的细节还是挺多的,比如:
- 通过每列的列名找到映射类的对应属性。这其实牵扯到了映射策略。有的数据表的列命名方式是:小写加下划线,如
university_course
,有的列命名是驼峰形式,如UniversityCourse
。显然,这里可以用到策略模式来配置不同的映射策略。 - 将列的值赋给该属性。这里要考虑类型匹配和转换。比如数据列是Date类型,但是映射的属性是String类型,我们得考虑这种类型不匹配的情况,是进行转换呢,还是直接抛出异常。
我们实现好这个转换类以后,该怎么用呢?我们现在可以自动将行数据转化成映射对象了,所以可以搞个辅助类来包装一下:
public class JdbcHelper
public <T> List<T> getList(ResultSet rs, Class<T> mappedClass)
List<T> results = new ArrayList<>();
RowMapper<T> rowMapper = new ObjectPropertyRowMapper(mappedClass);
int rowIndex = 0;
while(rs.next())
results.add(rowMapper.mapRow(rs, rowIndex++));
return results;
经过蘑菇君这一顿骚操作以后,之前的原始JDBC写法的代码就变成了:
// 原生写法
public List<Course> findCourseList()
...
rs = (ResultSet) pstmt.executeQuery();
while (rs.next())
int courseId = rs.getInt("id");
String courseName = rs.getString("name");
//每个记录对应一个对象
Course course = new Course();
course.setId(courseId);
course.setName(courseName);
// 省略...
//将对象放到集合中
courseList.add(course);
...
// 蘑菇君写法
public List<Course> findCourseList()
...
rs = (ResultSet) pstmt.executeQuery();
List<Course> courseList = new JdbcHelper().getList(rs, Course.class);
...
哦豁,感觉自己厉害坏了!我感觉Spring也就这样了!后面的,爱看不爱吧!
…
咳咳咳,有点狂了…那啥吧,还是看看Spring是怎么做的吧~
Spring JDBC 中的数据转换
(下面代码来自于spring-jdbc:5.1.4.RELEASE
)
同样的,Spring也有一个数据映射的接口:
@FunctionalInterface
public interface RowMapper<T>
@Nullable
T mapRow(ResultSet rs, int rowNum) throws SQLException;
它被声明是个函数式接口,可以用Lambda表达式来简写调用。比如:
List<Course> courseList = jdbcTemplate.query("SELECT * FROM course", (rs, rowNum) ->
int courseId = rs.getInt("id");
String courseName = rs.getString("name");
Course course = new Course();
course.setId(courseId);
course.setName(courseName);
);
Spring中, RowMapper 有3个实现类:
- BeanPropertyRowMapper
- ColumnMapRowMapper
- SingleColumnRowMapper
其中BeanPropertyRowMapper
这个类嘛,其实就是抄袭蘑菇君我上面的设计(真不要脸),通过反射来映射行数据和实体类。流程也跟上面伪代码中的一致。
ColumnMapRowMapper
实现很简单,就是将行数据转成了一个Map<ColumnName, ColumnValue>
结构:
public class ColumnMapRowMapper implements RowMapper<Map<String, Object>>
SingleColumnRowMapper
实现方式就是BeanPropertyRowMapper
的一种特殊case,处理查询结果只有一列的情况。
我们重点来看看BeanPropertyRowMapper
的实现。
BeanPropertyRowMapper
我们来看看这个类是怎么解决上面提到的两个问题的。
列名映射到类的属性
这一点,BeanPropertyRowMapper
只做了简单的处理:默认将类属性名转成小写形式,以及从驼峰转成下划线的命名风格,将这两者跟列名进行匹配。
protected void initialize(Class<T> mappedClass)
this.mappedClass = mappedClass;
this.mappedFields = new HashMap<>();
this.mappedProperties = new HashSet<>();
PropertyDescriptor[] pds = BeanUtils.getPropertyDescriptors(mappedClass);
for (PropertyDescriptor pd : pds)
if (pd.getWriteMethod() != null)
// 属性名转成小写,存一份, courseId -> courseid
this.mappedFields.put(lowerCaseName(pd.getName()), pd);
// 驼峰转成下划线,存一份, courseId -> course_id
String underscoredName = underscoreName(pd.getName());
if (!lowerCaseName(pd.getName()).equals(underscoredName))
this.mappedFields.put(underscoredName, pd);
this.mappedProperties.add(pd.getName());
为啥子这里的实现这么简单,不考虑扩展性呢?其实也有办法,lowerCaseName
和underscoreName
方法都是protected的,我们可以继承这个类,然后重写这两个方法来实现自己的映射逻辑。但是吧,你看着两个方法名,一看就是不太情愿被我们重写…
其次,这个类用得少,而且Spring后面也提供了更灵活优雅的配置方式。具体的,会再写其他文章来说明。
类型转换
上面提到过,将数据库里的类型转换成Java里的类型,肯定也得要留出接口,让我们去扩展,实现自己业务的类型转换逻辑。这一次,Spring没有偷懒了,BeanPropertyRowMapper
里有个ConversionService
类提供这种服务:
public class BeanPropertyRowMapper<T> implements RowMapper<T>
/** ConversionService for binding JDBC values to bean properties. */
@Nullable
private ConversionService conversionService =
DefaultConversionService.getSharedInstance();
看看这个接口:
public interface ConversionService
boolean canConvert(@Nullable Class<?> sourceType, Class<?> targetType);
boolean canConvert(@Nullable TypeDescriptor sourceType, TypeDescriptor targetType);
<T> T convert(@Nullable Object source, Class<T> targetType);
<T> T convert(@Nullable Object source, Class<T> targetType);
Object convert(@Nullable Object source, @Nullable TypeDescriptor sourceType,TypeDescriptor targetType);
接口方法很直观,canConvert
判断一个类是否能转换成另外一个类,convert
将一个类型对象转成另一个类型对象。
这个接口不在spring-jdbc
包下,而是spring-core
包下面。这意味着这个转换接口是Spring提供的的通用类型转换接口。不过细想也是,将一个类对象转换成另一个类对象,这的确是个通用的功能。所以这里挖个坑先,后续单独写一篇Spring的类型转换文章。╮( ̄▽ ̄)╭
题外话
我是蘑菇君,冬天太冷,手冷jio冷,我为自己点火
以上是关于Spring JDBC的优雅设计 - 数据转换的主要内容,如果未能解决你的问题,请参考以下文章