1.快速入门
1)MybatisPlus 的基本步骤:
①. 引入 MybatisPlus 依赖,代替 Mybatis 依赖
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.1</version>
</dependency>
②. 定义 Mapper 接口并继承 BaseMapper,注意要加泛型<>
public interface UserMapper extends BaseMapper<User> {}
2)常用注解
MyBatisPlus 通过扫描实体类,并基于反射获取实体类信息作为数据库表信息。
①.命名方式如下:
-
类名驼峰转下划线作为表名
-
名为 id 的字段作为主键
-
变量名驼峰转下划线作为表的字段名
②. MybatisPlus 中比较常用的几个注解如下:
- @TableName :用来指定表名
- @TableId :用来指定表中的主键字段信息
- TableField :用来指定表中的普通字段信息
注意:
i. 主键字段IdType的枚举方式:
- AUTO:数据库自增长
- INPUT:通过set方法自行输入
- ASSIGN_ID(默认):分配 ID,接口IdentifierGenerator的方法nextId来生成id,默认实现类为DefaultIdentifierGenerator雪花算法
ii. 使用@TableField的常见场景:
- 成员变量名与数据库字段名不一致
- 成员变量名以is开头,且是布尔值(否则,如字段is_married,通过反射会变为married)
- 成员变量名与数据库关键字冲突(加``)
- 成员变量不是数据库字段
3)常见配置
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)
2.核心功能
1)条件构造器
条件构造器的用法:
-
QueryWrapper和LambdaQueryWrapper通常用来构建select、delete、update的where条件部分
-
UpdateWrapper和LambdaUpdateWrapper通常只有在set语句比较特殊才使用,不然依旧使用QueryWrapper和LambdaQueryWrapper
-
尽量使用LambdaQueryWrapper和LambdaUpdateWrapper,避免硬编码
//查询出名字中带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 testUpdateQueryWrapper(){
//更新的数据
User user = new User();
user.setBalance(2000);
//构建查询条件
QueryWrapper<User> wrapper = new QueryWrapper<User>()
.eq("username","Jack");
//执行更新
userMapper.update(user,wrapper);
}
特殊的set语句
//更新id为1,2,4的用户的余额,扣200
@Test
void testUpdateWrapper(){
List<Long> ids = List.of(1L,2L,3L);
//构建查询条件
UpdateWrapper<User> wrapper = new UpdateWrapper<User>()
.setSql("balance = balance - 200")
.in("id",ids);
//执行更新
userMapper.update(null,wrapper);
}
使用LambdaQueryWrapper,避免硬编码
@Test
void testLambdaQueryWrapper(){
//构建查询条件 查询出名字中带o的,存款大于等于1000元的人的id、username、info、balance字段
LambdaQueryWrapper<User> wrapper = 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(wrapper);
users.forEach(System.out::println);
}
2)自定义SQL
可以利用MyBatisPlus的Wrapper来构建复杂的Where条件,然后自己定义SQL语句中剩下的部分。
例:将id在指定范围的用户(例如1、2、4 )的余额扣减指定值。
①基于Wrapper构建where条件
@Test
void testSqlWrapper(){
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(即Constants.WRAPPER)
void updateBalanceByIds(@Param("ew") LambdaQueryWrapper<User> wrapper, @Param("amount") int amount);
③自定义SQL,并使用Wrapper条件 – ${ew.customSqlSegment}
<update id="updateBalanceByIds">
UPDATE user SET balance = balance - #{amount} ${ew.customSqlSegment}
</update>
3)Service接口
- 自定义Service接口继承IService接口
public interface UserService extends IService<User> {}
- 自定义Service实现类,实现自定义接口并继承ServiceImpl类,加泛型
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {}
注意:成员变量的注入方式:
- 注解注入 @Autowired (常用,但Spring不推荐)
- 构造器注入,变量前加final,方法上加入注解**@RequiredArgsConstructor**
(1)基于Restful风格实现接口
可直接调用提供的api,复杂条件需要自己构建Sql
//controller层
@ApiOperation("新增用户")
@PostMapping
public void add(@ApiParam("用户表单实体") @RequestBody UserFormDTO userDto){
//转换为po
User user = BeanUtil.copyProperties(userDto, User.class);
userService.save(user);
}
@ApiOperation("删除用户")
@DeleteMapping("/{id}")
public void delete(@ApiParam("用户id") @PathVariable("id") Long id){
userService.removeById(id);
}
@ApiOperation("根据id查询用户")
@GetMapping("/{id}")
public UserVO getById(@ApiParam("用户id") @PathVariable("id") Long id){
User user = userService.getById(id);
//转换为userVo在前端展示
return BeanUtil.copyProperties(user, UserVO.class);
}
@ApiOperation("根据id批量查询")
@GetMapping()
public List<UserVO> getByIds(@ApiParam("用户id集合") @RequestParam("ids") List<Long> ids){
List<User> users = userService.listByIds(ids);
//转换为userVo在前端展示
return BeanUtil.copyToList(users, UserVO.class);
}
@ApiOperation("根据id扣减余额")
@PutMapping("/{id}/deduction/{money}")
public void getByIds(@ApiParam("用户id") @PathVariable("id") Long id,
@ApiParam("扣减金额") @PathVariable("money")Integer money){
userService.deduction(id,money);
}
//service层 复杂条件需要自己构建
public void deduction(Long id, Integer money) {
//查询用户
User user = getById(id);
//无用户 用户冻结
if (id == null || user.getStatus() == 2){
throw new RuntimeException("用户状态异常");
}
//余额是否充足
if (money > user.getBalance()){
throw new RuntimeException("用户余额不足");
}
//扣减余额
// // 1.构建条件
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<User>().eq(User::getId,id);
// //自定义sql
userMapper.updateDeduction(wrapper,money);
}
(2)IService的Lambda查询
需求:实现一个根据复杂条件查询用户的接口,查询条件如下:
-
name:用户名关键字,可以为空
-
status:用户状态,可以为空
-
minBalance:最小余额,可以为空
-
maxBalance:最大余额,可以为空
//controller层
@ApiOperation("根据复杂条件查询用户")
@GetMapping ("/list")
public List<UserVO> queryUsers(@ApiParam("用户查询对象") UserQuery query){
List<User> users = userService.queryUsers(query.getName(),query.getStatus(),query.getMinBalance(),query.getMaxBalance());
//转换为UserVo
return BeanUtil.copyToList(users,UserVO.class);
}
//service层
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();
}
(3)IService的Lambda更新
需求:改造根据id修改用户余额的接口,要求如下
- 完成对用户状态校验
- 完成对用户余额校验
- 如果扣减后余额为0,则将用户status修改为冻结状态2
public void deduction(Long id, Integer money) {
//查询用户
User user = getById(id);
//无用户 用户冻结
if (id == null || user.getStatus() == 2){
throw new RuntimeException("用户状态异常");
}
//余额是否充足
if (money > user.getBalance()){
throw new RuntimeException("用户余额不足");
}
//修改余额
int remainBalance = user.getBalance() - money;
lambdaUpdate()
.set(User::getBalance, remainBalance)
.set(remainBalance == 0, User::getStatus, 2)
.eq(User::getId,id)
.update();
}
(4)IService批量新增
三种方式:
- 普通for循环插入:逐条插入速度极差,不推荐
- IService的批量插入:MP的批量新增,基于预编译的批处理,性能不错
- 开启rewriteBatchedStatements=true参数:配置jdbc参数,开rewriteBatchedStatements,性能最好
MP的批量新增:
void testSaveBatch(){
List<User> list = new ArrayList<>(1000);
for (int i = 0; i < 100000; i++) {
//添加一个user
list.add(buildUser(i));
//每1000条批量插入一次
if (i % 1000 == 0){
userService.saveBatch(list);
//清空集合,准备下一批数据
list.clear();
}
}
}
然后在application.yml中的数据库配置中加上参数rewriteBatchedStatements=true:
spring:
datasource:
url: jdbc:mysql://127.0.0.1:3306/mp?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true
3.扩展功能
1)代码生成
下载插件生成基础代码
- 连接数据库
- 生成代码
2)DB静态工具
两张表相互注入service会导致循环依赖(可注入mapper),或者可用静态工具DB提供的接口实现
①改造根据id查询用户的接口,查询用户的同时,查询出用户对应的所有地址
public UserVO queryUSersById(Long id) {
//查询用户
User user = getById(id);
if (user == null || user.getStatus() == 2){
throw new RuntimeException("用户状态异常");
}
//转换对象
UserVO userVO = BeanUtil.copyProperties(user, UserVO.class);
//获取用户地址
List<Address> addressList = Db.lambdaQuery(Address.class).
eq(Address::getUserId, id)
.list();
//设置地址
if (ObjectUtil.isNotEmpty(addressList)){
userVO.setAddressVO(BeanUtil.copyToList(addressList,AddressVO.class));
}
return userVO;
}
②改造根据id批量查询用户的接口,查询用户的同时,查询出用户对应的所有地址
public List<UserVO> queryUSersByIds(List<Long> ids) {
//查询用户
List<User> users = listByIds(ids);
//用户为空
if (CollUtil.isEmpty(users)){
return Collections.emptyList();
}
//获取用户id
List<Long> userIds = users.stream().map(User::getId).collect(Collectors.toList());
//获取用户地址
List<Address> addressList = Db.lambdaQuery(Address.class).
in(Address::getUserId, userIds)
.list();
//转换地址
List<AddressVO> addressVOS = BeanUtil.copyToList(addressList, AddressVO.class);
//用户地址集合分组处理,相同用户的放入一个集合
Map<Long, List<AddressVO>> addressMap = new HashMap<>();
if (ObjectUtil.isNotEmpty(addressVOS)){
addressMap = addressVOS.stream().collect(Collectors.groupingBy(AddressVO::getUserId));
}
//转换为UserVo返回
List<UserVO> userVO = new ArrayList<>(users.size());
for (User user : users) {
UserVO uv = BeanUtil.copyProperties(user, UserVO.class);
userVO.add(uv);
//设置地址
uv.setAddressVO(addressMap.get(user.getId()));
}
return userVO;
}
③实现根据用户id查询收货地址功能,需要验证用户状态,冻结用户抛出异常(练习)
public List<AddressVO> queryUSersAddressById(Long id) {
List<Address> address = lambdaQuery().eq(Address::getUserId, id).list();
//用户状态冻结
List<User> list = Db.lambdaQuery(User.class).eq(User::getId,id).eq(User::getStatus, 2).list();
if (CollUtil.isNotEmpty(list)){
throw new RuntimeException("用户状态异常");
}
return BeanUtil.copyToList(address,AddressVO.class);
}
3)逻辑删除
基于代码逻辑模拟删除效果,但并不会真正删除数据。思路如下:
-
在表中添加一个字段标记数据是否被删除
-
当删除数据时把标记置为1
-
查询时只查询标记为0的数据
MybatisPlus提供了逻辑删除功能,无需改变方法调用的方式,而是在底层自动修改CRUD的语句。所以,只需要在application.yaml文件中配置逻辑删除的字段名称和值即可
mybatis-plus:
global-config:
db-config:
logic-delete-field: flag # 全局逻辑删除的实体字段名,字段类型可以是boolean、integer
logic-delete-value: 1 # 逻辑已删除值(默认为 1)
logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
缺点:
-
会导致数据库表垃圾数据越来越多,影响查询效率
-
SQL中全都需要对逻辑删除字段做判断,影响查询效率
因此,不太推荐采用逻辑删除功能,如果数据不能删除,可以采用把数据迁移到其它表的办法。
4)枚举处理器
- 给枚举中的与数据库对应value值添加@EnumValue注解
- 向前端展示添加@JsonValue注解
@Getter
public enum UserStatus {
NORMAL(1, "正常"),
FREEZE(2, "冻结")
;
@EnumValue
private final int value;
@JsonValue
private final String desc;
UserStatus(int value, String desc) {
this.value = value;
this.desc = desc;
}
}
- 在application.yml中配置全局枚举处理器,实现类型转换
mybatis-plus:
configuration:
default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler
5)JSON处理器
数据库中user表中有一个json类型的字段
@Data
@AllArgsConstructor
@NoArgsConstructor
public class UserInfo {
private Integer age;
private String intro;
private String gender;
}
在实体类中加入注解@TableField(typeHandler = JacksonTypeHandler.class),@TableName(autoResultMap = true)
@TableName(value = "user", autoResultMap = true)
public class User {
@TableField(typeHandler = JacksonTypeHandler.class)
private UserInfo info;
}
4.插件功能
序号 | 拦截器 | 描述 |
---|---|---|
1 | TenantLineInnerInterceptor | 多租户插件 |
2 | DynamicTableNameInnerInterceptor | 动态表名插件 |
3 | PaginationInnerInterceptor | 分页插件 |
4 | OptimisticLockerInnerInterceptor | 乐观锁插件 |
5 | IllegalSQLInnerInterceptor | SQL性能规范插件,检测并拦截垃圾SQL |
6 | BlockAttackInnerInterceptor | 防止全表更新和删除的插件 |
1)分页
- 在配置类中注册MyBatisPlus的核心插件,同时添加分页插件
@Configuration
public class MybatisConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
// 1.初始化核心插件
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 2.添加分页插件
PaginationInnerInterceptor pageInterceptor = new PaginationInnerInterceptor(DbType.MYSQL);
pageInterceptor.setMaxLimit(1000L); // 设置分页上限
interceptor.addInnerInterceptor(pageInterceptor);
return interceptor;
}
}
- 调用API实现
@Test
void testPageQuery() {
// 1.查询
int pageNo = 1, pageSize = 5;
// 1.1.分页参数
Page<User> page = Page.of(pageNo, pageSize);
// 1.2.排序参数, 通过OrderItem来指定
page.addOrder(new OrderItem("balance", false));
// 1.3.分页查询
Page<User> p = userService.page(page);
// 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);
}
2)通用分页实体
分页请求参数
@Data
public class PageQuery {
private Integer pageNo = 1;
private Integer pageSize = 5;
private String sortBy;
private Boolean isAsc = true;
}
pageDTO传输对象实体类
@Data
@NoArgsConstructor
@AllArgsConstructor
public class PageDTO<V> {
private Long total;
private Long pages;
private List<V> list;
}
service层实现
public PageDTO<UserVO> queryPage(UserQuery query) {
String name = query.getName();
Integer status = query.getStatus();
//1.构建分页条件
//1.1 分页
Page<User> page = Page.of(query.getPageNo(), query.getPageSize());
//1.2 排序 如果排序条件为空就默认排序
if (StrUtil.isNotBlank(query.getSortBy())){
page.addOrder(new OrderItem(query.getSortBy(),query.getIsAsc()));
}else{
page.addOrder(new OrderItem("update_time",false));
}
//2.分页查询
Page<User> userPage = lambdaQuery()
.like(name != null, User::getUsername, name)
.eq(status != null, User::getStatus, status)
.page(page);
//3.封装VO结果
PageDTO<UserVO> userVoPageDTO = new PageDTO<UserVO>();
//总条数
userVoPageDTO.setTotal(userPage.getTotal());
//总页数
userVoPageDTO.setPages(userPage.getPages());
//当前页数据
List<User> records = userPage.getRecords();
if (CollUtil.isEmpty(records)){
userVoPageDTO.setList(Collections.emptyList());
return userVoPageDTO;
}
//转换对象
List<UserVO> userVOS = BeanUtil.copyToList(records, UserVO.class);
userVoPageDTO.setList(userVOS);
return userVoPageDTO;
}
进一步封装,方便调用:
- 封装分页条件
//可变形参
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);
}
- 封装VO结果
/**
* 将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);
}
- service层改造
public PageDTO<UserVO> queryPage(UserQuery query) {
String name = query.getName();
Integer status = query.getStatus();
//1.构建分页条件
Page<User> page = query.toMpPage(query.getSortBy(),query.getIsAsc());
//2.分页查询
Page<User> userPage = lambdaQuery()
.like(name != null, User::getUsername, name)
.eq(status != null, User::getStatus, status)
.page(page);
//3.封装VO结果
// return PageDTO.of(userPage, user -> BeanUtil.copyProperties(user, UserVO.class));
return PageDTO.of(userPage,user -> {
//拷贝基础属性
UserVO userVO = BeanUtil.copyProperties(user, UserVO.class);
//处理特殊逻辑
userVO.setUsername(userVO.getUsername().substring(0,userVO.getUsername().length()-2) + "***");
return userVO;
});
}