在应用程序中针对数据库操作最常见的就是数据查询,而数据查询不可避免的要进行ORM操作。也就是数据库某某字段映射到简单对象某某字段。比如MyBatis当中通过以下的配置方式进行对象映射
<!-- 非常复杂的结果映射 -->
<resultMap id="detailedBlogResultMap" type="Blog">
<constructor>
<idArg column="blog_id" javaType="int"/>
</constructor>
<result property="title" column="blog_title"/>
<association property="author" javaType="Author">
<id property="id" column="author_id"/>
<result property="username" column="author_username"/>
<result property="password" column="author_password"/>
<result property="email" column="author_email"/>
<result property="bio" column="author_bio"/>
<result property="favouriteSection" column="author_favourite_section"/>
</association>
<collection property="posts" ofType="Post">
<id property="id" column="post_id"/>
<result property="subject" column="post_subject"/>
<association property="author" javaType="Author"/>
<collection property="comments" ofType="Comment">
<id property="id" column="comment_id"/>
</collection>
<collection property="tags" ofType="Tag" >
<id property="id" column="tag_id"/>
</collection>
<discriminator javaType="int" column="draft">
<case value="1" resultType="DraftPost"/>
</discriminator>
</collection>
</resultMap>
这种方式将数据库字段和对象字段都进行明确。这样当JDBC查询结果ResultSet返回时,可以通过 getXXX(String columnLabel)
操作获取到目标值,然后反射设置到对象对应的字段(property)上面。但是如果返回的目标对象是一个Map对象呢?比如hashmap
<select id="selectPerson" parameterType="int" resultType="hashmap">
SELECT name,sex,age FROM PERSON WHERE ID = #{id}
</select>
此时映射关系是怎样的呢?此时会返回一个 HashMap 类型的对象,其中的键是列名,值便是结果行中的对应值。这里映射为一个HashMap并没有问题。只不过在后面从这个HashMap中取值的时候就麻烦了。比如以上案例的结果中,是使用name去取值还时NAME去取值的呢?这里的大小写其实是由数据库决定的。比如Oracle数据库就是固定返回大小,而PG数据库则是强制返回小写。如果仅仅是使用一个数据库倒没有啥问题。如果是一个老项目突然改换数据库,就非常麻烦了。那么有没有很简单的办法解决呢?
通过查看MyBatis源码,这块逻辑在方法org.apache.ibatis.executor.resultset.DefaultResultSetHandler#createAutomaticMappings
当中的以下操作中
final String property = metaObject.findProperty(propertyName, configuration.isMapUnderscoreToCamelCase());
此处会将查询结果中还未完成数据库字段映射的列拿出来查找目标属性名称。
而其中org.apache.ibatis.reflection.MetaObject#findProperty实现如下
public String findProperty(String propName, boolean useCamelCaseMapping) {
return objectWrapper.findProperty(propName, useCamelCaseMapping);
}
获取属性名称又交给了objectWrapper去执行。而此处objectWrapper的初始化是在MetaObject构造时执行的,如下所示
private MetaObject(Object object, ObjectFactory objectFactory, ObjectWrapperFactory objectWrapperFactory, ReflectorFactory reflectorFactory) {
this.originalObject = object;
this.objectFactory = objectFactory;
this.objectWrapperFactory = objectWrapperFactory;
this.reflectorFactory = reflectorFactory;
if (object instanceof ObjectWrapper) {
this.objectWrapper = (ObjectWrapper) object;
} else if (objectWrapperFactory.hasWrapperFor(object)) {
this.objectWrapper = objectWrapperFactory.getWrapperFor(this, object);
} else if (object instanceof Map) {
this.objectWrapper = new MapWrapper(this, (Map) object);
} else if (object instanceof Collection) {
this.objectWrapper = new CollectionWrapper(this, (Collection) object);
} else {
this.objectWrapper = new BeanWrapper(this, object);
}
}
关键在下面的if else语句,其中的逻辑大致为
- 当前的目标对象为ObjectWrapper类型,直接作为objectWrapper属性
- 通过objectWrapperFactory来获取,前提是hasWrapperFor方法判断为true
- 如果目标对象为Map类型时,则使用MapWrapper(默认情况)
- 如果目标对象为集合类型时,则使用CollectionWrapper。
- 以上条件都不满足,则返回BeanWrapper。
如果是默认情况下,MyBatis要将目标映射为Map对象,则通过MapWrapper来findProperty的,而它的实现很简单
@Override
public String findProperty(String name, boolean useCamelCaseMapping) {
return name;
}
此处直接将数据库列名称作为Map的key值返回,在不同的数据库中,返回的值就可能有字母大小写的区别了。
如果返回的是普通bean呢?是否会受到大小写的影响呢?这里可以看下org.apache.ibatis.reflection.wrapper.BeanWrapper#findProperty
方法
@Override
public String findProperty(String name, boolean useCamelCaseMapping) {
return metaClass.findProperty(name, useCamelCaseMapping);
}
真实操作交给了metaClass。
public String findProperty(String name, boolean useCamelCaseMapping) {
if (useCamelCaseMapping) {
name = name.replace("_", "");
}
return findProperty(name);
}
这里首先根据useCamelCaseMapping是否进行下划线与驼峰的转换,但这里很奇怪,其实只是处理了下划线,并没有考虑字母大小写的问题。继续往下看
public String findProperty(String name) {
StringBuilder prop = buildProperty(name, new StringBuilder());
return prop.length() > 0 ? prop.toString() : null;
}
private StringBuilder buildProperty(String name, StringBuilder builder) {
PropertyTokenizer prop = new PropertyTokenizer(name);
if (prop.hasNext()) {
String propertyName = reflector.findPropertyName(prop.getName());
if (propertyName != null) {
builder.append(propertyName);
builder.append(".");
MetaClass metaProp = metaClassForProperty(propertyName);
metaProp.buildProperty(prop.getChildren(), builder);
}
} else {
String propertyName = reflector.findPropertyName(name);
if (propertyName != null) {
builder.append(propertyName);
}
}
return builder;
}
这里主要处理了对象嵌套映射的问题,我们关心的大小写问题在org.apache.ibatis.reflection.Reflector#findPropertyName方法中
public String findPropertyName(String name) {
return caseInsensitivePropertyMap.get(name.toUpperCase(Locale.ENGLISH));
}
从这里不难看出一些端倪了,这里无论传入的值是否大小写,都是直接转成大小写,然后去查找。所以前面数据库列名是否大小写都没有任何关系。这里的再看下caseInsensitivePropertyMap初始化的问题,在根据类对象构造Reflector对象的时候会建立对应的类的所有属性大写与属性的映射关系。这样每次查找一个类中是否有对应的属性的时候,就不用区分大小写了。
private Map<String, String> caseInsensitivePropertyMap = new HashMap<String, String>();
public Reflector(Class<?> clazz) {
type = clazz;
addDefaultConstructor(clazz);
addGetMethods(clazz);
addSetMethods(clazz);
addFields(clazz);
readablePropertyNames = getMethods.keySet().toArray(new String[getMethods.keySet().size()]);
writeablePropertyNames = setMethods.keySet().toArray(new String[setMethods.keySet().size()]);
for (String propName : readablePropertyNames) {
caseInsensitivePropertyMap.put(propName.toUpperCase(Locale.ENGLISH), propName);
}
for (String propName : writeablePropertyNames) {
caseInsensitivePropertyMap.put(propName.toUpperCase(Locale.ENGLISH), propName);
}
}
现在返回Map的情况,默认情况下映射为Map时使用的是MapWrapper。如果想更改这个逻辑,只有一个地方可以,如下图所示
只要能从对应的工厂中获取到包装器就可以了(hasWrapperFor返回true)。在默认情况下,MyBatis中ObjectWrapperFactory的实现类为DefaultObjectWrapperFactory。这个是在org.apache.ibatis.session.Configuration初始化的时候设置的。
protected ObjectWrapperFactory objectWrapperFactory = new DefaultObjectWrapperFactory();
然后每次创建MetaObject都是通过org.apache.ibatis.session.Configuration#newMetaObject方法来实现的
public MetaObject newMetaObject(Object object) {
return MetaObject.forObject(object, objectFactory, objectWrapperFactory, reflectorFactory);
}
此时会将objectWrapperFactory传入进去。这个objectWrapperFactory可以通过配置来修改,在org.apache.ibatis.builder.xml.XMLConfigBuilder#objectWrapperFactoryElement方法中
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
private void objectWrapperFactoryElement(XNode context) throws Exception {
if (context != null) {
String type = context.getStringAttribute("type");
ObjectWrapperFactory factory = (ObjectWrapperFactory) resolveClass(type).newInstance();
configuration.setObjectWrapperFactory(factory);
}
}
在mybatis-config.xml中配置如下
<objectWrapperFactory type="com.xQuant.common.jdbc.MapKeyUpperCaseWrapperFactory" />
对应的实现类如下
package com.xQuant.common.jdbc;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.wrapper.MapWrapper;
import org.apache.ibatis.reflection.wrapper.ObjectWrapper;
import org.apache.ibatis.reflection.wrapper.ObjectWrapperFactory;
import java.util.Map;
public class MapKeyUpperCaseWrapperFactory implements ObjectWrapperFactory {
@Override
public boolean hasWrapperFor(Object object) {
if (object instanceof Map) {
return true;
}
return false;
}
@Override
public ObjectWrapper getWrapperFor(MetaObject metaObject, Object object) {
if (object instanceof Map) {
return new CustomMapWrapper(metaObject, (Map) object);
}
return null;
}
public static class CustomMapWrapper extends MapWrapper {
public CustomMapWrapper(MetaObject metaObject, Map<String, Object> map) {
super(metaObject,map);
}
@Override
public String findProperty(String name, boolean useCamelCaseMapping) {
if (name != null && name.length() > 0) {
return name.toUpperCase();
}
return super.findProperty(name,useCamelCaseMapping);
}
}
}
当MyBaits要映射为Map对象的时候,会使用CustomMapWrapper来进行映射。而CustomMapWrapper继承了默认的MapWrapper,只有当findProperty的时候会将列名都转为大写即可。当然这里还可以继续扩展下。考虑下useCamelCaseMapping
public static class CustomMapWrapper extends MapWrapper {
public CustomMapWrapper(MetaObject metaObject, Map<String, Object> map) {
super(metaObject,map);
}
@Override
public String findProperty(String name, boolean useCamelCaseMapping) {
if (StringUtils.isNotBlank(name)){
if (useCamelCaseMapping) {
name = name.replace("_", "");
}
return name.toUpperCase();
}
return super.findProperty(name,useCamelCaseMapping);
}
}
如果是在SpringBoot中也可以通过在配置文件中通过配置属性指定。
mybatis.configuration.object-wrapper-factory