《企业实战分享 · MyBatis 使用合集》

📢 大家好,我是 【战神刘玉栋】,有10多年的研发经验,致力于前后端技术栈的知识沉淀和传播。 💗
🌻 近期刚转战 CSDN,会严格把控文章质量,绝不滥竽充数,如需交流,欢迎留言评论。👍

写在前面的话

对于Java程序猿而言,MyBatis应该是企业开发中再熟悉不过的技术了,通常搭配Spring使用。
笔者所在公司也采用了MyBatis、MyBatisPlus作为持久层框架,这边汇总分享若干MyBatis日常使用场景及应对方案,希望与君共勉。

Tips:这里不介绍MyBatis的基础用法,默认大家都熟悉了。


MyBatis 返回限制

场景描述:
企业开发中,经常出现由于 MyBatis 查询出来的数据量过多,导致内存溢出。
这种问题通常出现在大表查询中,并且 由于MyBatis 使用了大量动态标签,当参数都没有传递的时候,就执行了近乎全表查询。

解决方案:
为了防止出现接口入参不规范导致的全表查询问题,框架层面可以进行若干拦截,优雅处理这一问题。
自定义MyBatis插件,按具体配置完成返回条数限制功能。
拦截器的部分代码如下:

public MybatisQueryLimitInterceptor(MybatisPluginProperties mybatisProp) {
    MybatisPluginProperties.QueryLimit queryLimitProp = mybatisProp.getQueryLimit();
    Assert.notNull(queryLimitProp, "Mybatis查询拦截器配置信息不能为空");
    this.queryLimitProp = queryLimitProp;
    int queryLimit = queryLimitProp.getMaxResultRows();
    this.rowBounds = queryLimit > 0 ? new RowBounds(0, queryLimit + 1) : null;
}

@Override
public Object intercept(Invocation invocation) throws Throwable {
    Object[] args = invocation.getArgs();
    Class<?> returnType = invocation.getMethod().getReturnType();
    String methodId = ((MappedStatement) args[0]).getId();
    if (this.rowBounds == null || !Collection.class.isAssignableFrom(returnType) || !this.isPermit(methodId)) {
        return invocation.proceed();
    }
    args[2] = this.rowBounds;
    Object result = invocation.proceed();
    if (result instanceof Collection
            && ((Collection<?>) result).size() == this.rowBounds.getLimit()) {
        if (this.queryLimitProp.isThrowIfOverLimit()) {
            throw new MybatisQueryLimitException(this.queryLimitProp.getMaxResultRows());
        } else if (result instanceof List) {
            log.error("[Mybatis全表查询拦截] - 方法: {}, 返回结果超过阈值: {}, 已自动丢弃超出范围的数据.", methodId, this.rowBounds.getLimit() - 1);
            ((List<?>) result).remove(this.rowBounds.getLimit() - 1);
        }
    }
    return result;
}

配套的相关配置如下,仅供参考:

mybatis:
  plugins:
    enable-query-limit: true
    # SQL查询返回行数限制
    query-limit:
      # SQL查询最大返回行数 (默认2000,-1表示不限制)
      max-result-rows: 2000
      # 超过最大返回行数时候,true:抛出异常,false:丢弃超过最大行数后面的数据
      throw-if-over-limit: true
      # 白名单 (dao方法全路径名)
      exclude-methods:
        - xxx.TrainProductDao.findAll

MyBatis 超时配置

场景描述:
项目通常会在配置文件中对 MyBatis 的 sql 执行时间进行限制,默认为30秒,也可根据实际情况进行调整。
在 sql 执行时间超过配置的时间后,会抛出 “ORA-01013: 用户请求取消当前的操作”的异常。

解决方案:
这里框架层面没有额外封装,直接使用MyBatis、MyBatisPlus自带的超时限制。
如下所示,配置文件中的超时配置是全局配置,如果对于某个语法有特殊需求时,也可以在XML中用timeout属性对改语句进行特定的配置。

Tips:还有其他超时配置,例如事务超时时间等,这里不展开赘述。

mybatis-plus:
  configuration:
  	# SQL请求超时时间
    default-statement-timeout: 30
<update id="test" timeout="10">
  update xx set xx
</update>
<select id="test" timeout="10">
  select xx from xx
</select>

MyBatis 批量操作

场景描述:
当执行大量插入或更新动作时,传统是采用MyBatis的foreach动态标签,这种方式性能相当差。

解决方案:
框架层面针对这类型操作进行封装。当批量操作大量数据的时候,应使用框架提供的方法。
例如:insertBatchByJdbc、updateBatchByJdbc
框架层面自定义了相关插件类,自动根据方法名是否包含BatchByJdbc后缀来判断是否走jdbc方法还是mybatis的方法,实际用的是 NamedParameterJdbcTemplate 去执行该语句。
该操作比MyBatis正常的foreach批量操作会快非常多,但该SQL入参可能较大,因为对应的SQL日志线上环境不建议打印,这个后续另行讨论。

Tips:相关代码若有需要可以留言提供。


Mybatis 单字符判断

**背景:**程序猿写后端 xml 代码时候,if 语句的参数变量传入状态 ‘1’ 或 ‘a’,发现 if 明明满足但却不触发。

**分析:**Mybatis 是用 OGNL表达式来解析的,在OGNL的表达式中, ‘1’ 会被解析成字符,Java是强类型的,char 和 一个string 会导致不等,所以if标签中的sql不会被解析。

**解决:**单个的字符要写到双引号里面或者使用 .toString() 才行,如下:

<if test="takeWay == '1' and workday != null ">
改为 <if test='takeWay == "1" and workday != null '>
或改为 <if test="takeWay == '1'.toString() and workday != null ">即可。

扩展:
经常遇到这种错误,java.lang.NumberFormatException: For input string: “F”
这个也是单字符问题引起的一种,参考:链接

Tips:最新追加,解决方式使用单引号放外侧也可以,外单内双,,只有当比对的值是字符串才会有问题。

补充:
当你的控制台或者日志出现 java.lang.NumberFormatException 时,很可能就是字符串转换成数字类型出现的问题。例如,在调用Long.ValueOf(String)或者Long.parseLong(String)方法进行数据类型转换时,字符串内不能包含除数字之外的字符。

扩展:
整数类型的判断方式如下:

<if test="nDay!=0">
       and   enddate > sysdate + #{nDay}
</if>
<if test="nDay==0">
   and to_char(enddate, 'yyyymm')=to_char(sysdate, 'yyyymm')
</if>

字符类型包含的判断方式如下:

<if test="params.type != null and params.type.contains('ward')">
  AND a.dept_attribute = '4'
  AND a.parent_dept_code is null
</if>

Mybatis List 入参问题

场景描述:
批量操作、或 in 语法在功能开发中是比较常见的,通常传入 Array 或 List ,然后根据多个 id 获取多条符合要求的记录,但只要入参长度是0,很容易出现 SQL 报错,如下图。
image.png

解决方案:
1)Service 层面直接判断 ids 的是否为空,是的话,直接抛出异常,或其他处理,这是通常做法。
2)Xml-SQL 层面进行判断,如果入参为空,则不查这个条件,当然要衡量这个是否符合逻辑需要。
建议:后端接口容错要主动做好,不能依赖前端去保障列表必定不为空。
注意:这个问题往往会在发布现场后暴露出来,开发人员大多没有意识用单元测试覆盖去所有场景。


Mybatis 下划线转驼峰

问题描述:
SSM 项目中在 M 的配置文件中添加以下配置,可以将数据库中 user_name 转化成 userName 与实体类属性对应,如下:<settingname="mapUnderscoreToCamelCase"value=“true”/>
在 SpringBoot 项目中没有配置文件,也可以在 application.properties 中加入配置项:
mybatis.configuration.mapUnderscoreToCamelCase=true
注意:该操作对返回类型对Map的时候是无效的,需要的话,要额外处理。因此,如果返回的是Map类型,建议要明确指定别名。

扩展说明:
Mybatis 的 map-underscore-to-camel-case 参数设置为true时,可以将数据库的带下划线给去掉然后映射到实体类的属性上去,映射属性时的逻辑大致是:
1、先将下划线去掉,参考:MetaClass#findProperty

public String findProperty(String name, boolean useCamelCaseMapping) {
    if (useCamelCaseMapping) {
        name = name.replace("_", "");
    }
    return this.findProperty(name);
}

2、将字段转成大写,然后查找对象中匹配的属性,参考:Reflector#findPropertyName

public String findPropertyName(String name) {
  return caseInsensitivePropertyMap.get(name.toUpperCase(Locale.ENGLISH));
}

从以上分析可以看见,其实 M 的驼峰法映射并不是严格限制的驼峰法语法,具体来说,对应“aa_bb”字段,其既可以匹配上“aaBb”属性,也可以匹配上“Aabb”属性,这一点在日常写代码时需要注意下。
这也可以看出,如果是返回Map格式的时候,是无法自动完成映射的。


Mybatis in 集合超出1000

问题描述:
当oracle sql中的in()条件集合超出了1000之后,会出现异常,考虑到某些场景会碰到这种问题,mybatis可以使用这种方式,当要超出1000条时,对in进行结束,重新再加一个in条件。

解决方案:

<select id="batchSelectByIn" resultType="com.zoe.optimus.dia.modules.report.entity.ComStaffBasicInfo">
	select * from zoeods_his.COM_STAFF_BASIC_INFO
    where staff_no in
    <foreach collection="staffNoList" index="index" item="staffNo" open="(" separator="," close=")">
    	<if test="(index % 999) == 998"> #{staffNo} ) OR staff_no IN (</if>#{staffNo}
    </foreach>
</select>

最终生成的SQL如下:

//温馨提示:如果还需要加其他条件,这部分需要用括号包裹,不然影响OR的范围。 -- by.小庄
SELECT *
  FROM ZOEODS_HIS.COM_STAFF_BASIC_INFO
 WHERE STAFF_NO IN ('30080')
    OR STAFF_NO IN ('110308');

Tips:也可以从Java代码考虑分流逻辑,根据实际场景判定。


Mybatis 搜不等于时不包含null

需求:现在oracle数据库中有字段is_use 的值有:null,0,1,2。现在需要查询不等于2的数据解决办法的sql:

select * from uc_Users where nvl(is_use,'xx')<>'2'

nvl(is_use,‘xx’)的意思是:如果is_use为null,值为xx。
如果用select * from uc_Users where is_use<>‘2’ 只会查询出0,1的数据,null的数据查询不出来。

类似的问题记录:null <> ‘0’ = false
背景:现场角色分发表,增加了一个查看报表权限的字段,为1是可查看,为0不可查看,为兼容旧的数据(该字段值为null的),程序判定为1或者为null都是可查看,语法直接写为 xxxx != ‘0’,结果发现无效。
解析:查阅发现,Oracle的 null <> ‘0’ = false,因此改为如下语句:select * from zoecomm.com_role_privs t where nvl(t.report_auth,‘@’) <> ‘0’

类似的问题记录:not in (‘0’)
背景:门户更新用户密码时获取当前用户的所有账号,需要过滤附属账户,一开始使用条件<>'0’过滤,后发现用此语法过滤时会将值为null的记录过滤,后改成not in (‘0’),也有同样的问题
解析:查阅发现,<>, in, not in 做过滤时都会将值为null的记录过滤,因此有如下解决方案:
1、select * from sample where (a not in (‘A’) or a is null);在not in 后加上" or 条件 is null"
2、select * from sample where nvl(a,‘default’) <> ‘A’;使用nvl对null赋默认值,防止 null<>'A’情况的出现


MyBatis 其他注意事项

1、Data日期类型数据与字符串比较带来的问题
mybatis里面配置可以解决这个时间类型与字符串类型作比较。但是为了规范化,建议还是时间类型不要与字符串类型做比较,如果没有配置的话,很容易直接mybatis类型不匹配报错
这样容易报错,
建议改为

2、传入List参数问题
MyBatis 涉及批量操作、in 等场景,经常使用到 foreach,这时候要特别注意,先从Java代码层面就判空操作,如果数组或列表为空,就不要调 Dao 方法了,否则报错。
这个问题往往会在发布现场后暴露出来,开发人员大多没有意识用单元测试覆盖去所有场景。

3、数值类型且为0
Mybatis 判断是否为空一般为:
state = #{state}
但是若state是int类型,并且如果传入的值为0,就不运行该条。
因为Mybatis 默认0和""相等。
解决方案是:
上述判断只适用于String类型的判断,若state是Integer类型的,显然不应该用这种判断条件,要解决这个问题,可以把代码改为:

<if test="state!=null and state!='' or state==0">
<if test="state!=null">state = #{state}</if>

4、Mybatis SQL 日志打印
日常开发通常需要观察本地控制台的SQL执行日志输出情况。
新框架集成了MP进行数据操作,按如下配置即可打印SQL。

logging:
  level:
    # 输出Mybatis相关日志
    xxx.logging.trace.sql.mybatis: debug
    xxx.business.mybatis.interceptor: debug

总结陈词

上文分享若干企业实际开发中,MyBatis的日常使用场景及应对方案,希望对大家有帮助。
💗 后续会逐步分享企业实际开发中的实战经验,有需要交流的可以联系博主。

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

战神刘玉栋

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

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

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

打赏作者

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

抵扣说明:

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

余额充值