优化MyBatisPlus的autoResultMap生成策略

2023-11-19

前言

使用MyBatis-Plus的字段类型处理器,只需一个注解,就可以很方便的将数组、对象等数据直接映射到实体类中。

@Data
@Accessors(chain = true)
@TableName(autoResultMap = true)
public class User {
    private Long id;

    ...


    /**
     * 注意!! 必须开启映射注解
     *
     * @TableName(autoResultMap = true)
     *
     * 以下两种类型处理器,二选一 也可以同时存在
     *
     * 注意!!选择对应的 JSON 处理器也必须存在对应 JSON 解析依赖包
     */
    @TableField(typeHandler = JacksonTypeHandler.class)
    // @TableField(typeHandler = FastjsonTypeHandler.class)
    private OtherInfo otherInfo;

}

该注解对应了 XML 中写法为

<result column="other_info" jdbcType="VARCHAR" property="otherInfo" typeHandler="com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler" />

实现的原理可以参考TableInfo的源码initResultMapIfNeed方法:

    /**
     * 自动构建 resultMap 并注入(如果条件符合的话)
     */
    void initResultMapIfNeed() {
        if (autoInitResultMap && null == resultMap) {
            String id = currentNamespace + DOT + MYBATIS_PLUS + UNDERSCORE + entityType.getSimpleName();
            List<ResultMapping> resultMappings = new ArrayList<>();
            if (havePK()) {
                ResultMapping idMapping = new ResultMapping.Builder(configuration, keyProperty, keyColumn, keyType)
                    .flags(Collections.singletonList(ResultFlag.ID)).build();
                resultMappings.add(idMapping);
            }
            if (CollectionUtils.isNotEmpty(fieldList)) {
                fieldList.forEach(i -> resultMappings.add(i.getResultMapping(configuration)));
            }
            ResultMap resultMap = new ResultMap.Builder(configuration, id, entityType, resultMappings).build();
            configuration.addResultMap(resultMap);
            this.resultMap = id;
        }
    }

存在的问题

当使用autoResultMap=true时, MP会做以下事情:

  • 如果字段的Java类型不是基本类型,则会强制提示你设置typeHandler属性,比如这么写是不行的:
/**
 * IN 查询
*/
public static final String IN = "%s IN <foreach item=\"item\" collection=\"%s\" separator=\",\" open=\"(\" close=\")\" index=\"\">#{item}</foreach>";

@TableField(value="code",select=false, condition=IN )
private Strinng[] codes;
  • 如果字段比较复杂,比如包含函数,带有表别名限定等,那么自动生成的resultMap对应的column属性则会变得很奇怪:

@TableField(value = "array_agg(distinct code)",jdbcType = JdbcType.VARCHAR, typeHandler = ArrayTypeHandler.class)
private String[] codes;

上面的代码中,生成的resultMap的column属性是 rray_agg(distinct code , (value属性前后各截取一位),具体的逻辑可以查看MP的源码:

TableFieldInfo.java

    /**
  * 获取 ResultMapping
  *
  * @param configuration MybatisConfiguration
  * @return ResultMapping
  */
 ResultMapping getResultMapping(final Configuration configuration) {
     ResultMapping.Builder builder = new ResultMapping.Builder(configuration, property,
         StringUtils.getTargetColumn(column), propertyType);
     TypeHandlerRegistry registry = configuration.getTypeHandlerRegistry();
     if (jdbcType != null && jdbcType != JdbcType.UNDEFINED) {
         builder.jdbcType(jdbcType);
     }
     if (typeHandler != null && typeHandler != UnknownTypeHandler.class) {
         TypeHandler<?> typeHandler = registry.getMappingTypeHandler(this.typeHandler);
         if (typeHandler == null) {
             typeHandler = registry.getInstance(propertyType, this.typeHandler);
             // todo 这会有影响 registry.register(typeHandler);
         }
         builder.typeHandler(typeHandler);
     }
     return builder.build();
 }

StringUtils.java


   /**
    * 验证字符串是否是数据库字段
    */
   private static final Pattern P_IS_COLUMN = Pattern.compile("^\\w\\S*[\\w\\d]*$");
  
    /**
    * 判断字符串是否符合数据库字段的命名
    *
    * @param str 字符串
    * @return 判断结果
    */
   public static boolean isNotColumnName(String str) {
       return !P_IS_COLUMN.matcher(str).matches();
   }

   /**
    * 获取真正的字段名
    *
    * @param column 字段名
    * @return 字段名
    */
   public static String getTargetColumn(String column) {
       if (isNotColumnName(column)) {
           return column.substring(1, column.length() - 1);
       }
       return column;
   }

是的,就是这么简单粗暴,这里吐槽一下国内开源软件的毛病,逻辑莫名其妙,而且git上对于开发者提出的疑问视而不见。

优化思路

其实如果一个字段存在typeHander属性,那就必须要建一个ResultMap来处理类型映射了,根本不需要再画蛇添足的指定autoResultMap=true, 不过也好在有这个属性,MP才会自动生成一个ResultMap,这样我们就可以在不指定这个属性的时候,生成自己的ResultMap了。
直接修改官方源代码不是我的风格,好在MP提供了Sql注入器,在往Mapper中注入方法之前,我们把ResultMap生成就可以了。

优化步骤

  1. 创建抽象注入方法的子类:BetterAutoResultMap.java
import com.baomidou.mybatisplus.core.injector.AbstractMethod;
import com.baomidou.mybatisplus.core.metadata.TableFieldInfo;
import com.baomidou.mybatisplus.core.metadata.TableInfo;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.caspe.base.support.mybatisplus.toolkit.ConstantsX;
import lombok.SneakyThrows;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ResultMap;
import org.apache.ibatis.mapping.ResultMapping;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.TypeHandler;
import org.apache.ibatis.type.TypeHandlerRegistry;
import org.apache.ibatis.type.UnknownTypeHandler;

import java.lang.reflect.Field;
import java.util.List;
import java.util.stream.Collectors;

/**
 * 优化的AutoResultMap生成策略
 * 只要字段中指定了TypeHandler即自动生成ResultMap, 而不需要指定autoResultMap=true
 * 且只对设置了TypeHandler的字段生成ResultMapping, 而不是所有的字段
 *
 * @author yongfeng_meng
 */
public class BetterAutoResultMap extends AbstractMethod {

    /**
     * 强制重设TableInfo的resultMap属性
     */
    static Field ResultMapOfTableInfo;

    /**
     * 强制重设TableInfo的autoInitResultMap属性
     */
    static Field AutoInitResultMapOfTableInfo;

    static {
        try {
            ResultMapOfTableInfo = TableInfo.class.getDeclaredField("resultMap");
            ResultMapOfTableInfo.setAccessible(true);
            AutoInitResultMapOfTableInfo = TableInfo.class.getDeclaredField("autoInitResultMap");
            AutoInitResultMapOfTableInfo.setAccessible(true);
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        }
    }

    /**
     * 注入自定义 MappedStatement
     * <p>
     * 当实体类没有指定autoResultMap和resultMap时, 即可使用该方法自动注入ResultMap
     *
     * @param mapperClass mapper 接口
     * @param modelClass  mapper 泛型
     * @param tableInfo   数据库表反射信息
     * @return MappedStatement
     */
    @SneakyThrows
    @Override
    public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
        if (!tableInfo.isAutoInitResultMap() && tableInfo.getResultMap() == null) {

            // 只要字段中指定了TypeHandler即自动生成ResultMap
            if (tableInfo.getFieldList().stream().filter(this::needToAutoMap).findAny().isPresent()) {

                // 生成ResultMap
                ResultMap resultMap = generatorResultMap(tableInfo);
                configuration.addResultMap(resultMap);

                // 将ResultMap属性设置到TableInfo
                ResultMapOfTableInfo.set(tableInfo, resultMap.getId());
                AutoInitResultMapOfTableInfo.set(tableInfo, true);
            }
        }
        return null;
    }

    /**
     * 构建 resultMap
     */
    ResultMap generatorResultMap(TableInfo tableInfo) {
        String resultMapId = tableInfo.getCurrentNamespace() + DOT + ConstantsX.MYBATIS_PLUS_X + UNDERSCORE + tableInfo.getEntityType().getSimpleName();
        List<ResultMapping> resultMappings = tableInfo.getFieldList().stream().filter(this::needToAutoMap)
                .map(this::getResultMapping).collect(Collectors.toList());
        return new ResultMap.Builder(configuration, resultMapId, tableInfo.getEntityType(), resultMappings).build();
    }

    boolean needToAutoMap(TableFieldInfo f) {
        return f.getTypeHandler() != null && f.getTypeHandler() != UnknownTypeHandler.class;
    }

    /**
     * 构建 resultMapping (只针对typeHandler的字段)
     *
     * @param tableFieldInfo
     * @return
     */
    ResultMapping getResultMapping(TableFieldInfo tableFieldInfo) {
        String column = tableFieldInfo.getColumn();
        String property = tableFieldInfo.getProperty();
        if (!StringUtils.underlineToCamel(column).equals(property)) {
            column = property;
        }
        ResultMapping.Builder builder = new ResultMapping.Builder(configuration, property, column, tableFieldInfo.getPropertyType());
        TypeHandlerRegistry registry = configuration.getTypeHandlerRegistry();
        JdbcType jdbcType = tableFieldInfo.getJdbcType();
        if (jdbcType != null && jdbcType != JdbcType.UNDEFINED) {
            builder.jdbcType(jdbcType);
        }
        TypeHandler<?> typeHandlerMapped = registry.getMappingTypeHandler(tableFieldInfo.getTypeHandler());
        if (typeHandlerMapped == null) {
            typeHandlerMapped = registry.getInstance(tableFieldInfo.getPropertyType(), tableFieldInfo.getTypeHandler());
        }
        builder.typeHandler(typeHandlerMapped);
        return builder.build();
    }
}
  1. 创建SQL注入器的自定义类:SqlInjectorX.java
import com.baomidou.mybatisplus.core.injector.AbstractMethod;
import com.baomidou.mybatisplus.core.injector.DefaultSqlInjector;
import com.caspe.base.support.mybatisplus.injector.methods.BetterAutoResultMap;
import com.caspe.base.support.mybatisplus.injector.methods.DeleteByMultiId;
import com.caspe.base.support.mybatisplus.injector.methods.SelectByMultiId;
import com.caspe.base.support.mybatisplus.injector.methods.UpdateByMultiId;

import java.util.ArrayList;
import java.util.List;

public class SqlInjectorX extends DefaultSqlInjector {

    @Override
    public List<AbstractMethod> getMethodList(Class<?> mapperClass) {
        List<AbstractMethod> methodList = new ArrayList<>();
        // 优先注入AutoResultMap生成方法
        methodList.add(new BetterAutoResultMap());
        methodList.addAll(super.getMethodList(mapperClass));
        return methodList;
    }
}
  1. 将自定义Sql注入器加入到容器中
@Configuration
@Import({SqlInjectorX.class})
public class MybatisPlusAutoConfiguration {
}

验证

首先按照优化思路,肯定先要将autoResultMap=true这个属性删掉。
问题1将不复存在,问题2使用了我们自定义的ResultMap,column属性和property属性将一致,一切变得简单而自然。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

优化MyBatisPlus的autoResultMap生成策略 的相关文章

随机推荐

  • 浅谈我所见识的数据治理项目

    开篇一张图 与正文不一定有关 图片来源于朋友圈 01 写在前面 熟悉笔者的朋友可能知道 笔者之前做的并非纯数据相关工作 产品或项目 笔者属于半路出家的数据人 之前也几乎没有直接接触过数据仓库 数据中台 数据平台等产品或项目 与数据库是一直打
  • 大数据Mapreduce编程——矩阵乘法

    编程要求 完成矩阵乘法的 Map 函数和 Reduce 函数 1 设计两个矩阵 3050 50100 在每个单元格中填入一个 0 99 的随机数 并写入 两个文件中 作为 Map 函数的输入 2 测试运行矩阵乘法的 MapReduce 框架
  • Emoji表情符号用于文本情感分析-Improving sentiment analysis accuracy with emoji embedding

    Abstract Due to the diversity and variability of Chinese syntax and semantics accurately identifying and distinguishing
  • 显著性检测论文阅读整理

    1 Visual Saliency Based on Multiscale Deep Features 原文链接 https arxiv org pdf 1503 08663 pdf 翻译 https blog csdn net weixi
  • QCustomPlot获取选点坐标

    QCustomPlot版本 Version 2 1 1 设置点选择模式 customPlot gt setInteractions QCP iSelectPlottables 2 绑定点击事件 connect customPlot QCus
  • response实现文件下载(java)

    import javax servlet ServletException import javax servlet ServletOutputStream import javax servlet http HttpServlet imp
  • 阿里云多台内网ECS公用一条EIP实现访问外网

    第一步 开启ECS的ip转发功能 有公网的ECS上操作 1 vi etc sysctl conf 找到 net ipv4 ip forward 1 这一条 确保后面的值为1就行 如果没有这一条 手动加进去 保存退出 然后使用 sysctl
  • Linux内核源码学习(1)

    一 内核简介 1 在安装好的Linux系统中 内核的源代码位于 usr src linux 2的10次方就是1K 1024 16位CPU的地址空间是64K X86结构的80386是32位CPU 段描述结构伪代码 typedef struct
  • java.lang.Long cannot be cast to java.lang.Integer解决办法

    Integer属于不可更改类型 而且Long和Integer没有任何继承关系 当然不能这样转换 例如 public Integer getUsersCount String hql select count from Users List
  • vscode实现文件单步调试保姆级教程

    第一步 第二步 第三步 第四步 第五步 第六步 第七步 第八步 第九步 第十步 点击终端 gt 配置任务 第十一步 第十二步 第十三步 第十四步 设置完毕 可以在源程序打断点按F5执行了
  • Spring中Quartz的详细配置

    关于cron表达式 来自网络 Cron 表达式包括以下 7 个字段 秒 分 小时 月内日期 月 周内日期 年 可选字段 特殊字符 Cron 触发器利用一系列特殊字符 如下所示 反斜线 字符表示增量值 例如 在秒字段中 5 15 代表从第 5
  • 常见状态码

    SWITCH PROTOCOL 101 Switching Protocols OK 200 OK CREATED 201 Created ACCEPTED 202 Accepted NO CONTENT 204 No Content PA
  • 雷军 1994 年写的代码,经典老古董~

    整合整理 程序员的那些事 id iProgrammer 雷军的代码像诗一样优雅 有些网友在评论中质疑 说雷军代码不会是 屎 一样优雅吧 说这话的网友 也许是开玩笑的 也许是真没看过雷军写过的代码 在 2011 年的时候 我们在微博转过雷军在
  • 《计算机网络》——第四章知识点

    1 终点不可达 当路由器或主机不能交付数据报时就向源点发送终点不可达报文 无法交付 源点抑制 当路由器或主机由于拥塞而丢弃数据报时 就向源点发送源点抑制报文 使源点知道应当把数据报的发送速率放慢 拥塞丢数据 3 时间超过 当路由器收到生存时
  • [UE5蓝图基础一]13.类似”人类一败涂地”掉落一定距离会回到空中 最终着落点还是设定地形上

    利用合体触发器Box Conllision 目标点 在放置actor里 实现 修改盒体范围为2W 当人物与盒子重叠就瞬移到空中
  • 苹果美区app内购方法及经验

    苹果美区app内购方法及经验 方法一 礼品卡 失败 淘宝or亚马逊购买苹果礼品卡 经测试账号充值成功 也可以购买付费app 但内购会经典限购 等了n周依然没有解除 如果养号成功大概就没什么问题 但苯人等不及了 方法二 美区转国区 实在太想氪
  • 编译安装mysql5.7.10

    1 gt cmake MySQL使用cmake跨平台工具预编译源码 用于设置mysql的编译参数 如 安装目录 数据存放目录 字符编码 排序规则等 安装最新版本即可 2 gt make3 75 mysql源代码是由C和C 语言编写 在Lin
  • 算法 {掃描線}

    算法 掃描線 掃描線 定義 題目 二維空間上 給定若干個 凸多邊形 求他們的並集的面積 思路 用若干個x 與Y軸平行的線 將目標區域T劃分成若干個子區域Ai 即S A1 A2 然後單獨求每個Ai 性質 與積分有點類似 都是求一個區域的面積
  • SQLite、MySQL和PostgreSQL 三种关系数据库哪个好?

    https www ssdax com 2188 html
  • 优化MyBatisPlus的autoResultMap生成策略

    前言 使用MyBatis Plus的字段类型处理器 只需一个注解 就可以很方便的将数组 对象等数据直接映射到实体类中 Data Accessors chain true TableName autoResultMap true public