1、快速入门
1.1 入门案例
需求:基于课前资料提供的项目,实现下列功能:
- 新增用户功能
- 根据id查询用户
- 根据id批量查询用户
- 根据id更新用户
- 根据id删除用户
实现:
1.引入MybatisPlus的起步依赖
MyBatisPlus官方提供了starter,其中集成了Mybatis和MybatisPlus的所有功能,并且实现了自动装配效果。因此我们可以用MybatisPlus的starter代替Mybatis的starter:
2.定义Mapper
自定义的Mapper继承MybatisPlus提供的BaseMapper接口:
3.application.yaml
spring:
datasource:
url: jdbc:mysql://127.0.0.1:3306/mp?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: 123456
logging:
level:
com.itheima: debug
pattern:
dateformat: HH:mm:ss
mybatis:
mapper-locations: classpath*:mapper/*.xml
手写实现:
User类
package com.itheima.mp.domain.po;
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class User {
/**
* 用户id
*/
private Long id;
/**
* 用户名
*/
private String username;
/**
* 密码
*/
private String password;
/**
* 注册手机号
*/
private String phone;
/**
* 详细信息
*/
private String info;
/**
* 使用状态(1正常 2冻结)
*/
private Integer status;
/**
* 账户余额
*/
private Integer balance;
/**
* 创建时间
*/
private LocalDateTime createTime;
/**
* 更新时间
*/
private LocalDateTime updateTime;
}
UserMapper接口
package com.itheima.mp.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.itheima.mp.domain.po.User;
import org.apache.ibatis.annotations.Param;
import java.util.List;
public interface UserMapper extends BaseMapper<User> {
void saveUser(User user);
void deleteUser(Long id);
void updateUser(User user);
User queryUserById(@Param("id") Long id);
List<User> queryUserByIds(@Param("ids") List<Long> ids);
}
UserMapper.xml
<?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.itheima.mp.mapper.UserMapper">
<insert id="saveUser" parameterType="com.itheima.mp.domain.po.User">
INSERT INTO `user` (`id`, `username`, `password`, `phone`, `info`, `balance`)
VALUES
(#{id}, #{username}, #{password}, #{phone}, #{info}, #{balance});
</insert>
<update id="updateUser" parameterType="com.itheima.mp.domain.po.User">
UPDATE `user`
<set>
<if test="username != null">
`username`=#{username}
</if>
<if test="password != null">
`password`=#{password}
</if>
<if test="phone != null">
`phone`=#{phone}
</if>
<if test="info != null">
`info`=#{info}
</if>
<if test="status != null">
`status`=#{status}
</if>
<if test="balance != null">
`balance`=#{balance}
</if>
</set>
WHERE `id`=#{id};
</update>
<delete id="deleteUser" parameterType="com.itheima.mp.domain.po.User">
DELETE FROM user WHERE id = #{id}
</delete>
<select id="queryUserById" resultType="com.itheima.mp.domain.po.User">
SELECT *
FROM user
WHERE id = #{id}
</select>
<select id="queryUserByIds" resultType="com.itheima.mp.domain.po.User">
SELECT *
FROM user
<if test="ids != null">
WHERE id IN
<foreach collection="ids" open="(" close=")" item="id" separator=",">
#{id}
</foreach>
</if>
LIMIT 10
</select>
</mapper>
UserMapperTest测试类
package com.itheima.mp.mapper;
import com.itheima.mp.domain.po.User;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.time.LocalDateTime;
import java.util.List;
@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);
}
@Test
void testSelectById() {
User user = userMapper.queryUserById(5L);
System.out.println("user = " + user);
}
@Test
void testQueryByIds() {
List<User> users = userMapper.queryUserByIds(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);
}
@Test
void testDeleteUser() {
userMapper.deleteUser(5L);
}
}
MybatisPlus实现:
package com.itheima.mp.mapper;
import com.itheima.mp.domain.po.User;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.time.LocalDateTime;
import java.util.List;
@SpringBootTest
class UserMapperPlusTest {
@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.insert(user);
}
@Test
void testSelectById() {
User user = userMapper.selectById(5L);
System.out.println("user = " + user);
}
@Test
void testQueryByIds() {
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.updateById(user);
}
@Test
void testDeleteUser() {
userMapper.deleteById(5L);
}
}
1.2 常见注解
MyBatisPlus通过扫描实体类,并基于反射获取实体类信息作为数据库表信息。
- 类名驼峰转下划线作为表名
- 名为id的字段作为主键
- 变量名驼峰转下划线作为表的字段名
MybatisPlus中比较常用的几个注解如下:
- @TableName:用来指定表名
- @TableId:用来指定表中的主键字段信息
- @TableField:用来指定表中的普通字段信息
package com.itheima.mp.domain.po;
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.Data;
import java.time.LocalDateTime;
@Data
@TableName("tb_user")
public class User {
/**
* 用户id
*/
@TableId(type = IdType.AUTO)
private Long id;
/**
* 用户名
*/
@TableField("username")
private String username;
/**
* 密码
*/
@TableField(exist = false)
private String password;
/**
* 注册手机号
*/
private String phone;
/**
* 详细信息
*/
private String info;
/**
* 使用状态(1正常 2冻结)
*/
private Integer status;
/**
* 账户余额
*/
private Integer balance;
/**
* 创建时间
*/
private LocalDateTime createTime;
/**
* 更新时间
*/
private LocalDateTime updateTime;
}
1.3 常见配置
MyBatisPlus的配置项继承了MyBatis原生配置和一些自己特有的配置。例如:
具体可参考官方文档:使用配置 | MyBatis-Plus (baomidou.com)
2、核心功能
2.1 条件构造器
MyBatisPlus支持各种复杂的where条件,可以满足日常开发的所有需求。
基于QueryWrapper的查询
需求:查询出名字中带o的,存款大于等于1000元的人的id、username、info、balance字段
@Test
void testQueryWrapper() {
QueryWrapper<User> wrapper = new QueryWrapper<User>()
.select("id", "username", "info", "balance")
.like("username", "o")
.ge("balance", 1000);
List<User> users = userMapper.selectList(wrapper);
users.forEach(System.out::println);
}
更新用户名为jack的用户的余额为2000
@Test
void testUpdateWrapper() {
User user = new User();
QueryWrapper<User> wrapper = new QueryWrapper<User>()
.eq("username", "jack");
user.setBalance(2000);
userMapper.update(user,wrapper);
}
基于UpdateWrapper的更新
需求:更新id为1,2,4的用户的余额,扣200
@Test
void testUpdateUpdateWrapper() {
List<Long> ids = List.of(1l, 2l, 3l);
UpdateWrapper<User> updateWrapper = new UpdateWrapper<User>()
.setSql("balance = balance - 200")
.in("id", ids);
userMapper.update(null,updateWrapper);
}
基于LambdaQueryWrappe的查询
查询出名字中带o的,存款大于等于1000元的人的id、username、info、balance字段
@Test
void testLambdaQueryWrapper() {
LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<User>()
.select(User::getId, User::getUsername, User::getInfo, User::getBalance)
.like(User::getUsername, "o")
.ge(User::getBalance, 1000);
List<User> users = userMapper.selectList(lambdaQueryWrapper);
users.forEach(System.out::println);
}
条件构造器的用法:
- QueryWrapper和LambdaQueryWrapper通常用来构建select、delete、update的where条件部分
- UpdateWrapper和LambdaUpdateWrapper通常只有在set语句比较特殊才使用尽量使用
- LambdaQueryWrapper和LambdaUpdateWrapper,避免硬编码
2.2 自定义SQL
我们可以利用MyBatisPlus的Wrapper来构建复杂的Where条件,然后自己定义SQL语句中剩下的部分。
需求:将id在指定范围的用户(例如1、2、4 )的余额扣减指定值
MyBatisPlus的sql自己拼接违背了企业的开发规范,更新查询都会碰到,不用的话就有点麻烦,所以把条件用mp构建,那些表的字段啥的就用xml。
我们可以利用MyBatisPlus的Wrapper来构建复杂的Where条件,然后自己定义SQL语句中剩下的部分。
基于Wrapper构建where条件
在mapper方法参数中用Param注解声明wrapper变量名称,必须是ew
自定义SQL,并使用Wrapper条件
代码实现
UserMapper接口
package com.itheima.mp.mapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.toolkit.Constants;
import com.itheima.mp.domain.po.User;
import org.apache.ibatis.annotations.Param;
import org.yaml.snakeyaml.scanner.Constant;
import java.util.List;
public interface UserMapper extends BaseMapper<User> {
//@Param("ew") 或者
void updateBalanceByIds(@Param(Constants.WRAPPER) UpdateWrapper<User> updateWrapper, @Param("amount") int amount);
}
UserMapper.xml文件
<?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.itheima.mp.mapper.UserMapper">
<update id="updateBalanceByIds">
UPDATE `user` set balance = balance - #{amount} ${ew.customSqlSegment}
</update>
</mapper>
testCustomSqlUpdate测试方法
@Test
void testCustomSqlUpdate() {
List<Long> ids = List.of(1l, 2l, 4l);
int amount = 200;
UpdateWrapper<User> updateWrapper = new UpdateWrapper<User>()
.in("id", ids);
userMapper.updateBalanceByIds(updateWrapper,amount);
}
案例:基于Wrapper的多表关联查询
需求:查询出所有收货地址在北京的并且用户id在指定范围的用户(例如1、2、4 )的用户
2.3 Service接口
代码实现
IUserService接口
package com.itheima.mp.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.itheima.mp.domain.po.User;
public interface IUserService extends IService<User> {
}
IUserServiceImpl实现类
package com.itheima.mp.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.itheima.mp.domain.po.User;
import com.itheima.mp.mapper.UserMapper;
import com.itheima.mp.service.IUserService;
import org.springframework.stereotype.Service;
@Service
public class IUserServiceImpl extends ServiceImpl<UserMapper,User> implements IUserService {
}
IUserServiceTest测试类
package com.itheima.mp.service;
import com.itheima.mp.domain.po.User;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.time.LocalDateTime;
import java.util.List;
@SpringBootTest
class IUserServiceTest {
@Autowired
private IUserService iUserService;
@Test
void testInsert() {
User user = new User();
// user.setId(6L);
user.setUsername("lilei");
user.setPassword("123");
user.setPhone("18688990011");
user.setBalance(200);
user.setInfo("{\"age\": 24, \"intro\": \"英文老师\", \"gender\": \"female\"}");
user.setCreateTime(LocalDateTime.now());
user.setUpdateTime(LocalDateTime.now());
iUserService.save(user);
}
@Test
void testQuery() {
List<Long> longs = List.of(1l, 2l, 4l);
List<User> users = iUserService.listByIds(longs);
users.forEach(System.out::println);
}
}
需求:基于Restful风格实现下面的接口:
导入依赖
<!-- knife4j openapi3 接口文档 -->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-openapi2-spring-boot-starter</artifactId>
<version>4.1.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.7.17</version>
</dependency>
<dependency>
<groupId>io.swagger</groupId>
<artifactId>swagger-annotations</artifactId>
<version>1.6.2</version>
</dependency>
application.yaml文档添加配置
knife4j:
enable: true
openapi:
title: 用户管理接口文档
description: "用户管理接口文档"
email: zhanghh@hh.com
concat: https://www.zhanghh.cn
version: v1.0.0
group:
default:
group-name: default
api-rule: package
api-rule-rtesources:
- com.itheima.mp.controller
UserFormDTO实体类
package com.itheima.mp.domain.dto;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
@Data
@ApiModel(description = "用户表单实体")
public class UserFormDTO {
@ApiModelProperty("id")
private Long id;
@ApiModelProperty("用户名")
private String username;
@ApiModelProperty("密码")
private String password;
@ApiModelProperty("注册手机号")
private String phone;
@ApiModelProperty("详细信息,JSON风格")
private String info;
@ApiModelProperty("账户余额")
private Integer balance;
}
UserVO实体类
package com.itheima.mp.domain.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
@Data
@ApiModel(description = "用户VO实体")
public class UserVO {
@ApiModelProperty("用户id")
private Long id;
@ApiModelProperty("用户名")
private String username;
@ApiModelProperty("详细信息")
private String info;
@ApiModelProperty("使用状态(1正常 2冻结)")
private Integer status;
@ApiModelProperty("账户余额")
private Integer balance;
}
User实体类
package com.itheima.mp.domain.po;
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.Data;
import java.time.LocalDateTime;
@Data
@TableName("user")
public class User {
/**
* 用户id
*/
@TableId(type = IdType.AUTO)
private Long id;
/**
* 用户名
*/
@TableField("username")
private String username;
/**
* 密码
*/
@TableField(exist = true)
private String password;
/**
* 注册手机号
*/
private String phone;
/**
* 详细信息
*/
private String info;
/**
* 使用状态(1正常 2冻结)
*/
private Integer status;
/**
* 账户余额
*/
private Integer balance;
/**
* 创建时间
*/
private LocalDateTime createTime;
/**
* 更新时间
*/
private LocalDateTime updateTime;
}
UserMapper接口
package com.itheima.mp.mapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.toolkit.Constants;
import com.itheima.mp.domain.po.User;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Update;
import org.yaml.snakeyaml.scanner.Constant;
import java.util.List;
public interface UserMapper extends BaseMapper<User> {
@Update("update user set balance = balance - #{money} where id = #{id}")
void deductBanlance(@Param("id") Long id,@Param("money") Integer money);
}
IUserService接口
public interface IUserService extends IService<User> {
void deductBalance(Long id, Integer money);
}
IUserServiceImpl实现类
package com.itheima.mp.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.itheima.mp.domain.po.User;
import com.itheima.mp.mapper.UserMapper;
import com.itheima.mp.service.IUserService;
import org.springframework.stereotype.Service;
@Service
public class IUserServiceImpl extends ServiceImpl<UserMapper,User> implements IUserService {
@Override
public void deductBalance(Long id, Integer money) {
User user = getById(id);
if (user == null || user.getStatus() == 2){
throw new RuntimeException("用户状态异常");
}
if (user.getBalance() < money){
throw new RuntimeException("用户余额不足");
}
baseMapper.deductBanlance(id,money);
}
}
UserController类
package com.itheima.mp.controller;
import cn.hutool.core.bean.BeanUtil;
import com.itheima.mp.domain.dto.UserFormDTO;
import com.itheima.mp.domain.po.User;
import com.itheima.mp.domain.vo.UserVO;
import com.itheima.mp.service.IUserService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@Api(tags = "用户管理接口")
@RestController
@RequestMapping("/uesrs")
//@RequiredArgsConstructor
public class UserController {
@Autowired
private IUserService iUserServic;
@ApiOperation("新增用户接口")
@PostMapping
public void saveUser(@RequestBody UserFormDTO userFormDTO){
User user = BeanUtil.copyProperties(userFormDTO, User.class);
iUserServic.save(user);
}
@ApiOperation("删除用户接口")
@DeleteMapping("{id}")
public void deleteUser(@ApiParam("用户id") @PathVariable("id") Long id){
iUserServic.removeById(id);
}
@ApiOperation("查询用户接口")
@GetMapping("{id}")
public UserVO queryUserById(@ApiParam("用户id") @PathVariable("id") Long id){
User user = iUserServic.getById(id);
return BeanUtil.copyProperties(user,UserVO.class);
}
@ApiOperation("根据id批量查询用户接口")
@GetMapping
public List<UserVO> queryUserByIds(@ApiParam("用户id") @RequestParam("id") List<Long> id){
List<User> users = iUserServic.listByIds(id);
return BeanUtil.copyToList(users,UserVO.class);
}
@ApiOperation("根据id扣减余额")
@PutMapping("/{id}/deduction/{money}")
public void deductBalance(
@ApiParam("用户id") @PathVariable("id") Long id,
@ApiParam("扣减的金额") @PathVariable("money") Integer money){
iUserServic.deductBalance(id,money);
// return BeanUtil.copyToList(users,UserVO.class);
}
}
然后启动好项目之后,在浏览器输入Restful地址:http://localhost:8080/doc.html
IService的Lambda查询
需求:实现一个根据复杂条件查询用户的接口,查询条件如下:
- name:用户名关键字,可以为空
- status:用户状态,可以为空
- minBalance:最小余额,可以为空
- maxBalance:最大余额,可以为空
如果是mybatis的话,就是下面这种形式
代码实现
UserQuery类
package com.itheima.mp.domain.query;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
@Data
@ApiModel(description = "用户查询条件实体")
public class UserQuery {
@ApiModelProperty("用户名关键字")
private String name;
@ApiModelProperty("用户状态:1-正常,2-冻结")
private Integer status;
@ApiModelProperty("余额最小值")
private Integer minBalance;
@ApiModelProperty("余额最大值")
private Integer maxBalance;
}
IUserService接口类
List<User> queryUsers(String name,Integer status,Integer minBalance,Integer maxBalance);
IUserServiceImpl实现类
@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();
}
UserController类
@ApiOperation("根据复杂条件查询用户接口")
@PostMapping("/list")
public List<UserVO> queryUsers(@RequestBody UserQuery query){
// @ApiImplicitParam(name = "query",paramType = "query")
// @GetMapping("/list")
// public List<UserVO> queryUsers(UserQuery query) {
System.out.println("111111111===============" + query);
List<User> users = iUserServic.queryUsers(
query.getName(), query.getStatus(), query.getMinBalance(), query.getMaxBalance());
return BeanUtil.copyToList(users, UserVO.class);
}
IService的Lambda更新
需求:改造根据id修改用户余额的接口,要求如下:
- 完成对用户状态校验
- 完成对用户余额校验
- 如果扣减后余额为0,则将用户status修改为冻结状态(2)
@Override
public void deductBalance(Long id, Integer money) {
User user = getById(id);
if (user == null || user.getStatus() == 2){
throw new RuntimeException("用户状态异常");
}
if (user.getBalance() < money){
throw new RuntimeException("用户余额不足");
}
int remain= user.getBalance()-money;
lambdaUpdate().set(User::getBalance,remain)
.set(remain==0,User::getStatus,2)
.eq(User::getId,id)
.eq(User::getBalance,user.getBalance()) //乐观锁
.update();
// baseMapper.deductBanlance(id,money);
}
IService批量新增
需求:批量插入10万条用户数据,并作出对比:
- 普通for循环插入
- IService的批量插入
- 开启rewriteBatchedStatements=true参数
造user数据的方法testInsert
private User testInsert(int i) {
User user = new User();
// user.setId(6L);
user.setUsername("lilei_"+ i);
user.setPassword("123");
user.setPhone("18688990011");
user.setBalance(200);
user.setInfo("{\"age\": 24, \"intro\": \"英文老师\", \"gender\": \"female\"}");
user.setCreateTime(LocalDateTime.now());
user.setUpdateTime(LocalDateTime.now());
return user;
}
(1)方案一
普通for循环插入,一条一条插入数据,每次都io插入,100000条sql
@Test
void testInsertTenData() {
long start = System.currentTimeMillis();
for (int i = 0; i < 100000; i++) {
iUserService.save(testInsert(i));
}
long end = System.currentTimeMillis();
System.out.println("耗时:"+ (end-start));
}
(2)方案二
批处理,每次io插入1000条sql,总共100000条sql
@Test
void testInsertpichuliTenData() {
long start = System.currentTimeMillis();
List<User> userList = new ArrayList<>(1000);
for (int i = 0; i < 100000; i++) {
userList.add(testInsert(i));
if (i % 1000 == 0 ){
iUserService.saveBatch(userList);
userList.clear();
}
}
long end = System.currentTimeMillis();
System.out.println("耗时:"+ (end-start));
}
(3)方案三
批处理,一条sql一次性插入1000条数据,总共100io,100条sql
得开批处理rewriteBatchedStatements=true,拼接在URL后面
@Test
void testInsertpichuliTenData() {
long start = System.currentTimeMillis();
List<User> userList = new ArrayList<>(1000);
for (int i = 0; i < 100000; i++) {
userList.add(testInsert(i));
if (i % 1000 == 0 ){
iUserService.saveBatch(userList);
userList.clear();
}
}
long end = System.currentTimeMillis();
System.out.println("耗时:"+ (end-start));
}
3、扩展功能
3.1 逻辑删除
逻辑删除就是基于代码逻辑模拟删除效果,但并不会真正删除数据。思路如下:
- 在表中添加一个字段标记数据是否被删除
- 当删除数据时把标记置为1
- 查询时只查询标记为0的数据
例如逻辑删除字段为deleted:
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)
id-type: auto
表中有逻辑删除字段
Address实体类中也有该字段
private Boolean deleted;
IAddressServiceTest测试类
package com.itheima.mp.service;
import com.itheima.mp.domain.po.Address;
import com.itheima.mp.domain.po.User;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
@SpringBootTest
class IAddressServiceTest {
@Autowired
private IAddressService addressService;
@Test
void testDelete() {
boolean b = addressService.removeById(59L);
Address byId = addressService.getById(59L);
System.out.println(byId);
}
}
3.2 枚举处理器
User类中有一个用户状态字段:
mybatis实现枚举类型转化的类有:
User类中有一个用户状态字段:
像这种字段我们一般会定义一个枚举,做业务判断的时候就可以直接基于枚举做比较。但是我们数据库采用的是int
类型,对应的PO也是Integer
。因此业务操作时必须手动把枚举
与Integer
转换,非常麻烦。
因此,MybatisPlus提供了一个处理枚举的类型转换器,可以帮我们把枚举类型与数据库类型自动转换。
3.3.1.定义枚举
我们定义一个用户状态的枚举:
代码如下:
package com.itheima.mp.enums;
import com.baomidou.mybatisplus.annotation.EnumValue;
import lombok.Getter;
@Getter
public enum UserStatus {
NORMAL(1, "正常"),
FREEZE(2, "冻结")
;
private final int value;
private final String desc;
UserStatus(int value, String desc) {
this.value = value;
this.desc = desc;
}
}
然后把User
类中的status
字段改为UserStatus
类型:
要让MybatisPlus
处理枚举与数据库类型自动转换,我们必须告诉MybatisPlus
,枚举中的哪个字段的值作为数据库值。 MybatisPlus
提供了@EnumValue
注解来标记枚举属性:
3.3.2.配置枚举处理器
在application.yaml文件中添加配置:
mybatis-plus:
configuration:
default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler
3.3.3.测试
@Test
void testService() {
List<User> list = userService.list();
list.forEach(System.out::println);
}
最终,查询出的User
类的status
字段会是枚举类型:
同时,为了使页面查询结果也是枚举格式,我们需要修改UserVO中的status属性:
并且,在UserStatus枚举中通过@JsonValue
注解标记JSON序列化时展示的字段:
最后,在页面查询,结果如下:
3.3 json处理器
数据库的user表中有一个info
字段,是JSON类型:
格式像这样:
{"age": 20, "intro": "佛系青年", "gender": "male"}
而目前User
实体类中却是String
类型:
这样一来,我们要读取info中的属性时就非常不方便。如果要方便获取,info的类型最好是一个Map
或者实体类。
而一旦我们把info
改为对象
类型,就需要在写入数据库时手动转为String
,再读取数据库时,手动转换为对象
,这会非常麻烦。
因此MybatisPlus提供了很多特殊类型字段的类型处理器,解决特殊字段类型与数据库类型转换的问题。例如处理JSON就可以使用JacksonTypeHandler
处理器。
接下来,我们就来看看这个处理器该如何使用。
3.3.1.定义实体
首先,我们定义一个单独实体类来与info字段的属性匹配:
代码如下:
package com.itheima.mp.domain.po;
import lombok.Data;
@Data
public class UserInfo {
private Integer age;
private String intro;
private String gender;
}
3.3.2.使用类型处理器
接下来,将User类的info字段修改为UserInfo类型,并声明类型处理器:
在类上开启自动映射
测试可以发现,所有数据都正确封装到UserInfo当中了:
同时,为了让页面返回的结果也以对象格式返回,我们要修改UserVO中的info字段:
此时,在页面查询结果如下:
4、插件功能
MybatisPlus提供了很多的插件功能,进一步拓展其功能。目前已有的插件有:
PaginationInnerInterceptor
:自动分页TenantLineInnerInterceptor
:多租户DynamicTableNameInnerInterceptor
:动态表名OptimisticLockerInnerInterceptor
:乐观锁- MybatisPlus提供了很多的插件功能,进一步拓展其功能。目前已有的插件有:
PaginationInnerInterceptor
:自动分页TenantLineInnerInterceptor
:多租户DynamicTableNameInnerInterceptor
:动态表名OptimisticLockerInnerInterceptor
:乐观锁IllegalSQLInnerInterceptor
:sql 性能规范BlockAttackInnerInterceptor
:防止全表更新与删除
这里我们以分页插件为里来学习插件的用法。
4.1.分页插件
在未引入分页插件的情况下,MybatisPlus
是不支持分页功能的,IService
和BaseMapper
中的分页方法都无法正常起效。 所以,我们必须配置分页插件。
4.1.1.配置分页插件
在项目中新建一个配置类:
其代码如下:
package com.itheima.mp.config;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MybatisConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
// 初始化核心插件
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 添加分页插件
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
4.1.2.分页API
编写一个分页查询的测试:
@Test
void testPageQuery() {
// 1.分页查询,new Page()的两个参数分别是:页码、每页大小
Page<User> p = userService.page(new Page<>(2, 2));
// 2.总条数
System.out.println("total = " + p.getTotal());
// 3.总页数
System.out.println("pages = " + p.getPages());
// 4.数据
List<User> records = p.getRecords();
records.forEach(System.out::println);
}
运行的SQL如下:
这里用到了分页参数,Page,即可以支持分页参数,也可以支持排序参数。常见的API如下:
int pageNo = 1, pageSize = 5;
// 分页参数
Page<User> page = Page.of(pageNo, pageSize);
// 排序参数, 通过OrderItem来指定
page.addOrder(new OrderItem("balance", false));
userService.page(page);
4.2.通用分页实体
现在要实现一个用户分页查询的接口,接口规范如下:
参数 | 说明 |
请求方式 | GET |
请求路径 | /users/page |
请求参数 | |
返回值 | |
特殊说明 |
|
这里需要定义3个实体:
UserQuery
:分页查询条件的实体,包含分页、排序参数、过滤条件PageDTO
:分页结果实体,包含总条数、总页数、当前页数据UserVO
:用户页面视图实体
4.2.1.实体
由于UserQuery之前已经定义过了,并且其中已经包含了过滤条件,具体代码如下:
package com.itheima.mp.domain.query;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
@Data
@ApiModel(description = "用户查询条件实体")
public class UserQuery {
@ApiModelProperty("用户名关键字")
private String name;
@ApiModelProperty("用户状态:1-正常,2-冻结")
private Integer status;
@ApiModelProperty("余额最小值")
private Integer minBalance;
@ApiModelProperty("余额最大值")
private Integer maxBalance;
}
其中缺少的仅仅是分页条件,而分页条件不仅仅用户分页查询需要,以后其它业务也都有分页查询的需求。因此建议将分页查询条件单独定义为一个PageQuery
实体:
PageQuery
是前端提交的查询参数,一般包含四个属性:
pageNo
:页码pageSize
:每页数据条数sortBy
:排序字段isAsc
:是否升序
@Data
@ApiModel(description = "分页查询实体")
public class PageQuery {
@ApiModelProperty("页码")
private Long pageNo;
@ApiModelProperty("页码")
private Long pageSize;
@ApiModelProperty("排序字段")
private String sortBy;
@ApiModelProperty("是否升序")
private Boolean isAsc;
}
然后,让我们的UserQuery继承这个实体:
package com.itheima.mp.domain.query;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
@EqualsAndHashCode(callSuper = true)
@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;
}
返回值的用户实体沿用之前定一个UserVO
实体:
最后,则是分页实体PageDTO:
代码如下:
package com.itheima.mp.domain.dto;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.util.List;
@Data
@ApiModel(description = "分页结果")
public class PageDTO<T> {
@ApiModelProperty("总条数")
private Long total;
@ApiModelProperty("总页数")
private Long pages;
@ApiModelProperty("集合")
private List<T> list;
}
4.2.2.开发接口
我们在UserController
中定义分页查询用户的接口:
package com.itheima.mp.controller;
import com.itheima.mp.domain.dto.PageDTO;
import com.itheima.mp.domain.query.PageQuery;
import com.itheima.mp.domain.vo.UserVO;
import com.itheima.mp.service.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("users")
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
@GetMapping("/page")
public PageDTO<UserVO> queryUsersPage(UserQuery query){
return userService.queryUsersPage(query);
}
// 。。。 略
}
然后在IUserService
中创建queryUsersPage
方法:
PageDTO<UserVO> queryUsersPage(PageQuery query);
接下来,在UserServiceImpl中实现该方法:
@Override
public PageDTO<UserVO> queryUsersPage(PageQuery query) {
// 1.构建条件
// 1.1.分页条件
Page<User> page = Page.of(query.getPageNo(), query.getPageSize());
// 1.2.排序条件
if (query.getSortBy() != null) {
page.addOrder(new OrderItem(query.getSortBy(), query.getIsAsc()));
}else{
// 默认按照更新时间排序
page.addOrder(new OrderItem("update_time", false));
}
// 2.查询
page(page);
// 3.数据非空校验
List<User> records = page.getRecords();
if (records == null || records.size() <= 0) {
// 无数据,返回空结果
return new PageDTO<>(page.getTotal(), page.getPages(), Collections.emptyList());
}
// 4.有数据,转换
List<UserVO> list = BeanUtil.copyToList(records, UserVO.class);
// 5.封装返回
return new PageDTO<UserVO>(page.getTotal(), page.getPages(), list);
}
启动项目,在页面查看:
4.2.3.改造PageQuery实体
在刚才的代码中,从PageQuery
到MybatisPlus
的Page
之间转换的过程还是比较麻烦的。
我们完全可以在PageQuery
这个实体中定义一个工具方法,简化开发。 像这样:
package com.itheima.mp.domain.query;
import com.baomidou.mybatisplus.core.metadata.OrderItem;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.Data;
@Data
public class PageQuery {
private Integer pageNo;
private Integer pageSize;
private String sortBy;
private Boolean isAsc;
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);
}
}
这样我们在开发也时就可以省去对从PageQuery
到Page
的的转换:
// 1.构建条件
Page<User> page = query.toMpPageDefaultSortByCreateTimeDesc();
4.2.4.改造PageDTO实体
在查询出分页结果后,数据的非空校验,数据的vo转换都是模板代码,编写起来很麻烦。
我们完全可以将其封装到PageDTO的工具方法中,简化整个过程:
package com.itheima.mp.domain.dto;
import cn.hutool.core.bean.BeanUtil;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Collections;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class PageDTO<V> {
private Long total;
private Long pages;
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);
}
}
最终,业务层的代码可以简化为:
@Override
public PageDTO<UserVO> queryUserByPage(PageQuery query) {
// 1.构建条件
Page<User> page = query.toMpPageDefaultSortByCreateTimeDesc();
// 2.查询
page(page);
// 3.封装返回
return PageDTO.of(page, UserVO.class);
}
如果是希望自定义PO到VO的转换过程,可以这样做:
@Override
public PageDTO<UserVO> queryUserByPage(PageQuery query) {
// 1.构建条件
Page<User> page = query.toMpPageDefaultSortByCreateTimeDesc();
// 2.查询
page(page);
// 3.封装返回
return PageDTO.of(page, user -> {
// 拷贝属性到VO
UserVO vo = BeanUtil.copyProperties(user, UserVO.class);
// 用户名脱敏
String username = vo.getUsername();
vo.setUsername(username.substring(0, username.length() - 2) + "**");
return vo;
});
}
最终查询的结果如下: