文章目录
- MybatisPlus
- 快速入门
- 快速开始
- 常见注解
- 常见配置
- 核心功能
- 条件构造器(Wrapper)
- 自定义SQL
- Service接口
- 基本用法
- 基础业务接口
- 复杂业务接口
- Lamda查询
- Lamda更新
- 批量新增
- 扩展功能
- 代码生成
- 代码生成器
- 快速开发插件
- 静态工具
- 逻辑删除
- 枚举处理器
- JSON处理器
- 插件功能
- 分页插件
- 通用分页实体
- 乐观锁插件
- 示例
MybatisPlus
快速入门
快速开始
- 引入依赖
<!--引入MP后,自然替代原Mybatis-->
<dependency>
<groupId>com.baomidou</groudId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.1</version>
</dependency>
- Mapper继承
//自定义Mapper继承MP提供的BaseMapper接口
public interface UserMapper extends BaseMapper<User> {...}
- 实体类中添加注解声明表信息
application.yml
中添加配置- 根据需求选择对应方法
常见注解
MyBatisPlus通过扫描实体类,并基于反射获取实体类信息作为数据库表信息
- 约定大于配置
- 类名驼峰转下划线作为表名(eg. UserInfo—>user_info)
- 名为
id
的字段作为主键- 变量名驼峰转下划线作为表的字段名(eg. LocalDataTime—>local_data_time)
- 常用注解
@TableName
:用于指定表名(eg. @TableName(“表名”)@TableId
:用于指定表中主键字段,可通过类型指定实现:
- 自增:AUTO (eg. @TableId(“id”, type = IdType.AUTO)
- set注入:INPUT
- 自动分配:ASSIGN_ID,默认设置,使用雪花算法随机分配
@TableField
:用于指定表中普通字段
- 成员变量名与数据库字段名不一致
- 成员变量名以
is
开头,但为布尔值(eg. @TableField(“is_graduate”)- 成员变量名与数据库关键词冲突(eg. @TableField(" ‘order’ ")
- 成员变量名不是数据库字段(eg. @TableField(exist = false)
- 全部注解
MabatisPlus注解
常见配置
MybatisPlus使用配置
核心功能
条件构造器(Wrapper)
- MP通过
Wrapper
支持复杂where
条件,满足常用开发需求
子类AbstractWrapper
子类QueryWrapper
子类UpdateWrapper
- 示例
/*
*查询名称带'o'的,且存款大于1000的id、username、info、balance
*select id, username, info, balance from user where username like '%o%' and balance >= 1000
*/
@Test
void testQueryWrapper() {
//1.condition
QueryWrapper<User> wrapper = new QueryWrapper<User>()
.select("id", "username", "info", "balance")
.like("username", "o")
.ge("balance", 1000);
//2.query
List<User> users = userMapper.selectList(Wrapper);
user.forEach(System.out.println);
}
/*
*更新用户名为`jack`的用户的余额为2000
*update user set balance = 2000 where (username = 'jack')
*/
@Test
void testUpdateByQueryWrapper() {
//1.data
User user = new User();
user.setBalance(2000);
//2.condition
QueryWrapper<User> wrapper = new QueryWrapper<User>()
.eq("username", "jack");
//3.update
userMapper.update(user, wrapper);
}
/*
*更新id为`1` `2` `4`的用户的余额,扣除200
*update user set balance = balance - 200 where id in (1, 2, 4)
*/
@Test
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) ;
}
/*
*使用LamdaQueryWrapper替代第一个示例
*/
@Test
void testLamdaQueryWrapper() {
//1.condition
QueryWrapper<User> wrapper = new QueryWrapper<User>()
.select(user::getId, User::getUsername, User::getInfo, User::getBalance)
.like(User::getUsername, "o")
.ge(User::getBalance, 1000);
//2.query
List<User> users = userMapper.selectList(Wrapper);
user.forEach(System.out.println);
}
- 用法:
QueryWrapper
和LamdaQueryWrapper
构建select
delete
update
的where
条件部分UpdateWrapper
和LamdaUpdateWrapper
通常只在set
语句使用- 尽量使用
LamdaQueryWrapper
和LamdaUpdateWrapper
来避免硬编码
自定义SQL
- 使用Wrapper构建
where
条件后,自定义SQL语句补充2. 示例
/*
*基于Wrapper构建where条件
*/
List<Long> ids = List.of(1L, 2L, 4L);
int amount = 200;
//1.condition
LamdaQueryWrapper<User> wrapper = new LamdaQueryWrapper<User>.
.in(User::getId, ids);
//2.custom
userMapper.updateBalanceByIds(wrapper, amount);
/*
*mapper方法参数中用Param注解声明wrapper变量名,必须为ew
*/
void updateBalanceByIds(@Param("ew") LamdaQueryWrapper<User> wrapper, @Param("amount") int amount);
<!--自定义SQL,并使用Wrapper条件-->
<update id="updateBalanceByIds>
UPDATE user SET balance = balance - #{amount} ${ew.customSqlSegment}
</update>
Service接口
基本用法
- 自定义接口继承
IService
接口
//指定实体类型
public interface IUserService extend IService<User> {...}
- 自定义实现类继承
ServiceImpl
实现类
@Service
//指定泛型类型和实体类型,通过反射获取对应内容
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {...}
基础业务接口
-
基于Restful风格实现下面接口
-
引入依赖
<!--swagger-->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-openapi2-spring-boot-starter</artifactId>
<version>4.1.0</version>
<dependency>
<!--web-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<dependency>
- 配置swagger
knife4j:
enable:true
openapi:
title:用户管理接口文档
description:"用户管理接口文档"
contact:jobs
version:v1.0.0
group:
default:
group-name:default
api-rule:package
api-rule-resource:
- com.jobs.mp.controller
- 创建对应DTO、PO、VO实体
- 创建Controller类
@Api(tags = "用户管理接口")
@RequestMapping("/user")
@RestController
@RequiredArgsConstructor//必要参数构造方法,结合下列 final 关键字,使IUserService能够完成必要参数的构造后注入,不需要处理其中到底有多少个参数
public class UserController {
private final IUserService userService;
@ApiOperation("新增用户接口")
@PostMapping
public void savaUser(@RequestBody UserFormDTO userDTO) {
//1.copy DTO to PO
User user = BeanUtil.copyProperties(userDTO, User.class);
//2.save
userService.save(user);
}
@ApiOperation("删除用户接口")
@DeleteMapping("{id}")
public void queryUserById(@ApiParam("用户id") @PathVariable("id") Long id) {
userService.removeById(id);
}
@ApiOperation("根据id查询用户接口")
@GetMapping("{id}")
public UserVo lamdaQueryUserById(@ApiParam("用户id") @PathVariable("id") Long id) {
//1.get
User user = userService.getById(id);
//2.copy PO to VO
return BeanUtil.copyProperties(User, UserVO.class);
}
@ApiOperation("根据id批量查询用户接口")
@GetMapping
public List<User> lamdaQueryUserById(@ApiParam("用户id集合") @RequestParam("ids") List<Long> ids) {
//1.get
List<User> users = userService.listByIds(ids);
//2.copy PO to VO
return BeanUtil.copyToList(Users, UserVO.class);
}
}
复杂业务接口
- UserMapper接口中声明方法
public interface UserMapper extends BaseMapper<User> {
@Update("UPDATE user SET balance = balance - #{money} WHERE id = #{id}")
void deductBalance(@Param("id") Long id, @Param("money") Integer money);
}
- Service接口中声明方法
public interface IUserService extends IService<User> {
public void deductBalance(Long id, Integer money);
}
- Service实现类中定义方法
@Service
public class UserServiceImpl extends ServiceImpl implements IUserService {
@Override
public void deductBalance(Long id, Integer money) {
//1.query
User user = getById(id);
//2.check status
if (user == null || user.getStatus() == 2) {
throw new RuntimeException("用户状态异常!");
}
//3.check balance
if (user.getBalance() < money) {
throw new RuntimeException("用户余额不足!");
}
//4. deduct
baseMapper.deductBalance(id, money);
}
}
- Controller类中完成接口
@ApiOperation("扣减用户余额接口")
@PutMapping("/{id}/deduction/{money}")
public void deductBalance(@ApiParam("用户id") @PathVariable("id") Long id,
@ApiParam("扣减金额") @PathVariable("money") Integer money) {
userService.deductBalance(id, money);
}
Lamda查询
- 定义查询类
@Data
@ApiModel(description = "用户查询条件实体")
public class UserQuery {
@ApiModelProperty("用户名关键字")
private String name;
@ApiModelProperty("用户状态:1-正常,2-冻结")
private Integer status;
@ApiModelProperty("余额最小值")
private Integer minBalance;
@ApiModelProperty("余额最大值")
private Integer maxBalance;
}
- Service接口声明方法
public interface IUserService extends IService<User> {
public void deductBalance(Long id, Integer money);
List<User> queryUser(String name, Integer status, Integer minBalance, Integer maxBalance);
}
- Service实现类定义方法
@Service
public class UserServiceImpl extends ServiceImpl implements IUserService {
@Override
public void deductBalance(Long id, Integer money) {...}
@Override
public List<User> queryUser(String name, Integer status, Integer minBalance, Integer maxBalance) {
return lamdaQuery()
.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();
}
}
- Controller类中完成接口
@ApiOperation("根据复杂条件查询用户接口")
@GetMapping("/list")
public List<UserVO> queryUsers(UserQuery query) {
//1. query
List<User> users = userService.queryUsers(
query.getName(),
query.getStatus(),
query.getMinBalance(),
query.getMaxBalance());
//2. copy PO to VO
return BeanUtil.copyToList(users, UserVO.class);
}
Lamda更新
- Service实现类中更改扣除流程
@Override
@Transactional
public void deductBalance(Long id, Integer money) {
//1.query
User user = getById(id);
//2.check status
if (user == null || user.getStatus() == 2) {
throw new RuntimeException("用户状态异常!");
}
//3.check balance
if (user.getBalance() < money) {
throw new RuntimeException("用户余额不足!");
}
//4. deduct
int remainBalance = user.getBalance() - money;
lamdaUpdate()
.set(User::getBalance, remainBalance)
.set(remainBalance == 0, User::getStatuse, 2)
.eq(User::getId, id)
.eq(User::getBalance, user.getBalance())//乐观锁
.update();
}
批量新增
- 普通for循环逐条插入,性能极差
@Test
void testSaveOneByOne() {
long startTime = System.currentTimeMillis();
for (int i = 1; i <= 100000; i++) {
userService.save(buildUser(i));
}
long endTime = System.currentTimeMillis();
System.out.println(endTime - startTime;
}
// 10^5 ms
- MP批量新增,基于预编译的批处理,性能较好
@Test
void testSaveBatch() {
//1000 per time, 100 times
//create list
List<User> list = new ArrayList<>(1000);
long startTime = System.currentTimeMillis();
for (int i = 1; i <= 100000; i++) {
//insert object
list.add(buildUser(i));
//each time insert once
if (i % 1000 == 0) {
userService.saveBatch(list);
//clear
list.clear();
}
}
long endTime = System.currentTimeMillis();
System.out.println(endTime - startTime);
}
// 10^4 ms
- 配置jdbc参数,性能最佳
rewriteBatchedStatements = true
# 10^3 ms
- 优化策略
- 网络请求耗时多,减少网络请求(1—>2)
- SQL语句多条耗时多,减少为一条(2—>3)
扩展功能
代码生成
代码生成器
引入官方依赖,在配置文档中设定生成代码
较复杂
快速开发插件
- MyBatisX官方插件
- MyBatisPlus插件
- 安装插件
- 连接数据库
- 选择要生成代码的表(可多选)
- 按照选填项完成设置
- 生成
PO
**Mapper
I**Service
**ServiceImpl
**Controller
等内容
静态工具
为了避免表间循环调用的情况,可使用静态Db
方法代替复杂注入
- Service接口声明方法
public interface IUserService extends IService<User> {
public void deductBalance(Long id, Integer money);
List<User> queryUser(String name, Integer status, Integer minBalance, Integer maxBalance);
List<User> queryUserAndAddressByIds(List<Long> ids);
}
- Service实现类中定义方法
@Override
public List<User> queryUserAndAddressByIds(List<Long> ids) {
//1.user query
List<User> users = listByIds(ids);
if (CollUtil.isEmpty(user)) {
return Collections.emptyList();
}
//2.address query
//2.1.get user list
List<Long> userIds = users.stream().map(User::getId).collect(Collections.toList());
//2.2.get address list by userId
List<Address> addresses = Db.lamdaQuery(Address.class).in(Address::getUserId, userIds).list());
//2.3.transfer addressPO to VO
List<AddressVO> addressVOList = BeanUtil.copyToList(addresses, AddressVO.class);
//2.4.grouping user`s address, one user`s address put into same collection
Map<Long, List<AddressVO> addressMap = new HashMap<>(0);
if (collUtil.isNotEmpty(addressVOList)) {
addressMap = addressVOList.stream().collect(Collectors.groupingBy(AddressVO::getUserId));
}
//3.transfer User to UserVO
List<UserVO> list = new ArrayList<>(users.size());
for (User user : user) {
//3.1.transfer UserPO to VO
UserVO vo = BeanUtil.copyProperties(user, UserVO.class);
list.add(vo);
//3.2.transfer addressPO to VO
vo.setAddresses(addressMap.get(user.getId());
}
return list;
}
- Controller类中实现接口
@ApiOperation("根据id批量查询用户接口")
@GetMapping
public List<User> lamdaQueryUserById(@ApiParam("用户id集合") @RequestParam("ids") List<Long> ids) {
return userService.queryUserAndAddressByIds(ids);
}
逻辑删除
- 基于代码逻辑模拟删除效果,但并不真正删除数据
eg.
UPDATE user SET deleted = 1 WHERE id = 1 AND deleted = 0
- 配置方式
mybatis-plus:
global-config:
db-config:
logic-delete-field: flag # 全局逻辑删除的实体字段名,可以为boolean、integer
logic-delete-value: 1 # 逻辑已删除值(默认为1)
logic-not-delete-value: 0 # 逻辑未删除值(默认为0)
- 问题
- 数据库表垃圾数据增加,影响查询效率
- SQL使用全都需要对逻辑删除字段做判断,影响查询效率
- 可通过数据迁移弥补逻辑删除的缺陷
- 失效原因及解决
- MP依赖与代码生成器依赖版本不一致
- 切换相同版本依赖
枚举处理器
- 配置
mybatis-plus:
configuration:
default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler
- 状态类
@Getter
public enum UserStatus {
NORMAL(1, "正常"),
FROZEN(2, "冻结"),
;
@EnumValue
private final int value;
@JsonValue //返回时传输该注解标记的值
private final String desc;
UserStatus(int value, String desc) {
this.value = value;
this.desc = desc;
}
}
- 使用
eg.
if (user == null || user.getStatus() == UserStatus.FROZEN) {...}
JSON处理器
eg.
# | 名称 | 数据类型 | 注释 | 长度 |
---|---|---|---|---|
5 | info | JSON | 详细信息 |
- 对应字段创建实体类
@Data
public class UserInfo {
private Integer age;
private String intro;
private String gender;
}
- 字段标注注解,并指定类型处理器
...
@TableField(typeHandler = JacksonTypeHandler.class)
private UserInfo info;
...
- 向表类添加注解,开启映射
@Data
@TableName(value="user", autoResultMap=true)
public class User {...}
插件功能
拦截器 | 描述 |
---|---|
TenantLineInnerInterceptor | 多租户插件 |
DynamicTableNameInnerInterceptor | 动态表名插件 |
PaginationInnerInterceptor | 分页插件 |
OptimisitcLockerInnerInterceptor | 乐观锁插件 |
IllegalSQLInnerInterceptor | SQL性能规范插件(检测并拦截垃圾SQL) |
BlockAttackInnerInterceptor | 防止全表更新和删除的插件 |
分页插件
- 配置类中注册MP核心插件,添加分页插件
@Configuration
public class MybatisConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
//初始化核心插件
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
//添加分页插件
PaginationInnerInterceptor pageInterceptor = new PaginationInnerInterceptor(DbType.MYSQL);
pageInterceptor.setMaxLimit(1000L);//设置分页上限
interceptor.addInnerInterceptor(pageInterceptor);
return interceptor;
}
}
- ServiceTest类中使用分页插件
@Test
void testPageQuery() {
int pageNo = 1, pageSize = 2;
//1.prepare pagination
//1.1.page condition
Page<User> page = Page.of(pageNo, pageSize);
//1.2.sort condition
page.addOrder(new OrderItem("balance", true));
page.addOrder(new OrderItem("id", true));
//2.pagination query
Page<User> p = userService.page(page);
//3.analysis
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);
}
通用分页实体
- 分页查询实体
UserQuery.java
@EqualsAndHashCode(callSuper = true)
@Data
@ApiModel(description="用户查询条件实体")
public cl ass UserQuery extends PageQuery {
@ApiModelProperties("用户名关键字")
private String name;
@ApiModelProperties("用户状态:1-正常,2-冻结")
private Integer status;
@ApiModelProperties("余额最小值")
private Integer minBalance;
@ApiModelProperties("余额最大值")
private Integer maxBalance;
}
PageDTO.java
@Data
@ApiModel(description="分页结果")
public class PageDTO<T> {
@ApiModelProperties("总条数")
private Integer total;
@ApiModelProperties("总页数")
private Integer pages;
@ApiModelProperties("集合")
private Integer list;
}
- Service接口声明
...
PageDTO<UserVO> queryUsersPage(UserQuery query);
...
- Service实现类定义
@Override
public PageDTO<UserVO> queryUsersPage(UserQuery query) {
String name = query.getName();
Integer status = query.getStatus();
//1.condition
//1.1.page condition
Page<User> page = Page.of(query.getPageNo(), query.getPageSize());
//1.2.sort condition
if (StrUtil.isNotBlank(query.getSortBy())) {
//not null
page.addOrder(new OrderItem(query.getSortBy(), query.getIsAsc()));
}else {
//null, sory by update time
page.addOrder(new OrderItem("update_time", false));
}
//2.pagination query
Page<User> p = lamdaQuery()
.like(name != null, User::getUsername, name)
.eq(status != null, User::getStatus, status)
.page(page);
//3.pack VO
PageDTO<UserVO> dto = new PageDTO<>();
//3.1.total records
dto.setTotal(p.getTotal());
//3.2.total pages
dto.setPages(p.getPages());
//3.3.current page
List<User> records = p.getRecords();
if (CollUtil.isEmpty(records)) {
dto.setList(Collections.emptyList());
return dto;
}
//3.4.copy user`s VO
dto.setList(BeanUtil.copyToList(records, UserVO.class));
//4.return
return dto
}
- Controller类中实现
@ApiOperation("根据条件分页查询用户接口")
@GetMapping("/List")
public PageDTO<UserVO> queryUsersPage(UserQuery query) {
return userService.queryUsersPage(query);
}
乐观锁插件
- 定义
- 乐观锁:认为操作数据时始终处于乐观状态,不会有他人同时修改数据。因此乐观锁不会上锁,只在执行更新时判断他人是否在此期间修改过该数据,若修改则放弃本次操作,否则执行;
- 悲观锁:认为操作始终处于悲观状态,存在他人同时修改数据。因此悲观锁操作时直接锁定数据,操作结束后才会释放,期间他人无法修改该数据
- 实现方式
- 取出记录时,获取当前version
- 更新时附带此version
- 执行更新时,判定
SET version = newVersion WHERE version = oldVersion
- 如果version不对,则更新失败
示例
- 表实体中加上
version
@Data
@TableNmae("user")
public class User {
...
//数据库表中也应有此字段
@Version
@TableField(value = "version")
private Integer version;
}
- 启动类加上注解
@MapperScan(basePackages = "com.kaven.mybatisplus.dao")
- 添加插件
@Configuration
public class MybatisConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
//初始化核心插件
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
//添加分页插件
PaginationInnerInterceptor pageInterceptor = new PaginationInnerInterceptor(DbType.MYSQL);
pageInterceptor.setMaxLimit(1000L);//设置分页上限
interceptor.addInnerInterceptor(pageInterceptor);
//添加乐观锁插件
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
return interceptor;
}
}
- 测试
@SpringBootTest
public class LockerTest {
@Autowired
private UserMapper userMapper;
@Test
public void updateById() {
User user = userMapper.selectById("1");
int version = user.getVersion();
user.setPassword("new password");
user.setVersion(version);
int rows = userMapper.updateById(user);
System.out.println("infect: " + rows);
}
}
- 说明
- 支持的数据类型有:
int
Integer
long
Long
Date
Timestamp
LocalDateTime
- 整数类型下
newVersion = oldVersion + 1
newVersion
会写回到entity
中- 仅支持
updateById(entity)
与update(entity, wrapper)
方法- 且
update(entity, wrapper)
方法下,wrapper无法复用