【开发笔记】老生常谈之:结果集转换为PO/VO

【开发笔记】老生常谈之:结果集转换为PO/VO

(1)背景

  近期在研究Java工程模板的中间,发现了一个不可靠、又绕不开的问题:结果集(ResultSet)转换为PO/VO。经过一段时间的研究,颇有心得,特意记下来。
对于Java对象,定义实在太多,POJO/PO/EO/DO/VO,所以事先说明一下。

  • PO指的是用于数据存储的类,与数据库对象的对应关系是:类对表,字段(或叫属性)对列;JPA规范使用注解@Entity和@Table来定义类,注解@Column来修饰字段。PO对应单个数据表。
  • VO指的是用于数据展示的类,其与PO的关系,类似于数据库中视图(VIEW)与单表的关系。VO既可以是对PO字段的重组,也可以多个PO对象的组合(连表)。

  另外对于字段、属性和列的定义说明如下:

  • 字段(Field)是Java反射机制的定义,用于描述类的成员变量
  • 属性(Property)是Java Bean的定义,用于描述bean的成员变量,大多数场合可以和字段通用
  • 列(Column)是数据库对象中的定义,是数据表的基本组成项
(2)思路

  对于工程模板,初步的思路是:

项目内容
环境JDK:1.8,MySQL
应用框架Spring Boot (2.x)
安全框架Apache Shiro
数据库连接池Druid
数据存取组件1增删改查:Spring Data JPA + Hibernate
数据存取组件2复杂查询:Spring JDBC
JAVA对象转JSONfastjson

  其中,使用Spring Data JPA + Hibernate的组合的目的就是偷懒,对于普通的增删改查,这个组合非常的简便。其主要的缺点就是没法轻松自如地应对复合查询(虽然可以使用NativeQuery的方式)。
  所以,补充Spring JDBC的目的就是为了应对复杂的查询并转换为自定义VO。如何获取查询结果并转换为Java对象就成了其中重要的一环。

  实际上呢,在Spring JDBC框架中,提供了JdbcTemplate来实现sql的查询并转换成Java对象的方法。例如:

public abstract class VoBaseDAO extends NamedParameterJdbcDaoSupport {
	protected <T> List<T> findAll(String sql, Class<T> cls, String callingTag) {
		return getJdbcTemplate().query(sql, new BeanPropertyRowMapper<T>(cls));
	}
}

  其中BeanPropertyRowMapper就是将结果集中的一行转换为Bean的转换器。
  其方法mapRow方法就是将结果集中的每行中的列转换成目标bean(VO)的属性。

public T mapRow(ResultSet rs, int rowNumber) throws SQLException

  如此一来,转换思路也是比较清晰了:
(0)依据目标类(bean或vo)通过反射机制获取其字段(Field)列表信息,例如:字段名和类型。

(1)执行SQL,获取结果集(ResultSet)。
(2)依据结果集的元数据信息获取数据列的信息,例如:列名和类型。
(3)结果集逐行处理,每一行转换成一个bean或vo,从而实现将结果集转换成bean或vo列表。
(4)(3)中要做的就是(0)中的字段与(2)中的列进行对应,将列值转换成字段类型,
例如: varchar --> String, datetime --> Date,
进而调用字段的setter方法来设置bean/vo的属性/字段值。

(3)问题

  然而在实践中,缺发现转换过程并不平坦,主要是(4)会出现一些状况:
通过列名对应字段是一个推测的过程,其匹配代码如下:

for (int index = 1; index <= columnCount; index++) {
	String column = JdbcUtils.lookupColumnName(rsmd, index);
	String field = lowerCaseName(StringUtils.delete(column, " "));
	PropertyDescriptor pd = (this.mappedFields != null ? this.mappedFields.get(field) : null);
	if (pd != null) {
		try {
			Object value = getColumnValue(rs, index, pd);
			if (rowNumber == 0 && logger.isDebugEnabled()) {
				……
			}
			try {
				bw.setPropertyValue(pd.getName(), value);
			}

其规则就是将列名小写(例如:biz_type),在一个已存在的Map进行匹配。
该mappedFields的初始化在转换器的初始化方法中:

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) {
			this.mappedFields.put(lowerCaseName(pd.getName()), pd);
			String underscoredName = underscoreName(pd.getName());
			if (!lowerCaseName(pd.getName()).equals(underscoredName)) {
				this.mappedFields.put(underscoredName, pd);
			}
			this.mappedProperties.add(pd.getName());
		}
	}
}

其会按照bean/VO的字段名来“预先”生成列名,例如:bizTyp会生成两条记录:biztype和biz_type。如此一来,列名就会和第2条记录匹配上。以下代码是underscoreName的方法:

protected String underscoreName(String name) {
	if (!StringUtils.hasLength(name)) {
		return "";
	}
	StringBuilder result = new StringBuilder();
	result.append(lowerCaseName(name.substring(0, 1)));
	for (int i = 1; i < name.length(); i++) {
		String s = name.substring(i, i + 1);
		String slc = lowerCaseName(s);
		if (!s.equals(slc)) {
			result.append("_").append(slc);
		}
		else {
			result.append(s);
		}
	}
	return result.toString();
}

但是,如果列名为:reverse_1,字段/属性名为 reverse1,这样就会出现列名与字段名匹配不上的问题。因为:reverse_1不在(reverse1)里头。

按照通常的约定,数据库列名采用小写、下划线分隔的命名规范;字段/属性名采用驼峰的规范。
从列名推测字段名/属性名,要比从字段名/属性名推测列名要靠谱一些。例如:
biz_type,推测字段名为bizType;reverse_1,推测字段名为reverse1。

此外,对于逻辑型字段,数据库中可能通过整型或字符来定义,例如:is_force(是否强制)
其在Java对象的定义中,要求定义为名称为force,而不能是isForce。如此一来,BeanPropertyRowMapper就更加难以匹配数据列与Java类的字段了。

此外对于BeanPropertyRowMapper的读取列值,笔者觉得也有问题:

@Nullable
protected Object getColumnValue(ResultSet rs, int index, PropertyDescriptor pd) throws SQLException {
	return JdbcUtils.getResultSetValue(rs, index, pd.getPropertyType());
}

以下是被调用的、JdbcUtils类的getResultSetValue方法:

@Nullable
public static Object getResultSetValue(ResultSet rs, int index, @Nullable Class<?> requiredType) throws SQLException {
	if (requiredType == null) {
		return getResultSetValue(rs, index);
	}

	Object value;

	// Explicitly extract typed value, as far as possible.
	if (String.class == requiredType) {
		return rs.getString(index);
	}
	else if (boolean.class == requiredType || Boolean.class == requiredType) {
		value = rs.getBoolean(index);
	}
	else if (byte.class == requiredType || Byte.class == requiredType) {
		value = rs.getByte(index);
	}
	else if (short.class == requiredType || Short.class == requiredType) {
		value = rs.getShort(index);
	}
	else if (int.class == requiredType || Integer.class == requiredType) {
		value = rs.getInt(index);
	}
	else if (long.class == requiredType || Long.class == requiredType) {
		value = rs.getLong(index);
	}
	else if (float.class == requiredType || Float.class == requiredType) {
		value = rs.getFloat(index);
	}
	else if (double.class == requiredType || Double.class == requiredType ||
			Number.class == requiredType) {
		value = rs.getDouble(index);
	}
	else if (BigDecimal.class == requiredType) {
		return rs.getBigDecimal(index);
	}
	else if (java.sql.Date.class == requiredType) {
		return rs.getDate(index);
	}
	else if (java.sql.Time.class == requiredType) {
		return rs.getTime(index);
	}
	else if (java.sql.Timestamp.class == requiredType || java.util.Date.class == requiredType) {
		return rs.getTimestamp(index);
	}
	else if (byte[].class == requiredType) {
		return rs.getBytes(index);
	}
	else if (Blob.class == requiredType) {
		return rs.getBlob(index);
	}
	else if (Clob.class == requiredType) {
		return rs.getClob(index);
	}
	else if (requiredType.isEnum()) {

  其思路是依据VO类的字段类型例如:String.class、boolean.class等来选择不同的读取结果集的方法。该方法也存在一个局限:需要对VO类的字段类型定义得比较“精确”,否则会导致取值的“精度”丢失。
  对于VO类,日期类型既可以定义yyyy-mm-dd格式的日期,也可以定义hh:mm:ss格式的时间,以及yyyy-mm-dd hh:mm:ss格式时间戳。
  所以笔者的建议是,在目标类字段类型为主的前提下,还需要考虑数据列的数据类型。例如:

字段类型列类型(MySQL)结果集读取方式
字符串(String)字符串(getString)
整型(int、Integer)整型(getInt)
日期(Date)DATETIME时间戳(getTimestamp)
DATE日期(getDate)
TIMESTAMP时间戳(getTimestamp)
TIME时间(getTime)
长整形(long、Long)长整形(getLong)
双精度浮点型(double、Double)双精度浮点型(getDouble)
逻辑(boolean、Boolean)INT整型(getInt)是否=1
CHAR、VARCHAR字符串(getString)是否=“1”
浮点型(float、Float)浮点型(getFloat)
大数值(BigDecimal)大数值(getBigDecimal)
日期(Time)时间(getTime)
(4)破局

通过之前的问题,我们可以得出以下结论:

  • 无论是从列名推测字段名,还是从字段名推测列名,都只是推测而已,都无法保证精准匹配,除非明确指出。例如:PO对象的定义,就通过注解@Column明确指出了字段所对应的数据表列名。
  • 对于复杂情形需要明确指出字段与列的对应关系外,需要考虑简化普通场景,例如:直接通过列名来匹配字段名,列名和字段名相同
  • 对于结果集列值的读取,不仅需要参考目标类字段类型,还需要考虑数据列的数据类型
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值