0%

mybatis-plus特性

mybatis-plus特性

1.字段类型转换器

如果出现实体字段类型与数据库类型不一致情况,比如实体是String但是数据库是Date,或者实体中是对象,而数据库中是varchar存储json;则可以通过字段类型转换器进行统一转换,无需业务层在每次存取时进行手段转换。

1.实体上设置转换器类型(设置之后,在通过基础接口的处理都会进行转换)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 注意这里需要设置autoResultMap = true, 否则查询时不会将数据库中的json转换为对应实体
@TableName(autoResultMap = true)
public class User {

private String id;

private String name;

private String mobile;

private String password;

@TableLogic
private Integer deleteStatus;
// 设置转换器的类型,这个是内置转换器,也可以自定义
@TableField(typeHandler = JacksonTypeHandler.class)
private UserExtraInfo userExtraInfo;
}

2.第一步骤中的设置仅导致通过内置接口的处理会进行转换,但是通过自定义XML的脚本语句不会进行转换。
法一:在mapper语句上通过@Results设置对应的映射关系

1
2
3
4
5
6
7
8
9
@Mapper
public interface UserMapper extends CustomerMapper<User> {

@Results(value = {
@Result(column = "user_extra_info", property = "userExtraInfo", typeHandler = JacksonTypeHandler.class)
})
@Select("select * from user")
List<User> testXmlSql();
}

法二:需要在xml文件中配置resultMap结果集,并在结果集映射中设置对应的类型转换器。

1
2
3
4
5
@Mapper
public interface UserMapper extends CustomerMapper<User> {

List<User> testXmlSql();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<mapper namespace="cn.com.xiaocainiaoya.springbootsimple.mapper.UserMapper">

<resultMap type="cn.com.xiaocainiaoya.springbootsimple.entity.User" id="procurementTemplateMap">
<result property="id" column="ID"/>
<result property="name" column="NAME"/>
<result property="mobile" column="MOBILE"/>
<result property="password" column="PASSWORD"/>
<result property="deleteStatus" column="DELETE_STATUS"/>
<result property="userExtraInfo" column="USER_EXTRA_INFO" typeHandler = "com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler"/>
</resultMap>

<select id="testXmlSql" resultMap="procurementTemplateMap">
select * from user
</select>
</mapper>

2.基础类扩展

直接通过继承mybatis-plus提供的三层基础接口,可以直接具备一些数据操作能力,比如通过deleteById可以直接通过id删除记录,无需编写SQL语句。同时mybatis-plus还提供了对基础层的扩展,用户可自定义扩展点后,业务处理再继承于自定义的扩展点,使得系统应用具备自定义扩展底层接口的能力。
mybatis-plus基础接口扩展.png

上图中userFacadeuserServiceuserMapper是业务层;customerService<User>CustomerServiceImplCustomerMapper是自定义扩展层;IService<T>ServiceImplBaseMappermybatis-plus的三次基础接口。

假设现在要扩展Insert ignore的插入语句

1.继承AbstractMethod编写具体SQL的拼写等处理:可以参考原有的其他对AbstractMethod的实现,比如com.baomidou.mybatisplus.core.injector.methods

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class InsertIgnore extends AbstractMethod {

@Override
public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
KeyGenerator keyGenerator = new NoKeyGenerator();
CustomerSqlMethod sqlMethod = CustomerSqlMethod.INSERT_IGNORE_ONE;
String columnScript = SqlScriptUtils.convertTrim(tableInfo.getAllInsertSqlColumnMaybeIf(null),
LEFT_BRACKET, RIGHT_BRACKET, null, COMMA);
String valuesScript = SqlScriptUtils.convertTrim(tableInfo.getAllInsertSqlPropertyMaybeIf(null),
LEFT_BRACKET, RIGHT_BRACKET, null, COMMA);
String keyProperty = null;
String keyColumn = null;
// 表包含主键处理逻辑,如果不包含主键当普通字段处理
if (StringUtils.isNotBlank(tableInfo.getKeyProperty())) {
if (tableInfo.getIdType() == IdType.AUTO) {
/* 自增主键 */
keyGenerator = new Jdbc3KeyGenerator();
keyProperty = tableInfo.getKeyProperty();
keyColumn = tableInfo.getKeyColumn();
} else {
if (null != tableInfo.getKeySequence()) {
keyGenerator = TableInfoHelper.genKeyGenerator(sqlMethod.getMethod(), tableInfo, builderAssistant);
keyProperty = tableInfo.getKeyProperty();
keyColumn = tableInfo.getKeyColumn();
}
}
}
String sql = String.format(sqlMethod.getSql(), tableInfo.getTableName(), columnScript, valuesScript);
SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, modelClass);
return this.addInsertMappedStatement(mapperClass, modelClass, sqlMethod.getMethod(), sqlSource, keyGenerator, keyProperty, keyColumn);
}
}

2.设置自定义SQL注入器并注入到Spring容器:将步骤一中创建的具体SQL执行器注入到mybatis-plus相应处理中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class CustomerSqlInjector extends DefaultSqlInjector {

@Override
public List<AbstractMethod> getMethodList(Class<?> mapperClass) {
// 注意:这个很重要,这个是原本已经写好的数据处理能力
List<AbstractMethod> methodList = super.getMethodList(mapperClass);
// 插入数据,如果中已经存在相同的记录,则忽略当前新数据
methodList.add(new InsertIgnore());
return methodList;
}
}
//MybatisConfig
@Bean
public ISqlInjector iSqlInjector() {
return new CustomerSqlInjector();
}

3.创建扩展的mapper层:添加需要扩展的方法。

1
2
3
4
5
6
7
8
9
10
public interface CustomerMapper<T> extends BaseMapper<T> {
/**
* 插入数据,如果中已经存在相同的记录,则忽略当前新数据
* {@link InsertIgnore}
*
* @param entity 实体类
* @return 影响条数
*/
int insertIgnore(T entity);
}

4.创建扩展service

1
2
3
4
5
6
7
8
9
10
public interface CustomerService<T> extends IService<T> {
/**
* 插入数据,如果中已经存在相同的记录,则忽略当前新数据
* {@link InsertIgnore}
*
* @param entity 实体类
* @return 影响条数
*/
int insertIgnore(T entity);
}

4.创建扩展serviceImpl

1
2
3
4
5
6
7
public class CustomerServiceImpl<M extends CustomerMapper<T>, T> extends ServiceImpl<M, T> implements CustomerService<T> {

@Override
public int insertIgnore(T entity) {
return baseMapper.insertIgnore(entity);
}
}

5.到这里,业务层只要继承于扩展层,就具备了自定扩展的处理能力

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@Mapper
public interface UserMapper extends CustomerMapper<User> {
}

public interface UserFacade extends CustomerService<User> {
void insertUser();
}

@Service
public class UserService extends CustomerServiceImpl<UserMapper, User> implements UserFacade {

@Override
public void insertUser() {

UserExtraInfo userExtraInfo = UserExtraInfo.builder()
.address("北京市")
.city("北京")
.build();

User user = User.builder()
.id("12312311")
.name("name")
.mobile("mobile")
.password("password")
.deleteStatus(0)
.userExtraInfo(userExtraInfo)
.build();
// 这里就可以直接调用到扩展的接口
insertIgnore(user);
}
}

3.逻辑删除

只需要设置@TableLogic就具备了逻辑删除的能力,但是这仅限制与通过mybatis-plus的基础接口处理数据,如果是通过xml手写SQL是无法自动补充逻辑删除条件。

  • 插入语句不处理,也就是说默认值需要业务方插入或者通过数据库设置默认值来处理
  • 查找、更新会追加查询条件
  • 删除转为更新
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class User {

private String id;

private String name;

private String mobile;

private String password;

// 设置逻辑删除字段
@TableLogic
private Integer deleteStatus;
}

4.对第2点中的注入器分析

首先在mybatis对持久层进行解析时,需要对xml等进行解析,mybatis-plusmybatisxml解析进行了扩展,解析时会进入到MybatisMapperAnnotationBuilder#parse方法。每一个XxxMapper都会进入到这个解析方法中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
@Override
public void parse() {
// 这个resource的值就是全限定路径,比如‘cn.com.xiaocainiaoya.user.UserMapper’
String resource = type.toString();
if (!configuration.isResourceLoaded(resource)) {
loadXmlResource();
configuration.addLoadedResource(resource);
String mapperName = type.getName();
assistant.setCurrentNamespace(mapperName);
parseCache();
parseCacheRef();
InterceptorIgnoreHelper.InterceptorIgnoreCache cache = InterceptorIgnoreHelper.initSqlParserInfoCache(type);
// 因为UserMapper需要继承于或间接继承于mybatis-plus实现的通用BaseMapper接口
// 所以这个methods得到的方法即UserMapper的所有方法(包含继承的上层接口的方法)
for (Method method : type.getMethods()) {
if (!canHaveStatement(method)) {
continue;
}
if (getAnnotationWrapper(method, false, Select.class, SelectProvider.class).isPresent()
&& method.getAnnotation(ResultMap.class) == null) {
parseResultMap(method);
}
try {
// 这个地方主要是做缓存一类,默认情况下是不开启的
InterceptorIgnoreHelper.initSqlParserInfoCache(cache, mapperName, method);
SqlParserHelper.initSqlParserInfoCache(mapperName, method);
parseStatement(method);
} catch (IncompleteElementException e) {
// TODO 使用 MybatisMethodResolver 而不是 MethodResolver
configuration.addIncompleteMethod(new MybatisMethodResolver(this, method));
}
}
// TODO 注入 CURD 动态 SQL , 放在在最后, because 可能会有人会用注解重写sql
try {
// https://github.com/baomidou/mybatis-plus/issues/3038
if (GlobalConfigUtils.isSupperMapperChildren(configuration, type)) {
// 主要处理在这里
parserInjector();
}
} catch (IncompleteElementException e) {
configuration.addIncompleteMethod(new InjectorResolver(this));
}
}
parsePendingMethods();
}

进入parserInjector()方法。

1
2
3
4
5
6
7
void parserInjector() {
// 这里在不扩展的情况下获取到的是DefaultSqlInjector,扩展之后就是扩展的实现类,比如在我的实现中是CustomerSqlInjector这个类
// 注意在这个中:一定要获取super.getMethodList(mapperClass); 这是原本的通用方法的注入器,比如有selectById(id)、deleteById(id)
GlobalConfigUtils.getSqlInjector(configuration)
// 进入注入器逻辑,这个方法的主要实现是在DefaultSqlInjector的父类AbstractSqlInjector中
.inspectInject(assistant, type);
}

进入AbstractSqlInjector#inspectInject方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Override
public void inspectInject(MapperBuilderAssistant builderAssistant, Class<?> mapperClass) {
Class<?> modelClass = extractModelClass(mapperClass);
if (modelClass != null) {
String className = mapperClass.toString();
Set<String> mapperRegistryCache = GlobalConfigUtils.getMapperRegistryCache(builderAssistant.getConfiguration());
if (!mapperRegistryCache.contains(className)) {
// 获取需要操作的方法列表,这个this.getMethodList()是模板方法,由子类实现,默认在DefaultSqlInjector中已经添加了17个通用方法,
// 所以要求当需要自定义扩展时继承于DefaultSqlInjector类,并在getMethodList()实现中,先获取父类的getMethodList(),得到这17
// 个通用方法之后,再添加自定义扩展的操作方法。
List<AbstractMethod> methodList = this.getMethodList(mapperClass);
if (CollectionUtils.isNotEmpty(methodList)) {
TableInfo tableInfo = TableInfoHelper.initTableInfo(builderAssistant, modelClass);
// 循环注入自定义方法
methodList.forEach(m -> m.inject(builderAssistant, mapperClass, modelClass, tableInfo));
} else {
logger.debug(mapperClass.toString() + ", No effective injection method was found.");
}
mapperRegistryCache.add(className);
}
}
}

自定义的CustomerSqlInjector

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public class CustomerSqlInjector extends DefaultSqlInjector {

public List<AbstractMethod> getMethodList(Class<?> mapperClass) {
// 注意:这个很重要,这个是原本已经写好的数据处理能力
List<AbstractMethod> methodList = super.getMethodList(mapperClass);
// 插入数据,如果中已经存在相同的记录,则忽略当前新数据
methodList.add(new InsertIgnore());
return methodList;
}
}

public class DefaultSqlInjector extends AbstractSqlInjector {

/**
* DefaultSqlInjector中的通用17个方法sql注入器
*/
@Override
public List<AbstractMethod> getMethodList(Class<?> mapperClass) {
return Stream.of(
new Insert(),
new Delete(),
new DeleteByMap(),
new DeleteById(),
new DeleteBatchByIds(),
new Update(),
new UpdateById(),
new SelectById(),
new SelectBatchByIds(),
new SelectByMap(),
new SelectOne(),
new SelectCount(),
new SelectMaps(),
new SelectMapsPage(),
new SelectObjs(),
new SelectList(),
new SelectPage()
).collect(toList());
}
}

下面进入到具体注入器中,这里以mybatis-plus内置的一个但未启用的一个注入器InsertBatchSomeColumn为例。它是在com.baomidou.mybatisplus.extension.injector.methods目录下,这里内置了好几个未启用的注入器。InsertBatchSomeColumn是对批量插入的SQL注入器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
@NoArgsConstructor
@AllArgsConstructor
@SuppressWarnings("serial")
public class InsertBatchSomeColumn extends AbstractMethod {

/**
* 字段筛选条件
*/
@Setter
@Accessors(chain = true)
private Predicate<TableFieldInfo> predicate;

/**
* 这个方法的主要逻辑就是拼接出
* <script> INSERT INTO tableName (field1,field2,field3....) VALUES
* <foreach collection="list" item="et" separator=",">
* (#{et.field1},#{et.field2},#{et.field3}....)
* </foreach>
* </script>
* 动态的批量插入脚本语句,这里需要注意的一点是,对于在mysql中设置字段默认值的情况,由于在这个sql中会对空值的字段赋值null
* 所以使用这种情况的批量插入会导致mysql默认值失效。如果在mysql中设置字段not null,则这种情况会报错。
*/
@SuppressWarnings("Duplicates")
@Override
public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
KeyGenerator keyGenerator = new NoKeyGenerator();
SqlMethod sqlMethod = SqlMethod.INSERT_ONE;
List<TableFieldInfo> fieldList = tableInfo.getFieldList();
String insertSqlColumn = tableInfo.getKeyInsertSqlColumn(false) +
this.filterTableFieldInfo(fieldList, predicate, TableFieldInfo::getInsertSqlColumn, EMPTY);
String columnScript = LEFT_BRACKET + insertSqlColumn.substring(0, insertSqlColumn.length() - 1) + RIGHT_BRACKET;
String insertSqlProperty = tableInfo.getKeyInsertSqlProperty(ENTITY_DOT, false) +
this.filterTableFieldInfo(fieldList, predicate, i -> i.getInsertSqlProperty(ENTITY_DOT), EMPTY);
insertSqlProperty = LEFT_BRACKET + insertSqlProperty.substring(0, insertSqlProperty.length() - 1) + RIGHT_BRACKET;
String valuesScript = SqlScriptUtils.convertForeach(insertSqlProperty, "list", null, ENTITY, COMMA);
String keyProperty = null;
String keyColumn = null;
// 表包含主键处理逻辑,如果不包含主键当普通字段处理
if (tableInfo.havePK()) {
if (tableInfo.getIdType() == IdType.AUTO) {
/* 自增主键 */
keyGenerator = new Jdbc3KeyGenerator();
keyProperty = tableInfo.getKeyProperty();
keyColumn = tableInfo.getKeyColumn();
} else {
if (null != tableInfo.getKeySequence()) {
keyGenerator = TableInfoHelper.genKeyGenerator(getMethod(sqlMethod), tableInfo, builderAssistant);
keyProperty = tableInfo.getKeyProperty();
keyColumn = tableInfo.getKeyColumn();
}
}
}
// 最后生成的模板SQL
String sql = String.format(sqlMethod.getSql(), tableInfo.getTableName(), columnScript, valuesScript);
SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, modelClass);
// 绑定到对应的mapperId上
return this.addInsertMappedStatement(mapperClass, modelClass, getMethod(sqlMethod), sqlSource, keyGenerator, keyProperty, keyColumn);
}

/**
* 在扩展的CustomBaseMapper中需要与这个method通用,才会与这个注入器进行绑定。
*/
@Override
public String getMethod(SqlMethod sqlMethod) {
// 自定义 mapper 方法名
return "insertBatchSomeColumn";
}
}

5.批量操作

​ 在mybatis-plus中提供了一些批量操作,比如有updateBatchByIdsaveBatchsaveOrUpdateBatch等,这种批量方式与第4点中注入器的方式底层处理上是有本质不同的mybatis-plus提供的这些批量操作,本质上是对mybatis的批量操作再次封装,而mybatis的批量操作是对JDBC批处理进行封装。

​ 这种批量操作实际上是在一个session会话中,批量的执行一堆语句。而第4点种的注入器的实现是通过将批量插入的语句拼接成insert into tableName(field1, field2..) values(...)(....),这里只有一条插入语句。

​ 如果从执行的效率出发,使用将批量插入的语句拼接为一条长语句

-------------本文结束感谢您的阅读-------------