Mybatis 常见坑点
Mybatis SelectOne 并不是任意选择一个
mybatis的selectOne并不是任意选择一个, 而是select all, 然后赋值给一个对象. 如果返回的不只一个, 程序会报错. 如果想要实现selectAnyOne, 需要自行填写 limit 1 的后缀. 看过不少业务代码都犯了这个错误, 没有出现报错只是运气好.
正确的写法应该是
ClusterStatusDao clusterStatusDao = ApplicationContextUtil.getBean(ClusterStatusDao.class);
LambdaQueryWrapper<ClusterStatus> qw = new LambdaQueryWrapper<>();
qw.eq(ClusterStatus::getClusterId, clusterId)
.eq(ClusterStatus::getService, ClusterStatus.Ranger)
.orderByDesc(ClusterStatus::getUpdatedAt)
.last("limit 1");
ClusterStatus status = clusterStatusDao.selectOne(qw);
Mybatis 默认不更新空字段
2023-06-25
看代码发现对mybatis的field字段有注解 updateStrategy = FieldStrategy.IGNORED, 从sql语法角度看根本不理解有什么作用.
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@TableName(value = "audit_info", autoResultMap = true)
public class AuditRuleInfo {
@TableId(value = "id")
private Long id;
/**
* 项目ID
*/
@TableField(value = "project_id", updateStrategy = FieldStrategy.IGNORED)
private String projectId;
}
详细查找之后才发现原来是一个 mybatis 容易掉进去的坑. 如果字段设置为 null, 设置更新并不会被mybatis执行. 这并不是数据库行为, 而是框架的协助. 可以理解为什么mybatis有这么个场景, 有时候其实只想更新某个字段, 按默认NOT_NULL的用法只需要设置几个字段就行了.
平时我的写法都是 class a = queryWrapper.select(); a.setA(ABC); dao.updatebyId(a), 如果设置某个字段为 null, mybatis忽略后并不会报错, 不测试还发现不了.
FieldStrategy 参考文档
这篇文档写得很清楚, 只是了解的话看了就知道怎么使用了. 测试步骤主要是设置不同的配置, 开启sql日志模式, 直接看执行的sql就清楚了.
Mybatis-Plus字段策略FieldStrategy详解
https://developer.aliyun.com/article/1055572
1、FieldStrategy作用
Mybatis-Plus字段策略FieldStrategy的作用主要是在进行新增、更新时,根据配置的策略判断是否对实体对象的值进行空值判断,如果策略为字段不能为空,则不会对为空的字段进行赋值或更新。同样,在进行where条件查询时,根据whereStrategy策略判断是否对字段进行空值判断,如果策略为字段不能为空,则为空的字段不会作为查询条件组装到where条件中。
1、字段策略的3个使用场景:
- insertStrategy insert操作时的字段策略,是否进行空值判断,插入空值
- updateStrategy update操作时的字段策略,是否进行空值判断,插入空值
- whereStrategy where条件组装时的字段策略,是否进行控制判断,将空值作为查询条件
2、字段策略的5种类型:
- IGNORED 忽略空值判断,实体对象的字段是什么值就用什么值更新,支持null值更新操作
- NOT_NULL 进行非NULL判断,相当于age!=null,也是默认的策略
- NOT_EMPTY 进行非空判断,主要是针对字符串类型的字段,相当于name != null and name != ''
- NEVER 从不更新,不管字段是否有值,都不进行更新
- DEFAULT 追随全局配置
官方文档: https://baomidou.com/pages/223848/#tablename
mybatis 源代码查找
mybatis源代码的关联细节还是太多, 没法直接看出整条完整的链路. 大概看出通过
update stragety会通过xml的test影响更新字段的选择.
- field stategy 类型
package com.baomidou.mybatisplus.annotation;
/**
* 字段策略枚举类
* <p>
* 如果字段是基本数据类型则最终效果等同于 {@link #IGNORED}
*
* @author hubin
* @since 2016-09-09
*/
public enum FieldStrategy {
/**
* 忽略判断
*/
IGNORED,
/**
* 非NULL判断
*/
NOT_NULL,
/**
* 非空判断(只对字符串类型字段,其他类型字段依然为非NULL判断)
*/
NOT_EMPTY,
/**
* 默认的,一般只用于注解里
* <p>1. 在全局里代表 NOT_NULL</p>
* <p>2. 在注解里代表 跟随全局</p>
*/
DEFAULT,
/**
* 不加入 SQL
*/
NEVER
}
- table filed属性里有 update stragtegy
@Getter
@ToString
@EqualsAndHashCode
@SuppressWarnings("serial")
public class TableFieldInfo implements Constants {
/**
* 属性
*
* @since 3.3.1
*/
private final Field field;
/**
* 字段名
*/
private final String column;
/**
* 属性名
*/
private final String property;
/**
* 属性表达式#{property}, 可以指定jdbcType, typeHandler等
*/
private final String el;
/**
* 字段 update set 部分注入
*/
private String update;
/**
* 是否是基本数据类型
*
* @since 3.4.0 @2020-6-19
*/
private final boolean isPrimitive;
/**
* 属性是否是 CharSequence 类型
*/
private final boolean isCharSequence;
/**
* 字段验证策略之 insert
* Refer to {@link TableField#insertStrategy()}
*
* @since added v_3.1.2 @2019-5-7
*/
private final FieldStrategy insertStrategy;
/**
* 字段验证策略之 update
* Refer to {@link TableField#updateStrategy()}
*
* @since added v_3.1.2 @2019-5-7
*/
private final FieldStrategy updateStrategy;
/**
* 字段验证策略之 where
* Refer to {@link TableField#whereStrategy()}
*
* @since added v_3.1.2 @2019-5-7
*/
private final FieldStrategy whereStrategy;
/**
* 是否是乐观锁字段
*/
private final boolean version;
...
}
- 如果不设置字段的annotation, 默认为default, 使用的就是全局配置, 也就是
NOT_NULL.
this.insertStrategy = this.chooseFieldStrategy(tableField.insertStrategy(), dbConfig.getInsertStrategy());
this.updateStrategy = this.chooseFieldStrategy(tableField.updateStrategy(), dbConfig.getUpdateStrategy());
this.whereStrategy = this.chooseFieldStrategy(tableField.whereStrategy(), dbConfig.getWhereStrategy());
/**
* 优先使用单个字段注解,否则使用全局配置
*/
private FieldStrategy chooseFieldStrategy(FieldStrategy fromAnnotation, FieldStrategy fromDbConfig) {
return fromAnnotation == FieldStrategy.DEFAULT ? fromDbConfig : fromAnnotation;
}
- 全局配置默认为
not null
因此如果不配置, 如果某个字段为null数值, 则不会被切换为sql进行更新.
public class GlobalConfig implements Serializable {
/**
* 数据库相关配置
*/
private DbConfig dbConfig;
...
@Data
public static class DbConfig {
/**
* 主键类型
*/
private IdType idType = IdType.ASSIGN_ID;
/**
* 表名前缀
*/
private String tablePrefix;
...
* 字段验证策略之 insert
*
* @since 3.1.2
*/
private FieldStrategy insertStrategy = FieldStrategy.NOT_NULL;
/**
* 字段验证策略之 update
*
* @since 3.1.2
*/
private FieldStrategy updateStrategy = FieldStrategy.NOT_NULL;
/**
* 字段验证策略之 select
*
* @since 3.1.2
* @deprecated 3.4.4
*/
@Deprecated
private FieldStrategy selectStrategy;
/**
* 字段验证策略之 where
* 替代selectStrategy,保持与{@link TableField#whereStrategy()}一致
*
* @since 3.4.4
*/
private FieldStrategy whereStrategy = FieldStrategy.NOT_NULL;
- 关键步骤, 在转换sql的时候, 把字段切换为xml的
test字段
/**
* 转换成 if 标签的脚本片段
*
* @param sqlScript sql 脚本片段
* @param property 字段名
* @param fieldStrategy 验证策略
* @return if 脚本片段
*/
private String convertIf(final String sqlScript, final String property, final FieldStrategy fieldStrategy) {
if (fieldStrategy == FieldStrategy.NEVER) {
return null;
}
if (isPrimitive || fieldStrategy == FieldStrategy.IGNORED) {
return sqlScript;
}
if (fieldStrategy == FieldStrategy.NOT_EMPTY && isCharSequence) {
return SqlScriptUtils.convertIf(sqlScript, String.format("%s != null and %s != ''", property, property),
false);
}
return SqlScriptUtils.convertIf(sqlScript, String.format("%s != null", property), false);
}
每个字段的annotation判断, 其实是通过改写mybatis的xml完成的. 在代码里进行前置处理, 然后再统一交给xml解析模块.
@SuppressWarnings("serial")
public abstract class SqlScriptUtils implements Constants {
/**
* <p>
* 获取 带 if 标签的脚本
* </p>
*
* @param sqlScript sql 脚本片段
* @return if 脚本
*/
public static String convertIf(final String sqlScript, final String ifTest, boolean newLine) {
String newSqlScript = sqlScript;
if (newLine) {
newSqlScript = NEWLINE + newSqlScript + NEWLINE;
}
return String.format("<if test=\"%s\">%s</if>", ifTest, newSqlScript);
}
- 使用到update stragety的sqlset
update table set xxx = xxx123
/**
* 获取 set sql 片段
*
* @param ignoreIf 忽略 IF 包裹
* @param prefix 前缀
* @return sql 脚本片段
*/
public String getSqlSet(final boolean ignoreIf, final String prefix) {
final String newPrefix = prefix == null ? EMPTY : prefix;
// 默认: column=
String sqlSet = column + EQUALS;
if (StringUtils.isNotBlank(update)) {
sqlSet += String.format(update, column);
} else {
sqlSet += SqlScriptUtils.safeParam(newPrefix + el);
}
sqlSet += COMMA;
if (ignoreIf) {
return sqlSet;
}
if (withUpdateFill) {
// 不进行 if 包裹
return sqlSet;
}
return convertIf(sqlSet, convertIfProperty(newPrefix, property), updateStrategy);
}
/**
* 获取所有的 sql set 片段
*
* @param ignoreLogicDelFiled 是否过滤掉逻辑删除字段
* @param prefix 前缀
* @return sql 脚本片段
*/
public String getAllSqlSet(boolean ignoreLogicDelFiled, final String prefix) {
final String newPrefix = prefix == null ? EMPTY : prefix;
return fieldList.stream()
.filter(i -> {
if (ignoreLogicDelFiled) {
return !(isWithLogicDelete() && i.isLogicDelete());
}
return true;
}).map(i -> i.getSqlSet(newPrefix)).filter(Objects::nonNull).collect(joining(NEWLINE));
}
- 执行更新sql set的位置
table.getAllSqlSet
package com.baomidou.mybatisplus.core.injector;
/**
* SQL 更新 set 语句
*
* @param logic 是否逻辑删除注入器
* @param ew 是否存在 UpdateWrapper 条件
* @param table 表信息
* @param alias 别名
* @param prefix 前缀
* @return sql
*/
protected String sqlSet(boolean logic, boolean ew, TableInfo table, boolean judgeAliasNull, final String alias,
final String prefix) {
String sqlScript = table.getAllSqlSet(logic, prefix);
if (judgeAliasNull) {
sqlScript = SqlScriptUtils.convertIf(sqlScript, String.format("%s != null", alias), true);
}
if (ew) {
sqlScript += NEWLINE;
sqlScript += convertIfEwParam(U_WRAPPER_SQL_SET, false);
}
sqlScript = SqlScriptUtils.convertSet(sqlScript);
return sqlScript;
}