作者简介:☕️大家好,我是intelligent_M,一个Java后端开发者!
当前专栏:intelligent_M——MyBatisPlus ,CSDN博客。后续会更新Java相关技术栈以及链表哈希表二叉树…,回溯算法贪心算法…等等算法题。
创作不易 欢迎点赞评论!!!
MyBatisPlus
MyBatisPlus是什么?有什么功能?
- MyBatisPlus是一个基于MyBatis的增强工具(只做增强不做修改无侵入性),能够提高开发效率和代码质量。它提供了很多MyBatis不具备的功能,包括:
-
生成器:通过简单的配置可以自动生成JavaBean、Mapper和XML文件。
-
分页插件:支持多种数据库的分页查询,包括MySQL、Oracle、SQLServer等。
-
条件构造器:提供一种类似于SQL语句的方式来构造查询条件,避免了手写SQL的繁琐。
-
SQL注入器:通过自定义SQL注入器,可以在不使用XML文件的情况下编写复杂SQL语句。
-
数据库操作工具类:提供了一些常用的数据库操作方法,例如批量插入、更新、删除等。
-
其他:还提供了缓存、性能分析等功能,以及对Spring Boot的支持。
- 总之,MyBatisPlus提供了很多便利的功能,可以大大减少开发人员的工作量,提高开发效率和代码质量。
一、快速入门
1.入门案例
- 需求:基于项目,实现下列功能:
- 新增用户功能
- 根据id查询用户
- 根据id批量查询用户
- 根据id更新用户
- 根据id删除用户
- 1.引入依赖
MybatisPlus提供了starter,实现了自动Mybatis以及MybatisPlus的自动装配功能,坐标如下:
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.1</version>
</dependency>
- 2.定义Mapper
自定义的Mapper继承MybatisPlus提供的BaseMapper接口:
- 3.调用继承的BaseMapper接口的方法
@SpringBootTest
class UserMapperTest {
@Autowired
private UserMapper userMapper;
@Test
void testInsert() {
User user = new User();
user.setId(5L);
user.setUsername("Lucy");
user.setPassword("123");
user.setPhone("18688990011");
user.setBalance(200);
user.setInfo("{\"age\": 24, \"intro\": \"英文老师\", \"gender\": \"female\"}");
user.setCreateTime(LocalDateTime.now());
user.setUpdateTime(LocalDateTime.now());
//userMapper.saveUser(user);//注释掉的都是用Mybatis xml文件实现的方法
userMapper.insert(user);
}
@Test
void testSelectById() {
//User user = userMapper.queryUserById(5L);
User user = userMapper.selectById(5L);
System.out.println("user = " + user);
}
@Test
void testQueryByIds() {
//List<User> users = userMapper.queryUserByIds(List.of(1L, 2L, 3L, 4L));
List<User> users = userMapper.selectBatchIds(List.of(1l, 2l, 3l, 4l));
users.forEach(System.out::println);
}
@Test
void testUpdateById() {
User user = new User();
user.setId(5L);
user.setBalance(20000);
//userMapper.updateUser(user);
userMapper.updateById(user);
}
@Test
void testDeleteUser() {
//userMapper.deleteUser(5L);
userMapper.deleteById(5L);
}
}
2.常见注解
- MybatisPlus通过扫描实体类,并基于反射获取实体类信息作为数据库表的信息
- 通常有以下约定
- 类名驼峰转下划线作为表名
- 名为id的字段作为主键
- 变量名驼峰转下划线作为表的字段名
-
MybatisPlus中比较常用的几个注解如下:
-
@Tablename:用来指定表名(与数据库表名不一致时可指定)
-
@TableId:用来指定表中的主键字段信息
- IdType枚举:
- AUTO:数据库自增
- INPUT:通过set方法自行输入
- ASSIGN_ID:分配ID,接口IdentifierGenerator的方法nextId来生成 id,默认实现类为DefaultidentifierGenerator雪花算法
- IdType枚举:
-
@TableFiled:用来指定表中的普通字段信息
- @TableField常见使用场景:
- 成员变量名与数据库字段名不一致
- 成员变量名以is开头,且是布尔值
- 成员变量名与数据库关键字冲突
- 成员变量不是数据库字
- @TableField常见使用场景:
3.常见配置
- MybatisPlus的配置项继承了MyBatis原生配置和一些自己特有的配置。例如:
4.MyBatisPlus使用的基本流程是什么?
- 1.引入起步依赖
- 2.自定义Mapper继承BaseMapper
- 3.在实体类上添加注解声名表信息
- 4.在application.yml中根据需要添加配置
5.乐观锁与悲观锁
-
乐观锁是一种轻量级锁,通过在数据表中添加一个版本号字段或时间戳字段,每次更新数据时将版本号或时间戳加一,并在更新语句中带上当前版本号或时间戳的条件,当版本号或时间戳不一致时更新失败。乐观锁不会对数据库进行锁定,而是在应用层上检查并防止并发的数据修改,适用于多读少写的情况。
-
MyBatis-Plus 的乐观锁实现方式是:在实体类中添加一个 @Version 注解标注版本号字段,更新数据时 MyBatis-Plus 会自动在更新语句中带上当前版本号的条件。
-
悲观锁是一种重量级锁,在对数据库执行操作前对数据库表或数据行进行锁定,防止其他事务进行修改,直到当前事务提交或回滚后才释放锁。悲观锁适用于多写少读的情况。
-
MyBatis-Plus 的悲观锁实现方式是:使用 selectForUpdate 或 selectForUpdateNowait 方法进行锁定查询,将数据行锁定,直到事务提交或回滚后才释放锁。需要注意的是使用悲观锁会对数据库进行锁定,可能会影响系统吞吐量和性能。
二、核心功能
1.条件构造器
- MybatisPlus支持各种复杂的where条件,可以瞒足日常开发需求
- 案例需求(基于QueryWrapper的查询)
- 1.查询出名字中带o的,存款大于等于1000元的人的id,username,info,balance字段
@Test//1.查询出名字中带o的,存款大于等于1000元的人的id,username,info,balance字段
void testQueryWrapper(){
//1.构造条件
QueryWrapper<User> wrapper = new QueryWrapper<User>()
.select("id", "username", "info", "balance")
.like("username", "o")
.ge("balance", 1000);
//2.查询
List<User> users = userMapper.selectList(wrapper);
users.forEach(System.out::println);//遍历输出
}
- 2.更新用户名为jack的用户的余额
@Test//2.更新用户名为jack的用户的余额
void testUpdateByQueryWrapper(){
//1.要更新的数据
User user = new User();
user.setBalance(2000);
//2.更新的条件
QueryWrapper<User> wrapper = new QueryWrapper<User>().eq("username", "jack");
//3.执行更新
userMapper.update(user, wrapper);
}
- 案例(基于UpdateWrapper的更新)
- 更新id为1,2,4的用户的跃,扣200
@Test//更新id为1,2,4的用户的余额,扣200
void testUpdateWrapper(){
List<Long> ids = List.of(1L, 2L, 4L);
UpdateWrapper<User> wrapper = new UpdateWrapper<User>()
.setSql("balance = balance - 200")
.in("id", ids);
userMapper.update(null, wrapper);
}
- 关于Lambda的条件构造器(利用反射获取实体类的字段,避免了硬编码)
@Test//1.查询出名字中带o的,存款大于等于1000元的人的id,username,info,balance字段
void testQueryWrapper(){
//1.构造条件
QueryWrapper<User> wrapper = new QueryWrapper<User>()
.select("id", "username", "info", "balance")
.like("username", "o")
.ge("balance", 1000);
//2.查询
List<User> users = userMapper.selectList(wrapper);
users.forEach(System.out::println);//遍历输出
}
- 条件构造器的用法:
- QueryWrapper和LambdaQueryWrapper通常用来构建select,delete, update,和where条件部分
- UpdateWrapper和LambdaUpdateWrapper通常只有在set语句比较特殊才使用
- 尽量使用LambdaQueryWrapper和LambdaUpdateWrapper,避免硬编码
2.自定义SQL
- 我们可以利用MyBatisPlus的Wrapper来构建复杂的where条件,然后自己定义SQL语句中的剩下的部分
- 1.基于Wrapper构建where条件
//自定义Sql
void testCustomSqlUpdate(){
//1.更新条件
List<Long> ids = List.of(1L, 2L, 4L);
int amount = 200;
//2.定义条件
QueryWrapper<User> wrapper = new QueryWrapper<User>().in("id", ids);
//3.调用自定义sql方法
userMapper.updateBalanceByIds(wrapper , amount);
}
- 2.在mapper方法参数中用Param注解声明wrapper变量名称,必须是ew
void updateBalanceByIds(@Param(Constants.WRAPPER) QueryWrapper<User> wrapper, @Param("amount") int amount);
- 3.自定义SQL,并使用Wrapper条件(写在mapper.xml文件中或者用注解写sql)
<update id="updateBalanceByIds">
UPDATE user SET balance = balance - #{amount} ${ew.customSqlSegment}
</update>
3.IService接口基本用法
- UserServiceImpl类间接实现了IService接口有了接口里的方法,UserServiceImpl类继承了ServiceImpl有了IService里面的方法的实现
- Mp的Service接口使用流程是怎样的?
- 自定义Service接口继承IService接口
- 自定义Service实现类,实现自定义接口并继承ServiceImpl类
- 自定义Service接口继承IService接口
4.IService开发基础业务接口
- 案例:基于Restful风格实现下列接口
/**
* @author MBY
* @version 1.0
*/
@Api(tags = "用户管理接口")//swagger注解
@RequestMapping("/users")
@RestController
@RequiredArgsConstructor//必备的构造函数,它只会对那么需要在一开始就初始化的变量去做构造
public class UserController {
private final IUserService userService;//加了final将来是构造函数的一部分会被spring自动注入
@ApiOperation("新增用户接口")
@PostMapping
public void saveUser(@RequestBody UserFormDTO userDTO){
//1.把DTO拷贝到PO
User user = BeanUtil.copyProperties(userDTO, User.class);
//新增
userService.save(user);
}
@ApiOperation("删除用户接口")
@DeleteMapping("{id}")
public void deleteUserById(@ApiParam("用户id") @PathVariable("id") Long id){
userService.removeById(id);
}
@ApiOperation("根据id查询用户接口")
@GetMapping("{id}")
public UserVO queryUserById(@ApiParam("用户id") @PathVariable("id") Long id){
//1.查询用户PO
User user = userService.getById(id);
//2.把PO拷贝到VO
return BeanUtil.copyProperties(user, UserVO.class);
}
@ApiOperation("根据id批量查询用户接口")
@GetMapping
public List<UserVO> queryUserByIds(@ApiParam("用户id集合") @RequestParam("ids") List<Long> ids){
//1.查询用户PO
List<User> users = userService.listByIds(ids);
//2.把PO拷贝到VO
return BeanUtil.copyToList(users, UserVO.class);
}
}
5.IService开发复杂业务接口
- 延续上面的第五个接口 根据id扣减余额
- controller层
@ApiOperation("扣减用户余额接口")
@PutMapping("/{id}/deduction/{money}")
public void deductBalance(
@ApiParam("用户id") @PathVariable("id") Long id,
@ApiParam("扣减的金额") @PathVariable("money" )Integer money){
userService.deductBalance(id, money);
}
- IUserService
void deductBalance(Long id, Integer money);
- UserServiceImpl
@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 user set balance = balance - ?
baseMapper.deductBalance(id, money);
}
- UserMapper
@Update("update user set balance = balance - #{money} where id = #{id}")
void deductBalance(Long id, Integer money);
- 总结
- 对于简单的增删改查业务,我们都可以在controller层调用mp中的service方法,无需写自定义的service或者mapper。
- 当业务相对复杂,mp中的方法无法满足我们的需求时,就需要我们自定义service方法并且在其中编写业务逻辑。
- 当baseMapper中的方法不足以满足业务需求就需要我们自定义方法自定义SQL语句(用注解或者mapper.xml配置文件)。
6.IService的Lambda方法
- 案例一(LambdaQuery)
- 需求:实现一个根据复杂条件查询用户的接口,查询条件如下:
- name:用户名关键字,可以为空
- status:用户状态,可以为空
- minBalance:最小余额,可以为空
- maxBalance:最大余额,可以为空
- controller
@ApiOperation("根据复杂条件查询用户接口")
@GetMapping("/list")
public List<UserVO> queryUser(UserQuery query){
//1.查询用户PO
List<User> users = userService.queryUsers(
query.getName(), query.getStatus(), query.getMinBalance(), query.getMaxBalance());
//2.把PO拷贝到VO
return BeanUtil.copyToList(users, UserVO.class);
}
- UserServiceImpl
@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更新(LambdaUpdate)
- 需求:改造根据id修改用户余额的接口,要求如下:
- 完成对用户状态校验
- 完成对用户余额校验
- 如果扣减后余额为0,则将用户status修改为冻结状态(2)
@Override
@Transactional
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 user set balance = balance - ?
//baseMapper.deductBalance(id, money);
int remainBalance = user.getBalance() - money;
lambdaUpdate()
.set(User::getBalance, remainBalance)
.set(remainBalance == 0, User::getStatus, 2)
.eq(User::getId, id)
.eq(User::getBalance, user.getBalance())// 乐观锁
.update();
}
- 总结
- LambdaQuery主要用于帮助我们构建复杂查询
- LambdaUpdate主要用于帮助我们构建复杂更新
- update方法最后要加上.update()否则不会执行
- 两者都可加上条件(condation)满足才会拼接SQL,实现动态SQL
7.IService的批量新增
- 需求:批量插入10万条用户数据,并作出对比:
- 普通for循环插入
- IService的批量插入
- 开启rewriteBatchedStatements=true参数
- 批处理方案:
- 普通for循环逐条插入速度极差,不推荐
- MP的批量新增,基于预编译的批处理,性能不错
- 配置jdbc参数,开启rewriteBatchedStatements,性能最好
三、扩展功能
1.代码生成器
-
代码生成
-
对于同一个实体来说以下代码比较固定,重复写又比较繁琐
-
代码生成器就诞生了
-
这里有MybatisPlus提供的代码生成器的官方文档- 代码生成器(点击OPEN)这里不做介绍
-
这里提供一款非官方的Mybatisplus代码生成的一个插件
-
使用步骤如下:
-
点击other
-
点击Cinfig Database
-
点击test connect看能否连接成功
-
点击Code Generator
-
选择表做相关配置然后点击save点击 code generator即可生成相应的代码
2.DB静态工具
- 这个工具是最新版的MybatisPlus才有的功能
- 静态工具(里面的方法都是静态的无法利用反射获取对象)
- 使用静态工具可以解决循环依赖相互注入的问题
- 这里不做重点讲解
3.逻辑删除
- 逻辑删除就是基于代码逻辑模拟删除效果,但并不会正真删除数据。思路如下:
- 在表中添加一个字段标记数据是否被删除
- 当删除数据时把标记置为1
- 当查询时只查询标记为0的数据
- 例如逻辑删除字段为deleted:
- 删除操作
- 查询操作
- 这时我做的测试,删除方法变成update语句,查询方法后面多了对逻辑删除字段deleted的判断
- MybatisPlus提供了逻辑删除功能,无需改变方法调用的方式,而是在底层帮我们自动修改CRUD的语句。我们要做的就是在application.yaml文件中配置逻辑删除的字段名称和值即可:
逻辑删除本身也有自己的问题,比如: - 会导致数据库表垃圾数据越来越多,影响查询效率
- SQL中全都需要对逻辑字段做判断,影响查询效率
因此,我不太推荐采用逻辑删除功能,如果数据不能删除,可以采用把数据迁移到其他表的办法来解决
4.枚举处理器
- User类中有一个用户状态字段:
- 使用第一步:创建新的类定义相应的枚举利用注解进行标记开发
- 使用第二步:在applicatin.yaml中配置全局枚举处理器
- 不过这里枚举返回给前端的数据时枚举,我们可以通过@JsonValue注解来决定返回哪个数据
5.JSON处理器
- 数据库表中user表中有一个json类型的字段:
- 处理前 (该字段在Java中用的String类型)
- 处理后
四、插件功能
- MybatisPlus提供的内置拦截器有下面这些
- 不过我们常用的也就时分页插件
1.分页插件基本用法
- 首先,要在配置类中注册MybatisPlus的核心插件,同时添加分页插件:
- 接着就可以使用分页的API了
- 测试使用分页插件
@Test
void testPageQuery() {
int pageNo = 1, pageSize = 2;
//1.准备分页条件
//1.1分页条件
Page<User> page = Page.of(pageNo, pageSize);
//1.2排序条件
page.addOrder(new OrderItem("balance", true));
page.addOrder(new OrderItem("id", true));
//2.分页查询
Page<User> p = userService.page(page);
//3.解析
long total = p.getTotal();
System.out.println("total = " + total);
long pages = p.getPages();
System.out.println("pages = " + pages);
List<User> users = p.getRecords();
users.forEach(System.out::println);
}
2.通用分页实体
- 封装统一的查询条件(要么直接使用要么继承)
@Data
@ApiModel(description = "分页查询实体")
public class PageQuery {
@ApiModelProperty("页码")
private Integer pageNo;
@ApiModelProperty("分页大小")
private Integer pageSize;
@ApiModelProperty("排序字段")
private String sortBy;
@ApiModelProperty("是否升序")
private Boolean isAsc;
}
- 封装统一的返回值
@Data
@NoArgsConstructor
@AllArgsConstructor
@ApiModel(description = "分页结果")
public class PageDTO<V> {
@ApiModelProperty("总条数")
private Long total;
@ApiModelProperty("总页数")
private Long pages;
@ApiModelProperty("集合")
private List<V> list;
}
3.通用分页实体与MP转换
- 下面这里看不懂的可以不看,就是封装了一些业务简化开发
- 在PageQuery中定义方法,将PageQuery的对象转为MyBatisPlus中的Page对象
@Data
@ApiModel(description = "分页查询实体")
public class PageQuery {
@ApiModelProperty("页码")
private Integer pageNo;
@ApiModelProperty("分页大小")
private Integer pageSize;
@ApiModelProperty("排序字段")
private String sortBy;
@ApiModelProperty("是否升序")
private Boolean isAsc;
//自动的将PageQuery对象转为MyBatisPlus中的Page对象
public <T> Page<T> toMpPage(OrderItem ... orders){
// 1.分页条件
Page<T> p = Page.of(pageNo, pageSize);
// 2.排序条件
// 2.1.先看前端有没有传排序字段
if (sortBy != null) {
p.addOrder(new OrderItem(sortBy, isAsc));
return p;
}
// 2.2.再看有没有手动指定排序字段
if(orders != null){
p.addOrder(orders);
}
return p;
}
public <T> Page<T> toMpPage(String defaultSortBy, boolean isAsc){
return this.toMpPage(new OrderItem(defaultSortBy, isAsc));
}
public <T> Page<T> toMpPageDefaultSortByCreateTimeDesc() {
return toMpPage("create_time", false);
}
public <T> Page<T> toMpPageDefaultSortByUpdateTimeDesc() {
return toMpPage("update_time", false);
}
}
- 在PageDTO中定义方法,将MyBatisPlus中的Page结果转为PageDTO结果
@Data
@NoArgsConstructor
@AllArgsConstructor
@ApiModel(description = "分页结果")
public class PageDTO<V> {
@ApiModelProperty("总条数")
private Long total;
@ApiModelProperty("总页数")
private Long pages;
@ApiModelProperty("集合")
private List<V> list;
/**
* 返回空分页结果
* @param p MybatisPlus的分页结果
* @param <V> 目标VO类型
* @param <P> 原始PO类型
* @return VO的分页对象
*/
public static <V, P> PageDTO<V> empty(Page<P> p){
return new PageDTO<>(p.getTotal(), p.getPages(), Collections.emptyList());
}
/**
* 将MybatisPlus分页结果转为 VO分页结果
* @param p MybatisPlus的分页结果
* @param voClass 目标VO类型的字节码
* @param <V> 目标VO类型
* @param <P> 原始PO类型
* @return VO的分页对象
*/
public static <V, P> PageDTO<V> of(Page<P> p, Class<V> voClass) {
// 1.非空校验
List<P> records = p.getRecords();
if (records == null || records.size() <= 0) {
// 无数据,返回空结果
return empty(p);
}
// 2.数据转换
List<V> vos = BeanUtil.copyToList(records, voClass);
// 3.封装返回
return new PageDTO<>(p.getTotal(), p.getPages(), vos);
}
/**
* 将MybatisPlus分页结果转为 VO分页结果,允许用户自定义PO到VO的转换方式
* @param p MybatisPlus的分页结果
* @param convertor PO到VO的转换函数
* @param <V> 目标VO类型
* @param <P> 原始PO类型
* @return VO的分页对象
*/
public static <V, P> PageDTO<V> of(Page<P> p, Function<P, V> convertor) {
// 1.非空校验
List<P> records = p.getRecords();
if (records == null || records.size() <= 0) {
// 无数据,返回空结果
return empty(p);
}
// 2.数据转换
List<V> vos = records.stream().map(convertor).collect(Collectors.toList());
// 3.封装返回
return new PageDTO<>(p.getTotal(), p.getPages(), vos);
}
}
五、总结
- MyBatis-Plus是MyBatis的增强工具,主要用于简化和增强MyBatis的操作。为了掌握MyBatis-Plus,我们需要掌握以下重点:
1.MyBatis-Plus的基本使用:了解MyBatis-Plus的配置和使用方法,包括实体类、映射文件、Dao接口以及MyBatis-Plus提供的常用方法和工具类。
2.MyBatis-Plus的高级查询:掌握MyBatis-Plus提供的高级查询功能,包括条件构造器、Wrapper、QueryWrapper、LambdaWrapper等,并且了解如何使用这些功能进行复杂查询。
3.分页查询:掌握MyBatis-Plus的分页查询功能,了解分页查询的原理和实现方法,并且了解MyBatis-Plus的分页查询API和使用方法。
4.代码自动生成:掌握MyBatis-Plus提供的代码自动生成功能,了解如何使用MyBatis-Plus自动生成实体类、Mapper接口和XML映射文件。
5.性能优化:了解MyBatis-Plus的性能优化技巧,包括缓存、批量操作、SQL优化等,以便在实际应用中提高系统的性能。
- 综上所述,掌握MyBatis-Plus需要我们掌握其基本使用、高级查询、分页查询、代码自动生成和性能优化等重点。
有需要详细了解MyBatisPlus的小伙伴,可以前往MyBatisPlus的官方文档(点击OPEN)