1、初探
1.1、简介
MyBatis-Plus
(opens new window)(简称 MP)是一个 MyBatis
(opens new window)的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。
1.2、SpringBoot快速集成
1.2.1、创建SpringBoot工程
这里怎么创建SpringBoot工程就不做详细的介绍了,创建一个SpringBoot工程有如下方式:
- 在SpringBoot官网上快速生成:https://start.spring.io/
- 使用IDEA的Spring Initializr进行创建
- 使用Maven手动创建添加相关依赖
1.2.2、添加MyBatis-Plus依赖
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.2</version>
</dependency>
1.2.3、添加MySql即Lombok依赖
既然是使用MyBatis,那么数据库一定是少不了的。然后为了简化我们的代码,这里使用到了Lombok。
如对Lombok不熟的可以顺带了解下Lombok具体使用。
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.19</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.18</version>
</dependency>
1.2.4、完整的SpringBoot的pom.xml配置
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>spring-boot</artifactId>
<packaging>pom</packaging>
<version>1.0-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.3</version>
</parent>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.18</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.19</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.2</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<jvmArguments>-Dfile.encoding=UTF-8</jvmArguments>
<mainClass>com.tenghu.sbi.Application</mainClass>
</configuration>
</plugin>
</plugins>
</build>
</project>
1.2.5、准备一张万能的用户表
CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
`name` varchar(240) DEFAULT NULL COMMENT '姓名',
`age` int(11) DEFAULT NULL COMMENT '年龄',
`address` varchar(2000) DEFAULT NULL COMMENT '家庭住址',
`phone` varchar(11) DEFAULT NULL COMMENT '联系电话',
`crt_time` datetime DEFAULT NULL COMMENT '创建时间',
`upd_time` datetime DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=51 DEFAULT CHARSET=utf8 COMMENT='用户信息表'
1.2.6、SpringBoot配置
server:
port: 8088
servlet:
context-path: /smp
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
username: root
password: xiaohu
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
mapper-locations: classpath*:mapper/*.xml
这里的mybatis-plus.configuration.log-impl配置的是MyBatis日志
1.2.7、编写Entity、Mapper、Service、Controller代码
- 编写表对应的Entity
@Data
@TableName(value = "user")
public class User {
@TableId(type = IdType.AUTO)
private Integer id;
@TableField(value = "name")
private String name;
@TableField(value = "age")
private Integer age;
@TableField(value = "address")
private String address;
@TableField(value = "phone")
private String phone;
@TableField(value = "crt_time",fill = FieldFill.INSERT)
private Timestamp crtTime;
@TableField(value = "upd_time",fill = FieldFill.UPDATE)
private Timestamp updTime;
}
- 编写Mapper
public interface UserMapper extends BaseMapper<User> {
}
BaseMapper
接口中默认支持日常常用的一些对数据的操作方法,查看源码如下:
public interface BaseMapper<T> extends Mapper<T> {
int insert(T entity);
int deleteById(Serializable id);
int deleteByMap(@Param("cm") Map<String, Object> columnMap);
int delete(@Param("ew") Wrapper<T> queryWrapper);
int deleteBatchIds(@Param("coll") Collection<? extends Serializable> idList);
int updateById(@Param("et") T entity);
int update(@Param("et") T entity, @Param("ew") Wrapper<T> updateWrapper);
T selectById(Serializable id);
List<T> selectBatchIds(@Param("coll") Collection<? extends Serializable> idList);
List<T> selectByMap(@Param("cm") Map<String, Object> columnMap);
T selectOne(@Param("ew") Wrapper<T> queryWrapper);
Integer selectCount(@Param("ew") Wrapper<T> queryWrapper);
List<T> selectList(@Param("ew") Wrapper<T> queryWrapper);
List<Map<String, Object>> selectMaps(@Param("ew") Wrapper<T> queryWrapper);
List<Object> selectObjs(@Param("ew") Wrapper<T> queryWrapper);
<E extends IPage<T>> E selectPage(E page, @Param("ew") Wrapper<T> queryWrapper);
<E extends IPage<Map<String, Object>>> E selectMapsPage(E page, @Param("ew") Wrapper<T> queryWrapper);
}
到这里Mapper就完成了,还需要在启动类中添加扫描Mapper接口对应的包,如下:
@SpringBootApplication(scanBasePackages = "com.tenghu.smp")
@MapperScan(basePackages = "com.tenghu.smp.mapper")
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
其实这里也可以不用配置@MapperScan
注解,如果不配置@MapperScan
注解,就需要在对应的UserMapper
上加@Mapper
注解。
- 编写Service
//接口
public interface UserService {
/**
* 获取所有用户
* @return
*/
List<User> queryAllUser();
/**
* 添加用户
* @param user
*/
void addUser(User user);
/**
* 修改用户
* @param user
*/
void updUser(User user);
}
//实现
@Service
@Slf4j
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper;
@Override
public List<User> queryAllUser() {
return userMapper.selectList(null);
}
@Override
public void addUser(User user) {
userMapper.insert(user);
}
@Override
public void updUser(User user) {
Integer userId = Optional.ofNullable(user.getId()).orElseThrow(()->new RuntimeException("用户ID不能为空!"));
User oldUser = Optional.ofNullable(userMapper.selectById(userId)).orElseThrow(()->new RuntimeException("用户不存在!"));
BeanUtils.copyProperties(user,oldUser);
userMapper.updateById(oldUser);
}
}
这里的更新方法中用到了java8中的Optional
类来判断对象是否为空。
- 编写Controller
@RestController
@RequestMapping(path = "/api/v1/user")
public class UserController {
@Autowired
private UserService userService;
@GetMapping(path = "/all")
public RestResponse queryAllUser(){
return RestResponse.getOkMessage(userService.queryAllUser());
}
@PostMapping(path = "")
public RestResponse addUser(@RequestBody User user){
userService.addUser(user);
return RestResponse.getOkMessage();
}
@PutMapping(path = "/{userId}")
public RestResponse updUser(@PathVariable(name = "userId") Integer userId,
@RequestBody User user){
user.setId(userId);
userService.updUser(user);
return RestResponse.getOkMessage();
}
}
这里封装了个简单的统一出参格式,如下:
@Data
public class RestResponse {
private String code;
private String message;
private Object data;
private RestResponse(String code,String message,Object data){
this.code = code;
this.message = message;
this.data = data;
}
public static RestResponse getOkMessage(){
return new RestResponse("0","success",null);
}
public static RestResponse getOkMessage(Object data){
return new RestResponse("0","success",data);
}
public static RestResponse getOkMessage(String message,Object data){
return new RestResponse("0",message,data);
}
public static RestResponse getOkMessage(String message){
return new RestResponse("0",message,null);
}
public static RestResponse getErrMessage(String message){
return new RestResponse("1",message,null);
}
}
到这为止,SpringBoot整合MyBatis-Plus就算完成了,对于测试,我们可以直接用Postman来调用写的接口,也可以写测试类。这里我直接使用Postman调用接口,日志打印如下:
(1)添加用户:
==> Preparing: INSERT INTO user ( name, age, address, phone, crt_time ) VALUES ( ?, ?, ?, ?, ? )
==> Parameters: 小B(String), 45(Integer), 长隆一路(String), 120(String), null
<== Updates: 1
(2)查询用户:
==> Preparing: SELECT id,name,age,address,phone,crt_time,upd_time FROM user
==> Parameters:
<== Columns: id, name, age, address, phone, crt_time, upd_time
<== Row: 51, 小B, 45, 长隆一路, 120, null, null
<== Total: 1
(3)更新用户:
==> Preparing: UPDATE user SET name=?, age=?, address=?, phone=?, upd_time=? WHERE id=?
==> Parameters: 小A(String), 45(Integer), 长隆一路(String), 120(String), null, 51(Integer)
<== Updates: 1
2、扩展功能
在这里一些简单的功能就不做介绍了,下面介绍实际工作中常用的一些扩展功能。
2.1、条件构造器
在实际工作中肯定会遇到多个条件查询数据,如果没有条件构造器,那么就只能在xml文件中写sql语句,既然用了MyBatis-Plus,那么就能少写SQL就尽量的少写。MyBatis-Plus的条件构造器分为两种:查询与更新两种。
2.1.1、查询构造器
查询构造器有如下几种:
- QueryWrapper
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.like("address","天府");
queryWrapper.gt("age",15);
List<User> userList = userMapper.selectList(queryWrapper);
- QueryChainWrapper:
List<User> userList = new QueryChainWrapper<>(userMapper)
.like("address","天府")
.gt("age",15)
.list();
- LambdaQueryWrapper
LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper();
lambdaQueryWrapper.like(User::getAddress,"天府");
lambdaQueryWrapper.gt(User::getAge,15);
List<User> userList = userMapper.selectList(lambdaQueryWrapper);
- LambdaQueryChainWrapper
List<User> userList = new LambdaQueryChainWrapper<>(userMapper)
.like(User::getAddress,"天府")
.gt(User::getAge,15).list();
从写法上来看,个人更偏向使用Lambda
表达式,因为如果后面字段变了,使用到对应的字段在编译的时候就会报错;如果使用简单版的,传入字符串的字段,实体类的字段改了,相应用到改的字段就不容易发现,最终会导致上线后出现异常。
下面是以上几种查询的日志:
==> Preparing: SELECT id,name,age,address,phone,crt_time,upd_time FROM user WHERE (address LIKE ? AND age > ?)
==> Parameters: %天府%(String), 15(Integer)
<== Columns: id, name, age, address, phone, crt_time, upd_time
<== Row: 57, 小AA, 25, 天府三路, 110, 2021-03-27 20:55:22, null
<== Row: 58, 小AB, 25, 天府四路, 110, 2021-03-27 20:55:36, null
<== Total: 2
2.1.2 、修改构造器
修改构造器与查询构造器一样,也是有4种。
- UpdateWrapper
User user = new User();
user.setName("小D");
user.setAge(25);
//修改的查询条件
UpdateWrapper<User> updateWrapper = new UpdateWrapper<>();
updateWrapper.eq("name","小AA");
userMapper.update(user,updateWrapper);
- UpdateChainWrapper
User user = new User();
user.setName("小CD");
user.setAge(25);
UpdateChainWrapper updateChainWrapper = new UpdateChainWrapper(userMapper);
updateChainWrapper.eq("name","小D");
updateChainWrapper.update(user);
- LambdaUpdateWrapper
LambdaUpdateWrapper<User> lambdaUpdateWrapper = new LambdaUpdateWrapper();
lambdaUpdateWrapper.eq(User::getName,"小CD");
userMapper.update(user,lambdaUpdateWrapper);
- LambdaUpdateChainWrapper
boolean result = new LambdaUpdateChainWrapper<>(userMapper)
.eq(User::getName,"小AB")
.update(user);
以上打印出的日志如下:
==> Preparing: UPDATE user SET name=?, age=?, upd_time=? WHERE (name = ?)
==> Parameters: 小AB(String), 25(Integer), 2021-03-28 00:44:15.476(Timestamp), 小AB(String)
<== Updates: 0
2.2 分页
在实际开发中,分页功能那是避免不了的,使用MyBatis-Plus的分页插件如下:
- 首先需要定义分页使用到的Bean
@Configuration
public class MyBatisConfiguration {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor(){
MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return mybatisPlusInterceptor;
}
}
- 在Service中使用如下
Page<User> page = new Page<>();
userMapper.selectPage(page,null);
可以对Page的参数进行指定,在实际中我们有可能用不完Page中的所有信息,我们可以自己封装一层自己想用的信息,然后从Page里面提取出来给前端。输出日志如下:
==> Preparing: SELECT COUNT(*) FROM user
==> Parameters:
<== Columns: COUNT(*)
<== Row: 4
<== Total: 1
==> Preparing: SELECT id,name,age,address,phone,crt_time,upd_time FROM user LIMIT ?
==> Parameters: 10(Long)
<== Columns: id, name, age, address, phone, crt_time, upd_time
<== Row: 55, 小C, 15, 天府一路, 110, 2021-03-27 20:54:56, null
<== Row: 56, 小A, 15, 天府二路, 110, 2021-03-27 20:55:07, null
<== Row: 57, 小FD, 24, 天府三路, 110, 2021-03-27 20:55:22, 2021-03-27 16:41:39
<== Row: 58, 小FD, 24, 天府四路, 110, 2021-03-27 20:55:36, null
<== Total: 4
2.3 自动填充功能
自动填充功能一般应用在一些默认字段或新增/修改时自动写入,比如创建时间、更新时间等字段。实现步骤如下:
- 在需要自动填充的字段加上填充类型
@TableField(value = "crt_time",fill = FieldFill.INSERT)
private Timestamp crtTime;
@TableField(value = "upd_time",fill = FieldFill.UPDATE)
private Timestamp updTime;
这里对创建时间插入时自动填充,更新时间字段更新时自动填充。
- 使用一个组件处理自动填充
@Component
@Slf4j
public class MetaObjectComponent implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
log.info("start insert fill ...............");
this.strictInsertFill(metaObject,"crtTime",Timestamp.class,new Timestamp(System.currentTimeMillis()));
log.info("end insert fill .................");
}
@Override
public void updateFill(MetaObject metaObject) {
log.info("start update fill ...............");
this.strictUpdateFill(metaObject,"updTime",Timestamp.class,new Timestamp(System.currentTimeMillis()));
log.info("end update fill .................");
}
}
注意:自动填充的值类型必须与实体类中的字段类型一致,不然就会插入Null。
插入一条数据测试结果如下:
入参:
{"address":"天府四路","age":25,"name":"小ABC","phone":"110"}
入参没有对crtTime赋值,后端service里面也没对crtTime赋值,从日志可以看出,自动填充功能已经生效。
==> Preparing: INSERT INTO user ( name, age, address, phone, crt_time ) VALUES ( ?, ?, ?, ?, ? )
==> Parameters: 小ABC(String), 25(Integer), 天府四路(String), 110(String), 2021-03-28 01:00:20.832(Timestamp)
<== Updates: 1
2.4 乐观锁
实现步骤如下:
- 添加OptimisticLockerInterceptor
@Bean
public OptimisticLockerInterceptor optimisticLockerInnerInterceptor(){
return new OptimisticLockerInterceptor();
}
注意:官网文档这里是提示用
OptimisticLockerInnerInterceptor
,但是经过测试会报异常,因此还是用OptimisticLockerInterceptor
类,虽然提示已失效,但测试结果version增加了。
- 在实体乐观锁版本增加@version注解
@Version
@TableField(value = "version")
private Integer version;
- 测试
userMapper.updateById(oldUser);
oldUser.setVersion(user.getVersion());
oldUser.setName("李四");
userMapper.updateById(oldUser);
上面代码,更新一次后将旧版本再设置回去并将姓名改了,日志如下:
==> Preparing: UPDATE user SET name=?, age=?, address=?, phone=?, version=?, upd_time=? WHERE id=? AND version=?
==> Parameters: 小CB(String), 25(Integer), 天府四路(String), 110(String), 12(Integer), 2021-03-28 01:35:11.132(Timestamp), 55(Integer), 11(Integer)
<== Updates: 1
==> Preparing: UPDATE user SET name=?, age=?, address=?, phone=?, version=?, upd_time=? WHERE id=? AND version=?
==> Parameters: 李四(String), 25(Integer), 天府四路(String), 110(String), 12(Integer), 2021-03-28 01:35:11.132(Timestamp), 55(Integer), 11(Integer)
<== Updates: 0
从日志可以看出第二次并没有修改成功,因为第一次修改版本已经升级,第二次修改的版本号还是旧版本,因此对旧版本号的数据修改失败。