MybatisPlus
快速入门
入门案例
1.引入MybatisPlus的起步依赖
MybatisPlus官方提供了starter,其中集成了Mybatis和MybatisPlus的所有功能,并且实现了自动装配的效果。因此我们可以用MybatisPlus的starter代替Mybatis的starter:
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.1</version>
</dependency>
2.定义Mapper
自定义的Mapper继承MybatisPlus提供的BaseMapper接口:
public interface UserMapper extends BaseMapper<User> {
}
常见注解
MybatisPlus通过扫描实体类,并基于反射获取实体类信息作为数据库表信息。
- 类名驼峰转下划线作为表名
- 名为id的字段作为主键
- 变量名驼峰下划线作为表的字段名
MybatisPlus中的几个比较常用的注解如下:
@TableName:用来指定表名
@TableId:用来指定表中的主键字段信息
@TableFieId:用来指定表中的普通字段信息
IdType枚举:
-
AUTO:数据库自增长
-
INPUT:通过set方法自行输入
-
ASSIGN_ID:分配 ID,接口IdentifierGenerator的方法nextId来生成id,默认实现类为DefaultIdentifierGenerator雪花算法
使用@TableField的常见场景:
-
成员变量名与数据库字段名不一致
-
成员变量名以is开头,且是布尔值
-
成员变量名与数据库关键字冲突
-
成员变量不是数据库字段
@TableName("tb_user")
public class User {
@TableId(type = IdType.AUTO)
private Long id;
//@TableField(exist = false)
private String username;
private String password;
private String phone;
private String info;
private Integer status;
private Integer balance;
private LocalDateTime createTime;
private LocalDateTime updateTime;
}
常见配置
MybatisPlus的配置继承了Mybatis原生配置和一些自己特有的配置。例如
mybatis-plus:
type-aliases-package: com.itheima.mp.domain.po # 别名扫描包
mapper-locations: "classpath*:/mapper/**/*.xml" # Mapper.xml文件地址,默认值
configuration:
map-underscore-to-camel-case: true # 是否开启下划线和驼峰的映
cache-enabled: false # 是否开启二级缓存
global-config:
db-config:
id-type: assign_id # id为雪花算法生
update-strategy: not_null # 更新策略:只更新非空字段
具体可参考官方文档使用配置 | MyBatis-Plus (baomidou.com)
核心功能
条件构造器
基于QueryWrapper的查询
需求:
- 查询出名字中带o的,存款大于等于1000元的人的id、username、info、balance字段
SELECT id,username,info,balance FROM user WHERE username LIKE ? AND balance >= ?
- 更新用户名为jack的用户的余额为2000
UPDATE user SET balance = 2000 WHERE (username = "jack")
基于UpdateWrapper的更新
-
需求:更新id为1,2,4的用户的余额,扣200
UPDATE user SET balance = balance - 200 WHERE id in (1, 2, 4)
条件构造器的用法:
-
QueryWrapper和LambdaQueryWrapper通常用来构建select、delete、update的where条件部分
-
UpdateWrapper和LambdaUpdateWrapper通常只有在set语句比较特殊才使用
-
尽量使用LambdaQueryWrapper和LambdaUpdateWrapper,避免硬编码
自定义sql
我们可以利用MyBatisPlus的Wrapper来构建复杂的Where条件,然后自己定义SQL语句中剩下的部分。
-
需求:将id在指定范围的用户(例如1、2、4 )的余额扣减指定值
-
基于Wrapper构建where条件
List<Long> ids = List.of(1L, 2L, 4L); int amount = 200;// 1.构建条件 LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<User>().in(User::getId, ids);// 2.自定义SQL方法调用 userMapper.updateBalanceByIds(wrapper, amount);
-
在mapper方法参数中用Param注解声明wrapper变量名称,必须是ew
void updateBalanceByIds(@Param("ew") LambdaQueryWrapper<User> wrapper, @Param("amount") int amount);
-
自定义SQL,并使用Wrapper条件
<update id="updateBalanceByIds"> UPDATE tb_user SET balance = balance - #{amount} ${ew.customSqlSegment} </update>
-
Service接口
MP的Service接口使用流程是怎样的?
-
自定义Service接口继承IService接口
public interface IUserService extends IService<User> { }
-
自定义Service实现类,实现自定义接口并继承ServiceImpl类
@Service public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService { }
案例:基于Restful风格实现下列接口
@RequestMapping("/users")
@RestController
@RequiredArgsConstructor
public class UserController {
//@Autowired 不推荐 springboot 推荐用构造函数注入 ,
private final IUserService userService;
/* 用构造函数注入太繁琐了 加final 用@RequiredArgsConstructor 注解
public UserController(IUserService iUserService) {
this.iUserService = iUserService;
}*/
@PostMapping
@ApiOperation("新增用户接口")
public void saveUser(@RequestBody UserFormDTO userFormDTO){
//1.把DDTO 拷贝到 PO
User user = BeanUtil.copyProperties(userFormDTO,User.class);
//2。新增
userService.save(user);
}
@DeleteMapping("/{id}")
@ApiOperation("删除用户接口")
public void removeUser(@ApiParam("用户id") @PathVariable("id") Long id){
userService.removeById(id);
}
@GetMapping("/{id}")
@ApiOperation("根据id查询用户接口")
public UserVO queryUserById(@ApiParam("用户id") @PathVariable("id") Long id){
User user = userService.getById(id);
return BeanUtil.copyProperties(user, UserVO.class);
}
@GetMapping
@ApiOperation("根据id批量查询用户接口")
public List<UserVO> queryUserByIds(@ApiParam("用户id集合") @RequestParam("ids") List<Long> ids){
List<User> users = userService.listByIds(ids);
return BeanUtil.copyToList(users, UserVO.class);
}
}
自定义service
controller
@ApiOperation("扣减用户余额接口")
@PutMapping("/{id}/deduction/{money}")
public void deductMoneyById(@ApiParam("用户id")@PathVariable Long id, @ApiParam("扣减的余额")@PathVariable Integer money){
userService.deductBalance(id,money);
}
service
void deductBalance(Long id, Integer money);
serviceImpl
@Override
public void deductBalance(Long id, Integer money) {
//1.查询用户
User user = getById(id);
//2.校验用户状态
if (user == null || user.getStatus() == 2){
throw new RuntimeException("用户状态异常");
}
//3.校验余额是否充足
if (user.getBalance() < money){
throw new RuntimeException("用户余额不足");
}
//4.扣减余额 update tb-user set balance = balance - ?
baseMapper.deductBalance(id,money);
}
mapper
@Update("update tb_user set balance = balance - #{money} where id = #{id}")
void deductBalance(@Param("id") Long id, @Param("money") Integer money);
案例:IService的Lambda查询
需求:实现一个根据复杂条件查询用户的接口,查询条件如下:
-
name:用户名关键字,可以为空
-
status:用户状态,可以为空
-
minBalance:最小余额,可以为空
-
maxBalance:最大余额,可以为空
mybatis 实现:
<select id="queryUsers" resultType="com.itheima.mp.domain.po.User">
SELECT *
FROM tb_user
<where>
<if test="name != null">
AND username LIKE #{name}
</if>
<if test="status != null">
AND `status` = #{status}
</if>
<if test="minBalance != null and maxBalance != null">
AND balance BETWEEN #{minBalance} AND #{maxBalance}
</if>
</where>
</select>
mybatisPlus实现:
@GetMapping("/list")
@ApiOperation("根据复杂条件批量查询用户接口")
public List<UserVO> queryUsers(UserQuery userQuery){
//1.查询用户po
List<User> users = userService.queryUsers(userQuery.getName(),userQuery.getStatus(),userQuery.getMinBalance(),userQuery.getMaxBalance());
//2.po 拷贝到vo
return BeanUtil.copyToList(users, UserVO.class);
}
List<User> queryUsers(String name, Integer status, Integer minBalance, Integer maxBalance);
@Override
public List<User> queryUsers(String name, Integer status, Integer minBalance, Integer maxBalance) {
return lambdaQuery() // 复杂条件查询
.like(name!= null,User::getUsername,name)
.eq(status!= null,User::getStatus,status)
.ge(minBalance!= null,User::getBalance,minBalance)
.le(maxBalance!= null,User::getBalance,maxBalance)
.list();
}
案例:IService的Lambda更新
需求:改造根据id修改用户余额的接口,要求如下
-
完成对用户状态校验
-
完成对用户余额校验
-
如果扣减后余额为0,则将用户status修改为冻结状态(2)
int remainBalance = user.getBalance() - money;
// baseMapper.deductBalance(id,money);
lambdaUpdate() // 复杂条件跟新
.set(User::getBalance, remainBalance)
.set(remainBalance == 0, User::getStatus, 2)
.eq(User::getId, id) //构建
.eq(User::getBalance ,user.getBalance())// 乐观锁
.update(); // 执行
案例:IService批量新增
-
普通for循环插入
@Test void testSaveOneByOne() { Long b = System.currentTimeMillis(); for (int i = 0; i < 100000; i++) { iUserService.save(buildUser(i)); } Long e = System.currentTimeMillis(); System.out.println("耗时:" + (e - b)); }
耗时11万毫秒
-
IService的批量插入
@Test void testSaveBatch() { //我们准备每次批量插入1000条,插入100次 即100000条数据 //1.准备一个容量为1000的集合 List<User> list = new ArrayList<>(1000); Long b = System.currentTimeMillis(); for (int i = 0; i < 100000; i++) { //添加一个user list.add(buildUser(i)); if (i % 1000 == 0) { //每1000条插入一次 iUserService.saveBatch(list); //清空 list.clear(); } } Long e = System.currentTimeMillis(); System.out.println("耗时:" + (e - b)); }
耗时5万毫秒
-
开启rewriteBatchedStatements=true参数
耗时5000毫秒
批处理方案:
普通for循环逐条插入速度极差,不推荐
-
MP的批量新增,基于预编译的批处理,性能不错
-
配置jdbc参数,开rewriteBatchedStatements,性能最好
-
扩展功能
代码生成
-
通过MybatisPlus 插件
-
配置插件数据库连接 ,注意加上表名( 报错:url加上 ?serverTimezone=UTC)
-
选择生成配置
静态工具
案例:静态工具查询
-
改造根据id查询用户的接口,查询用户的同时,查询出用户对应的所有地址
-
实现根据用户id查询收货地址功能,需要验证用户状态,冻结用户抛出异常(练习)
@Override
public UserVO queryUserAndAdressById(Long id) {
//1,查询用户
User user = getById(id);
if(user == null || user.getStatus() == 2){
throw new RuntimeException("用户状态异常");
}
//2.查询地址
List<Address> addresses = Db.lambdaQuery(Address.class)
.eq(Address::getUserId, id)
.list();
//3.封装Vo
UserVO userVO = BeanUtil.copyProperties(user, UserVO.class);
if (CollUtil.isNotEmpty(addresses)){
userVO.setAddresses(BeanUtil.copyToList(addresses, AddressVO.class));
}
return userVO;
}
避免了循环依赖
- 改造根据id批量查询用户的接口,查询用户的同时,查询出用户对应的所有地址
@Override
public List<UserVO> queryUserAndAddressByIds(List<Long> ids) {
// 1.查询用户
List<User> users = listByIds(ids);
if (CollUtil.isEmpty(users)) {
return Collections.emptyList();
}
// 2.查询地址
// 2.1.获取用户id集合
List<Long> userIds = users.stream().map(User::getId).collect(Collectors.toList());
// 2.2.根据用户id查询地址
List<Address> addresses = Db.lambdaQuery(Address.class).in(Address::getUserId, userIds).list();
// 2.3.转换地址VO
List<AddressVO> addressVOList = BeanUtil.copyToList(addresses, AddressVO.class);
// 2.4.用户地址集合分组处理,相同用户的放入一个集合(组)中
Map<Long, List<AddressVO>> addressMap = new HashMap<>(0);
if (CollUtil.isNotEmpty(addressVOList)) {
addressMap = addressVOList.stream().collect(Collectors.groupingBy(AddressVO::getUserId));
}
// 3.转换VO返回
List<UserVO> list = new ArrayList<>(users.size());
for (User user : users) {
// 3.1.转换User的PO为VO
UserVO vo = BeanUtil.copyProperties(user, UserVO.class);
list.add(vo);
// 3.2.转换地址VO
vo.setAddresses(addressMap.get(user.getId()));
}
return list;
}
强大的steam流
逻辑删除
逻辑删除就是基于代码逻辑模拟删除效果,但并不会真正删除数据。思路如下:
-
在表中添加一个字段标记数据是否被删除
-
当删除数据时把标记置为1
-
查询时只查询标记为0的数据
例如逻辑删除字段为deleted:
- 删除操作:
UPDATE user SET deleted = 1 WHERE id = 1 AND deleted = 0
-
查询操作:
SELECT * FROM user WHERE deleted = 0
MybatisPlus提供了逻辑删除功能,无需改变方法调用的方式,而是在底层帮我们自动修改CRUD的语句。我们要做的就是在application.yaml文件中配置逻辑删除的字段名称和值即可:
mybatis-plus:
global-config:
db-config:
logic-delete-field: deleted # 全局逻辑删除的实体字段名,字段类型可以是boolean、integer
logic-delete-value: 1 # 逻辑已删除值(默认为 1)
logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
逻辑删除本身也有自己的问题,比如:
-
会导致数据库表垃圾数据越来越多,影响查询效率
-
SQL中全都需要对逻辑删除字段做判断,影响查询效率
因此,不太推荐采用逻辑删除功能,如果数据不能删除,可以采用把数据迁移到其它表的办法。
枚举处理器
- 配置统一的枚举类型处理器,实现类型转换
mybatis-plus:
configuration:
default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler
使用枚举类代替代码中的 1,2 ,3,4
@Getter
public enum UserStatus {
NORMAL(1,"正常"),
FROZEN(2,"冻结"),
;
@EnumValue //通过这个注解表明存入数据库的值 实现枚举类型的变量与数据库字段的转换
@JsonValue // 通过这个注解表明返回前端的字段
private final int value;
private final String desc;
UserStatus(int value, String desc) {
this.value = value;
this.desc = desc;
}
}
JSON处理器
当数据库中有一个json类型的字段(—> 转对象)
-
先定义一个对象实体
@Data @NoArgsConstructor @AllArgsConstructor(staticName = "of") public class UserInfo { private Integer age; private String intro; private String gender; }
@Data @TableName(value = "tb_user",autoResultMap = true) // 开启自动结果映射 public class User { @TableField(typeHandler = JacksonTypeHandler.class) // 指定类型处理器 private UserInfo info; }
插件功能
分页插件
首先,要在配置类中注册MyBatisPlus的核心插件,同时添加分页插件:
@Configuration
public class MybatisConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor(){
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 1.创建分页插件
PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor(DbType.MYSQL);
paginationInnerInterceptor.setMaxLimit(1000L);
// 2.添加分页插件
interceptor.addInnerInterceptor(paginationInnerInterceptor);
return interceptor;
}
}
接着,就可以使用分页的API了:
@Test
void testPageQuery(){
//1.准备分页条件
int pageNo = 1;
int pageSize = 2;
//1.1分页条件
Page<User> page = Page.of(pageNo,pageSize);
//1.2排序条件
page.addOrder(new OrderItem("balance",true)); // 先按照金额排序 金额相同再按照id排序
page.addOrder(new OrderItem("id",true));
//2.分页查询
Page<User> p = iUserService.page(page);
System.out.println(p.getTotal());
System.out.println(p.getPages());
List<User> users = p.getRecords();
users.forEach(System.out::println);
}
通用分页实体
封装统一查询条件实体
@Data
@ApiModel(description = "分页查询实体")
public class PageQuery {
@ApiModelProperty("页码")
private Integer pageNo = 1;
@ApiModelProperty("页码")
private Integer pageSize = 5;
@ApiModelProperty("排序字段")
private String sortBy;
@ApiModelProperty("是否升序")
private Boolean isAsc = true;
}
userPageQuery 继承
@Data
@ApiModel(description = "用户查询条件实体")
public class UserQuery extends PageQuery {
@ApiModelProperty("用户名关键字")
private String name;
@ApiModelProperty("用户状态:1-正常,2-冻结")
private Integer status;
@ApiModelProperty("余额最小值")
private Integer minBalance;
@ApiModelProperty("余额最大值")
private Integer maxBalance;
}
封装统一的返回结果实体
@Data
@ApiModel(description = "分页结果")
public class PageDTO<T> {
@ApiModelProperty("总条数")
private Long total;
@ApiModelProperty("总页数")
private Long pages;
@ApiModelProperty("集合")
private List<T> list;
}
@Override
public PageDTO<UserVO> queryUsersPage(UserQuery query) {
String name = query.getName();
Integer status = query.getStatus();
// 1.构建分页条件
Page<User> page = Page.of(query.getPageNo(), query.getPageSize());
//1.2 排序条件
if (StrUtil.isNotEmpty(query.getSortBy())) {
//不为空
page.addOrder(new OrderItem(query.getSortBy(), query.getIsAsc()));
} else {
//为空
page.addOrder(new OrderItem("update_time", false));
}
// 2.分页查询
Page<User> p = lambdaQuery()
.like(name != null, User::getUsername, name)
.eq(status != null, User::getStatus, status)
.page(page);
// 3.封装VO结果
PageDTO<UserVO> dto = new PageDTO<>();
dto.setTotal(p.getTotal());
dto.setPages(p.getPages());
List<User> records = p.getRecords();
if (CollUtil.isEmpty(records)) {
dto.setList(Collections.emptyList());
return dto;
}
//拷贝user 的vo
List<UserVO> vos = BeanUtil.copyToList(records, UserVO.class);
dto.setList(vos);
//返回结果
return dto;
}
在PageQuery中定义方法,将PageQuery对象转为MyBatisPlus中的Page对象
@Data
@ApiModel(description = "分页查询实体")
public class PageQuery {
@ApiModelProperty("页码")
private Integer pageNo = 1;
@ApiModelProperty("页码")
private Integer pageSize = 5;
@ApiModelProperty("排序字段")
private String sortBy;
@ApiModelProperty("是否升序")
private Boolean isAsc = true;
public <T> Page<T> toMpPage(OrderItem... items){
// 1.分页条件
Page<T> page = Page.of(pageNo, pageSize);
// 2.排序条件
if(StrUtil.isNotBlank(sortBy)){
// 不为空
page.addOrder(new OrderItem(sortBy, isAsc));
}else if(items != null){
// 为空,默认排序
page.addOrder(items);
}
return page;
}
public <T> Page<T> toMpPage(String defaultSortBy, Boolean defaultAsc){
return toMpPage(new OrderItem(defaultSortBy, defaultAsc));
}
public <T> Page<T> toMpPageDefaultSortByCreateTime(){
return toMpPage(new OrderItem("create_time", false));
}
public <T> Page<T> toMpPageDefaultSortByUpdateTime(){
return toMpPage(new OrderItem("update_time", false));
}
}
在PageDTO中定义方法,将MyBatisPlus中的Page结果转为PageDTO结果
@Data
@ApiModel(description = "分页结果")
public class PageDTO<T> {
@ApiModelProperty("总条数")
private Long total;
@ApiModelProperty("总页数")
private Long pages;
@ApiModelProperty("集合")
private List<T> list;
public static <PO, VO> PageDTO<VO> of(Page<PO> p, Class<VO> clazz) {
PageDTO<VO> dto = new PageDTO<>();
// 1.总条数
dto.setTotal(p.getTotal());
// 2.总页数
dto.setPages(p.getPages());
// 3.当前页数据
List<PO> records = p.getRecords();
if (CollUtil.isEmpty(records)) {
dto.setList(Collections.emptyList());
return dto;
}
// 4.拷贝user的VO
dto.setList(BeanUtil.copyToList(records, clazz));
// 5.返回
return dto;
}
public static <PO, VO> PageDTO<VO> of(Page<PO> p, Function<PO, VO> convertor) {
PageDTO<VO> dto = new PageDTO<>();
// 1.总条数
dto.setTotal(p.getTotal());
// 2.总页数
dto.setPages(p.getPages());
// 3.当前页数据
List<PO> records = p.getRecords();
if (CollUtil.isEmpty(records)) {
dto.setList(Collections.emptyList());
return dto;
}
// 4.拷贝user的VO
dto.setList(records.stream().map(convertor).collect(Collectors.toList()));
// 5.返回
return dto;
}
}
简化
@Override
public PageDTO<UserVO> queryUsersPage(UserQuery query) {
String name = query.getName();
Integer status = query.getStatus();
// 1.构建分页条件
Page<User> page = query.toMpPageDefaultSortByUpdateTime();
// 2.分页查询
Page<User> p = lambdaQuery()
.like(name != null, User::getUsername, name)
.eq(status != null, User::getStatus, status)
.page(page);
// 3.封装VO结果
return PageDTO.of(p, user -> {
// 1.拷贝基础属性
UserVO vo = BeanUtil.copyProperties(user, UserVO.class);
// 2.处理特殊逻辑
vo.setUsername(vo.getUsername().substring(0, vo.getUsername().length() - 2) + "**");
return vo;
});
}
或者
@Override
public PageDTO<UserVO> queryUsersPage(UserQuery query) {
String name = query.getName();
Integer status = query.getStatus();
// 1.构建分页条件
Page<User> page = query.toMpPageDefaultSortByUpdateTime();
// 2.分页查询
Page<User> p = lambdaQuery()
.like(name != null, User::getUsername, name)
.eq(status != null, User::getStatus, status)
.page(page);
// 3.封装VO结果
return PageDTO.of(p, UserVO.class);
}
3.封装VO结果
return PageDTO.of(p, user -> {
// 1.拷贝基础属性
UserVO vo = BeanUtil.copyProperties(user, UserVO.class);
// 2.处理特殊逻辑
vo.setUsername(vo.getUsername().substring(0, vo.getUsername().length() - 2) + “**”);
return vo;
});
}
或者
```java
@Override
public PageDTO<UserVO> queryUsersPage(UserQuery query) {
String name = query.getName();
Integer status = query.getStatus();
// 1.构建分页条件
Page<User> page = query.toMpPageDefaultSortByUpdateTime();
// 2.分页查询
Page<User> p = lambdaQuery()
.like(name != null, User::getUsername, name)
.eq(status != null, User::getStatus, status)
.page(page);
// 3.封装VO结果
return PageDTO.of(p, UserVO.class);
}