📢 大家好,我是 【战神刘玉栋】,有10多年的研发经验,致力于前后端技术栈的知识沉淀和传播。 💗
🌻 近期刚转战 CSDN,会严格把控文章质量,绝不滥竽充数,如需交流,欢迎留言评论。👍
文章目录
- 写在前面的话
- MyBatis 返回限制
- MyBatis 超时配置
- MyBatis 批量操作
- Mybatis 单字符判断
- Mybatis List 入参问题
- Mybatis 下划线转驼峰
- Mybatis in 集合超出1000
- Mybatis 搜不等于时不包含null
- MyBatis 其他注意事项
- 总结陈词
写在前面的话
对于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 报错,如下图。
解决方案:
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
的日常使用场景及应对方案,希望对大家有帮助。
💗 后续会逐步分享企业实际开发中的实战经验,有需要交流的可以联系博主。