MyBatis中数据库字段强制大小写统一问题

在应用程序中针对数据库操作最常见的就是数据查询,而数据查询不可避免的要进行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语句,其中的逻辑大致为

  1. 当前的目标对象为ObjectWrapper类型,直接作为objectWrapper属性
  2. 通过objectWrapperFactory来获取,前提是hasWrapperFor方法判断为true
  3. 如果目标对象为Map类型时,则使用MapWrapper(默认情况)
  4. 如果目标对象为集合类型时,则使用CollectionWrapper。
  5. 以上条件都不满足,则返回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
  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

lang20150928

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值