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;
	

从上面的代码,我们可以看出不方便的几处:

  1. 要自己处理结果集,将每行数据封装成实体对象Course
  2. 要自己处理各种异常。如果报错信息要精确,比如网络超时,查询条件有问题等等,还要自己处理
    SQLException,根据里面的状态码来获得更详细的报错信息
  3. 要自己管理数据库资源,比如连接,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());
			
		
	

为啥子这里的实现这么简单,不考虑扩展性呢?其实也有办法,lowerCaseNameunderscoreName方法都是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的优雅设计 - 数据转换的主要内容,如果未能解决你的问题,请参考以下文章

Spring JDBC的优雅设计 - 异常封装(下)

Spring JDBC的优雅设计 - 异常封装(下)

Spring JDBC的优雅设计 - 数据转换

Spring JDBC的优雅设计 - 数据转换

Java JDBC的优雅设计

Java JDBC的优雅设计