目录
mybatis plus简介
mybatis的缺点
- 未提供通用curd实现,不管多简单的crud都要自行写sql,影响开发效率
- 未集成分页之类的常用功能,但mybatis支持插件 扩展性不错,可借助第三方插件实现
mybatis plus 在 mybatis 的基础上进行了大量的扩展,旨在简化 mybatis 开发
- 优点:提供了通用mapper、service,简化了单表操作,集成了分页、 乐观锁等常用功能,可以提高 mybatis 开发效率。
- 缺点:将很多持久层的批量操作方法封装在通用service中,对service层侵入严重。
官网:https://baomidou.com
github:https://github.com/baomidou/mybatis-plus
springboot整合mybatis plus
依赖
<!-- 已经包含了mybatis的依赖 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.1</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<!-- 默认是mysql 8.x的驱动,可以用<version>指定mysql 5.x的驱动 -->
<scope>runtime</scope>
</dependency>
yaml
mybatis-plus:
#指定xml映射文件位置,多个时逗号分隔
mapper-locations: classpath*:mapper/**.xml
#实体类别名,有多个包时以英文逗号或分号隔开
type-aliases-package: com.chy.mall.po
configuration:
#(字段名)下划线自动转驼峰,mp默认为true,mybatis默认为false
map-underscore-to-camel-case: true
#日志打印sql语句,常用的实现类:org.apache.ibatis.logging.slf4j.Slf4jImpl、org.apache.ibatis.logging.stdout.StdOutImpl
log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl
#全局默认配置
global-config:
db-config:
#主键策略,默认为ASSIGN_ID通过雪花算法自动生成(支持数值、String)。@TableId未指定type时自动使用此处的全局配置。
#常用的值:AUTO数据库自增、INPUT开发自行设置主键字段值。数据库自增、雪花算法生成 这些自动分配主键的,insert后会自动将主键值回填到po中
id-type: AUTO
#表名前缀,持久层实体类上未使用 @TableName 显式指定表名时,会自动转换为snake写法,并拼接指定前缀,eg. UserInfo => t_user_info
#table-prefix: t_
引导类
@MapperScan(basePackages = { "com.chy.mall.mapper"})
持久层实体类
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@TableName("t_user") //显示指定表名
public class UserPo {
//指定主键字段。value指定主键字段名称,缺省时默认取变量名的snake写法;type指定主键策略,缺省时默认使用全局配置
@TableId(value = "user_id", type = IdType.AUTO)
private Integer id;
//显示指定对应的数据库字段。缺省时默认取变量名的snake写法(单词全小写,下划线连接)
@TableField("user_name")
private String userName;
private String password;
private String tel;
//不映射此字段。缺省时默认会映射到数据表
@TableField(exist = false)
private boolean isNull;
}
dao层继承通用mapper
//泛型指定对应的po类
public interface UserMapper extends BaseMapper<UserPo> {
}
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.chy.mall.mapper.UserMapper">
</mapper>
service层继承通用service
//泛型指定对应的po类
public interface UserService extends IService<UserPo> {
}
@Service //泛型指定对应的dao类、po类
public class UserServiceImpl extends ServiceImpl<UserMapper, UserPo> implements UserService {
}
通用mapper、service常用方法
通用mapper
userMapper.insert(userPo);
userMapper.updateById(userPo);
userMapper.deleteById(id);
userMapper.deleteBatchIds(ids);
//查询
UserPo userPo = userMapper.selectById(id);
List<UserPo> userPos = userMapper.selectBatchIds(ids);
Integer count = userMapper.selectCount(new QueryWrapper<>(userPo));
List<UserPo> userPos = userMapper.selectList(new QueryWrapper<>(userPo));
UserPo userPo1 = userMapper.selectOne(new QueryWrapper<>(userPo));
通用service
UserPo userPo = userService.getById(id);
//如果结果是多条,会直接抛异常
UserPo userPo = userService.getOne(new QueryWrapper<>(userPo));
List<UserPo> userPos = userService.list();
List<UserPo> userPos = userService.listByIds(ids);
List<UserPo> userPos = userService.list(new QueryWrapper<>(userPo));
int count = userService.count();
int count = userService.count(new QueryWrapper<>(userPo));
userService.save(userPo);
userService.updateById(userPo);
userService.saveOrUpdate(userPo);
userService.removeById(id);
userService.removeByIds(ids);
//批量操作,可指定单个批次处理数量,缺省时默认1000
userService.saveBatch(userPos, 1000);
userService.updateBatchById(userPos, 1000);
userService.saveOrUpdateBatch(userPos, 1000);
userService.saveBatch(userPos);
userService.updateBatchById(userPos);
userService.saveOrUpdateBatch(userPos);
- 通用mapper:封装原子层面的dao操作,方法对应单条sql、没加事务。
- 通用service:提供更高层面的封装,提供了批量添加/更新的方法。这也是 mybatis plus备受诟病的一点,把持久层的批量操作封装在service层,对service层侵入严重。mybatis plus 给通用service中批量添加/更新以及 saveOrUpdate() 方法上都标注了
@Transactional(rollbackFor = Exception.class)
,在事务中使用这些方法时,需要注意事务的传播行为。
typeHandler 自定义类型转换
除了可以在xml映射文件中使用mybatis的指定方式,也可以在po中使用mybatis plus提供的 @TableField 注解指定,insert、update时只要入参是对应的po,select时只要返回值是对应的po、po集合,mybatis plus就会自动拦截 应用指定的 typeHandler
@TableField(typeHandler = EncryptionTypeHandler.class)
private String password;
@TableField(typeHandler = EncryptionTypeHandler.class)
private String tel;
QueryWrapper、UpdateWrapper
QueryWrapper、UpdateWrapper 都是 AbstractWrapper 的子类,用于拼接sql语句。二者都可以拼接where条件,其中 UpdateWrapper 提供了set()方法,可以拼接update的set子句。习惯上:查用QueryWrapper,改UpdateWrapper ,删二者均可。
//可以直接new创建实例
QueryWrapper<UserPo> queryWrapper = new QueryWrapper<>();
UpdateWrapper<UserPo> updateWrapper = new UpdateWrapper<>();
QueryWrapper<UserPo> queryWrapper = new QueryWrapper<>(userPo);
UpdateWrapper<UserPo> updateWrapper = new UpdateWrapper<>(userPo);
//也可以通过Wrappers的静态方法创建实例
QueryWrapper<UserPo> queryWrapper = Wrappers.query();
UpdateWrapper<UserPo> updateWrapper = Wrappers.update();
//po参数会作为where条件拼接到sql中
QueryWrapper<UserPo> queryWrapper = new QueryWrapper<>(userPo)
//也可以拼接其它where条件
.eq("source", "xxx")
.like("url", "%xxx%")
.notLike("url", "%xxx%")
.lt("create_time", "2010-01-01 00:00:00")
.ge("create_time", "2020-01-01 00:00:00")
.between("create_time", "2010-01-01 00:00:00", "2020-01-01 00:00:00")
.in("status", 0, 1, 2)
.notIn("status", 0, 1, 2)
.isNull("source")
.isNotNull("source")
//拼接and、or,会把作为整体放到小括号()中
.and(queryWrapper -> queryWrapper.like("username", "%xxx%").eq("status", 1))
.or(queryWrapper -> queryWrapper.like("username", "%xxx%").eq("status", 1))
//均可指定条件,满足条件时才拼接到sql语句中
.eq(StringUtils.isNotBlank(username), "username", username)
//可以拼接排序、分组
.orderByDesc("id")
.groupBy("status");
List<UserPo> userPos = userMapper.selectList(queryWrapper);
List<UserPo> userPos = userService.list(queryWrapper);
//po参数会作为where条件拼接到sql中
UpdateWrapper<UserPo> updateWrapper = new UpdateWrapper<>(userPo)
//可以拼接其它where条件
.eq("status", 1)
//拼接set子句
.set("statue", 2)
//可以指定拼接条件,满足条件是才拼接
.set(StringUtils.isNotBlank(username), "username", username);
userService.update(updateWrapper);
//po也可以直接作为update()参数,同样会被拼接到where中作为条件
userMapper.update(userPo, updateWrapper);
userService.update(userPo, updateWrapper);
说明
1、po参数作为where条件时,值不为null的字段(只判断是否为null,不判断是否为空串、空格串),才会以 = 等号形式拼接到where条件中。示例
- 【/user/list?id=1】前端传递了id(不为null)、没传username(为null),拼接的sql是【where id=1】,
- 【/user/list?id=1&username=】前端传递了id(不为null)、username(空串,不为null,也会被拼接到sql中),拼接的sql是 【where id=1 and username=‘’】
2、QueryWrapper、UpdateWrapper偏重量级,尽量减少使用
- eq()、like()、notIn()、groupBy()、set()… 大量方法调用会拉低性能;
- 字符串形式的数据表字段名散落在各层中,代码改动时不好查看受影响处,不好维护;
- 使用字符串类型的值拼接sql,有sql注入的风险,即使要拼接sql,也不能直接使用系统外部传入的值。
对数据库记录的操作统一维护在dao层,数据库字段名统一维护在xml映射文件中。对于mb提供的通用mapper、service,避免使用eq()、like() 之类的方法拼接sql,尽量使用 new QueryWrapper<>(po)、updateById() 之类的通用方法,如果dao层没有现成合适的通用方法,或者sql比较复杂,可以在dao层自行抽取 listByQo(qo) 之类的自定义通用方法。
FieldStrategy 字段的插入、更新策略
com.baomidou.mybatisplus.core.config.GlobalConfig.DbConfig
private FieldStrategy insertStrategy = FieldStrategy.NOT_NULL;
private FieldStrategy updateStrategy = FieldStrategy.NOT_NULL;
mp默认的字段插入、更新策略是 NOT_NULL,只把值是 not null 的字段拼接到 insert、update 语句中,值是 null 字段不拼接进去。
插入:null 值拼不拼接到 insert 中,DB处理结果都一样,没影响。
- DB字段没 not null 约束,拼不拼接到 insert 中保存的都是 null 值;
- 有 not null default xxx,拼不拼接到 insert 中保存的都是默认值;
- 只有 not null 没 default,null 值拼不拼接到 insert 中都是报错。
更新:不会更新为 null 值,非必需字段的置为 null 值不生效。
可能为 null 的非必填字段,如果DB原来有值,现在用户清空、删除了表单中此字段的值,提交给后端,mp默认更新策略是 NOT_NULL,不会把 值为 null 的字段拼接到 udpate 语句中,这个 null 值字段也就不会更新到 DB。
常见的 3 种解决方案
1、数据库实体类中,在要更新 null 值的字段上单独指定 updateStrategy (推荐)
@TableField(updateStrategy = FieldStrategy.IGNORED)
private Date birthday;
@TableField(updateStrategy = FieldStrategy.IGNORED)
private Integer height;
2、yml 中在 mp 的全局配置中指定更新策略(禁止)
mybatis-plus:
global-config:
db-config:
update-strategy: ignored
全局配置影响范围大,更新操作 如果没给 PO 的某些字段赋值,null 值会直接覆盖掉这些字段原来的值,风险大,禁止这样做。
3、mp 的 update() 搭配 UpdateWrapper 进行更新,显式给要置为 null 的字段赋 null 值。(太麻烦,不推荐)
mp常用插件
配置插件
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
//分页插件。单一数据库类型尽量指定DbType,避免每次分页时都根据数据库连接url推导
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
//乐观锁插件
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
return interceptor;
}
/**
* 使用分页插件时,需要setUseDeprecatedExecutor(false),防止一、二级缓存出现问题
* 后续useDeprecatedExecutor属性会随旧版本分页插件一起移除
*/
@Bean
public ConfigurationCustomizer configurationCustomizer() {
return configuration -> configuration.setUseDeprecatedExecutor(false);
}
mp插件是以拦截器形式实现的,避免使用太多插件,防止影响性能。
分页插件 PaginationInnerInterceptor
//当前页码(从1开始,缺省默认为1)、每页记录数(缺省默认为10)、是否统计满足的记录总数(缺省默认为true)
//为true查询时先select count()查询满足条件的记录总数,填充total、pages字段,再查询满足条件的分页记录list,会多查询一次
Page<UserPo> userPage = new Page<>(1, 10);
// Page<UserPo> userPage = new Page<>(1, 10, true);
//执行分页查询,数据会自动填充到page实例中。没有查询条件时可缺省QueryWrapper
userMapper.selectPage(userPage, new QueryWrapper<>(userPo));
// userMapper.selectPage(userPage);
// Page<UserPo> userPage = userMapper.selectPage(new Page<>(1, 10), new QueryWrapper<>(userPo));
//满足条件的总记录数、总页数
long total = userPage.getTotal();
long pages = userPage.getPages();
List<UserPo> userPos = userPage.getRecords();
通用service分页和通用mapper分页差不多,只不过方法名是page()。
mybatis plus的分页 page必须要作为(第一个)参数传入,对dao层查询方法侵入大,对自定义通用查询加分页时,需要单独写一个分页查询,不像 pagehelper 对dao层代码零侵入,没 pagehelper 方便,mybatis plus官方不推荐使用 pagehelper 的分页
- https://github.com/baomidou/mybatis-plus/issues/1665
- https://github.com/baomidou/mybatis-plus/issues/1668
public interface UserDao extends BaseMapper<UserPo> {
List<UserPo> listByQo(UserQo qo);
Page<UserPo> page(Page<UserPo> page, @Param("qo") UserQo qo);
}
我个人更推荐使用 page helper 分页。注意:同时使用 mybatis plus、pagehelper 会存在依赖冲突,需要排除pagehelper中的mybatis、mybatis-spring
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.2.3</version>
<!-- 解决与mybatis plus的依赖冲突 -->
<exclusions>
<exclusion>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
</exclusion>
<exclusion>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
</exclusion>
</exclusions>
</dependency>
乐观锁插件 OptimisticLockerInnerInterceptor
//po version字段上标注@Version,推荐使用Integer、Long类型
@Version
private Long version;
UserPo oldUserPo = userService.getById(id);
UserPo updatePo = UserPo.builder()
.id(id)
.username(username)
//update时需要携带原来的版本号
.version(oldUserPo.getVersion())
.build();
//版本号插件只支持 updateById(id)、update(entity,wrapper) 方法,其中update(entity,wrapper)的wrapper不能复用
//set version=旧版本号+1 where version=旧版本号,会自动更新version、将newVersion回填到updatePo中,版本号不匹配时受影响记录数为0,返回false
boolean result = userService.updateById(updatePo);