主题
前面写过一篇文章,分享了公司是怎么动态封装SQL查询条件的(http://www.cnblogs.com/abcwt112/p/5874401.html).
里面提到数据库查询结果二维数组最后是封装到List<DTO>里的.即把数据库结果集封装成了对象集合.就像hibernate那样...那么公司是怎么做到的呢,这就是这篇文章的主题.
原理
类似于其他持久成框架,数据库结果要转化成对象,一定会有个映射规则.比如Mybatis里的Mapper文件用XML去描述映射规则,JPA则用注解@Column去描述规则.
公司也有自己的描述方法,参考了JPA,同样也是用注解的方式.有了规则,只要将数据库查询结果应用下规则即可.
规则
1 @Target({ ElementType.FIELD }) 2 @Retention(RetentionPolicy.RUNTIME) 3 @Inherited 4 @Documented 5 public @interface DbNameColumn { 6 7 8 String fieldName() default ""; 9 10 11 Class<? extends Converter> converter() default Converter.class; 12 }
1 @Target({ ElementType.FIELD }) 2 @Retention(RetentionPolicy.RUNTIME) 3 @Inherited 4 @Documented 5 public @interface DbColumn { 6 7 int index() default -1; 8 9 Class<? extends Converter> converter() default Converter.class; 10 11 }
有2种规则,前面那种是通过结果集的column name(DbNameColumn.fieldName其实叫columnName似乎更好)与对象字段名做关联,下面那种规则是通过结果集的column index(DbColumn.index)与对象字段名做映射.
很明显,通过index来映射是不好的方式,因为当多个service共用一套规则的时候,如果某一个service想修改SQL在数据库结果中额外插入一列的话是做不到的,因为会影响其他映射的index.而DbColumnName这套映射规则就可以.因为这个依赖于数据库列的别名,而不是顺序.
converter是org.apache.commons.beanutils.Converter接口的实现类,这里依赖apache的包,可以自己指定Converter.虽然实际使用中没人会指定Converter.因为用公司这种NativeSQL就是为了省力,不需为了每个查询都写个Entity实体去映射数据库,而是定义一个DTO去映射N种相同列(或者部分列)查询结果的不同SQL..如果这里要自己再写个Converter的话还不如写个Entity简单.
实现规则
有了规则就只要实现这个规则就可以了
1 package cn.com.servyou.framework.jpa; 2 3 import java.lang.reflect.Field; 4 import java.lang.reflect.InvocationTargetException; 5 import java.util.ArrayList; 6 import java.util.List; 7 import java.util.Map; 8 9 import org.apache.commons.beanutils.PropertyUtils; 10 import org.slf4j.Logger; 11 import org.slf4j.LoggerFactory; 12 13 import cn.com.servyou.framework.annotation.AnnotationUtils; 14 import cn.com.servyou.framework.beans.converter.ConverterUtilsWrapper; 15 import cn.com.servyou.framework.exception.SystemException; 16 17 18 public final class DbColumnMapper { 19 20 /** 21 * The Constant LOGGER. 22 */ 23 private static final Logger LOGGER = LoggerFactory.getLogger(DbColumnMapper.class); 24 25 /** 26 * Instantiates a new db column mapper. 27 */ 28 private DbColumnMapper() { 29 } 30 31 /** 32 * The list must contains the object array, which is the row. Each object in 33 * the array is the column element. 34 * 35 * @param <T> 36 * the generic type 37 * @param queryResult 38 * the query result 39 * @param targetClass 40 * the target class 41 * @return the list 42 */ 43 public static <T> List<T> resultMapping(List<?> queryResult, Class<? extends T> targetClass) { 44 45 return innerMapping(queryResult, targetClass); 46 } 47 48 /** 49 * Checks if is named mapping. 50 * 51 * @param targetClass 52 * the target class 53 * @return true, if is named mapping 54 */ 55 public static boolean isNamedMapping(Class<?> targetClass) { 56 57 List<Field> indexAnnotationFieldList = AnnotationUtils.getAnnotationFieldsInClass(DbColumn.class, targetClass); 58 List<Field> nameAnnotationFieldList = AnnotationUtils.getAnnotationFieldsInClass(DbNameColumn.class, 59 targetClass); 60 if (!indexAnnotationFieldList.isEmpty() && nameAnnotationFieldList.isEmpty()) { 61 return false; 62 } else if (!nameAnnotationFieldList.isEmpty() && indexAnnotationFieldList.isEmpty()) { 63 return true; 64 } else { 65 throw new SystemException("不允许使用混合的数据库mapping", SystemException.INCONSISTENCE_EXCEPTION); 66 } 67 } 68 69 /** 70 * Inner mapping. 71 * 72 * @param <D> 73 * the generic type 74 * @param queryResult 75 * the query result 76 * @param targetClass 77 * the target class 78 * @return the list 79 */ 80 @SuppressWarnings("unchecked") 81 private static <D> List<D> innerMapping(List<?> queryResult, Class<? extends D> targetClass) {// NOSONAR 82 83 List<D> resultList = new ArrayList<D>(); 84 85 if (!isNamedMapping(targetClass)) { 86 List<Field> annotationFieldList = AnnotationUtils.getAnnotationFieldsInClass(DbColumn.class, targetClass); 87 for (Object obj : queryResult) { 88 try {// NOSONAR 89 D targetDto = targetClass.newInstance(); 90 Object[] row = (Object[]) obj; 91 for (Field field : annotationFieldList) { 92 DbColumn dbColumn = field.getAnnotation(DbColumn.class); 93 if (dbColumn.converter() != null && !dbColumn.converter().isInterface()) { 94 ConverterUtilsWrapper.register(dbColumn.converter().newInstance(), field.getType()); 95 } 96 try {// NOSONAR 97 if (PropertyUtils.isWriteable(targetDto, field.getName())) { 98 if (dbColumn.index() >= 0 && dbColumn.index() < row.length) { 99 // BUG#1865 100 int columnIndex = field.getAnnotation(DbColumn.class).index(); 101 102 PropertyUtils.setProperty(targetDto, field.getName(), 103 ConverterUtilsWrapper.convert(row[columnIndex], field.getType())); 104 } else { 105 throw new SystemException("类" + targetClass.getName() + "的字段" + field.getName() 106 + "没有正确设置字段索引,应设置小于" + row.length + "的索引值", new Exception("类" 107 + targetClass.getName() + "的字段" + field.getName() + "没有正确设置字段索引,应设置小于" 108 + row.length + "的索引值"), SystemException.REQUEST_EXCEPTION); 109 } 110 } 111 112 } catch (InvocationTargetException e) { 113 LOGGER.error("[Error!]", e); 114 } catch (NoSuchMethodException e) { 115 LOGGER.error("[Error!]", e); 116 } 117 } 118 resultList.add(targetDto); 119 } catch (InstantiationException e1) { 120 LOGGER.error("[Error!]", e1); 121 } catch (IllegalAccessException e1) { 122 LOGGER.error("[Error!]", e1); 123 } 124 125 } 126 return resultList; 127 } else { 128 List<Field> annotationFieldList = AnnotationUtils.getAnnotationFieldsInClass(DbNameColumn.class, 129 targetClass); 130 for (Object obj : queryResult) { 131 try {// NOSONAR 132 D targetDto = targetClass.newInstance(); 133 Map<String, ?> row = (Map<String, ?>) obj; 134 for (Field field : annotationFieldList) { 135 DbNameColumn dbColumn = field.getAnnotation(DbNameColumn.class); 136 if (dbColumn.converter() != null && !dbColumn.converter().isInterface()) { 137 ConverterUtilsWrapper.register(dbColumn.converter().newInstance(), field.getType()); 138 } 139 try {// NOSONAR 140 if (PropertyUtils.isWriteable(targetDto, field.getName())) { 141 String colName = dbColumn.fieldName(); 142 PropertyUtils.setProperty(targetDto, field.getName(), 143 ConverterUtilsWrapper.convert(row.get(colName.toUpperCase()), field.getType())); 144 } 145 } catch (InvocationTargetException e) { 146 LOGGER.error("[Error!]", e); 147 } catch (NoSuchMethodException e) { 148 LOGGER.error("[Error!]", e); 149 } 150 } 151 resultList.add(targetDto); 152 } catch (InstantiationException e1) { 153 LOGGER.error("[Error!]", e1); 154 } catch (IllegalAccessException e1) { 155 LOGGER.error("[Error!]", e1); 156 } 157 } 158 return resultList; 159 } 160 } 161 162 }
以上是完整的映射规则的实现.类很长,但是核心代码不多..总共就2条分支,一条是index来映射,一条是column name来映射,分别对应前面的2种规则.
index那种映射真的不好.所以我就分享下DbNameColumn那套映射规则.核心代码如下:
1 List<Field> annotationFieldList = AnnotationUtils.getAnnotationFieldsInClass(DbNameColumn.class, 2 targetClass); 3 for (Object obj : queryResult) { 4 try {// NOSONAR 5 D targetDto = targetClass.newInstance(); 6 Map<String, ?> row = (Map<String, ?>) obj; 7 for (Field field : annotationFieldList) { 8 DbNameColumn dbColumn = field.getAnnotation(DbNameColumn.class); 9 if (dbColumn.converter() != null && !dbColumn.converter().isInterface()) { 10 ConverterUtilsWrapper.register(dbColumn.converter().newInstance(), field.getType()); 11 } 12 try {// NOSONAR 13 if (PropertyUtils.isWriteable(targetDto, field.getName())) { 14 String colName = dbColumn.fieldName(); 15 PropertyUtils.setProperty(targetDto, field.getName(), 16 ConverterUtilsWrapper.convert(row.get(colName.toUpperCase()), field.getType())); 17 } 18 } catch (InvocationTargetException e) { 19 LOGGER.error("[Error!]", e); 20 } catch (NoSuchMethodException e) { 21 LOGGER.error("[Error!]", e); 22 } 23 } 24 resultList.add(targetDto); 25 } catch (InstantiationException e1) { 26 LOGGER.error("[Error!]", e1); 27 } catch (IllegalAccessException e1) { 28 LOGGER.error("[Error!]", e1); 29 } 30 }
实现细节:
1.数据库查询结果是List<Map>这种形式,为什么不是List<Object[]>或者其他形似呢?这个是hibernate执行原生SQL的时候可以选择.用Map的话和DbNameColumn映射比较简单,List的话和DbColumn映射比较简单.
2.我们知道要映射的对象的class以后可以用反射生成空的对象,同样用反射得到class里标注了DbNameColumn注解的所有Field,得到这些Field上面的DbNameColumn注解里需要映射到的数据库Column name.
3.从Map里找那一行数据库记录的值,设置到对象里就行,这里用的是org.apache.commons.beanutils.PropertyUtils.
这样就可以将数据库结果映射到对象了.
思考小结
用1句话来表述实现的话就是:
通过注解描述字段与数据库结果集列名的映射关系,通过反射和PropertyUtils来实现映射.
公司的这套实现方案算是对Spring Data映射的一种补充吧..
1.当不想写很多entity(比如太多代码表或者参数表) 或者
2.为很多很多相同或相似结构结果的SQL(比如不同表查询出相同树形结果的结果)提供一个统一的映射方案
的话是一种不错的选择..
但是似乎也有一些地方是可以优化的,比如:
这里对象设值用的是apache的PropertyUtils.此外公司对象之间转化有自己定义的ConverterUtil(底层用cglib的BeanCopier实现),再或者公司框架都是基于Spring的,Spring也有自己的类型转化方法.
这里是不是可以统一呢? Spring框架源码我们不会去修改,那我们能不能把我们的实现合并到Spring的实现中呢?
比如前面转化的时候的2行代码:
ConverterUtilsWrapper.register(dbColumn.converter().newInstance(), field.getType());
ConverterUtilsWrapper.convert(row.get(colName.toUpperCase());
这是不是似曾相识?!
Spring本身已经自带了类型转化的方法,提供了很多基本类型的转化,那么我们可以使用他的...除此之外的业务相关特有的转化再添加到这个service里就可以了..
不过不管怎么说...这只是实现细节的不同.....现在的实现也是一种不错的实现...