入门
MyBatis-Plus (opens new window)(简称 MP)是一个 MyBatis (opens new window) 的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。
准备数据
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user`
(
id BIGINT NOT NULL COMMENT '主键ID',
name VARCHAR(30) NULL DEFAULT NULL COMMENT '姓名',
age INT NULL DEFAULT NULL COMMENT '年龄',
email VARCHAR(50) NULL DEFAULT NULL COMMENT '邮箱',
PRIMARY KEY (id)
);
DELETE FROM `user`;
INSERT INTO `user` (id, name, age, email) VALUES
(1, 'Jone', 18, '[email protected]'),
(2, 'Jack', 20, '[email protected]'),
(3, 'Tom', 28, '[email protected]'),
(4, 'Sandy', 21, '[email protected]'),
(5, 'Billie', 24, '[email protected]');
pom
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>3.0.3</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
controller
@RestController
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/selectAll")
public List<User> queryAll(){
return userService.queryAll();
}
}
service
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper;
@Override
public List<User> queryAll() {
return userMapper.selectAll();
}
}
mapper
@Mapper
public interface UserMapper {
public List<User> selectAll();
}
<mapper namespace="com.eun.mapper.UserMapper">
<select id="selectAll" resultType="com.eun.domain.User">
select * from user
</select>
</mapper>
Mybatis开发效率
每当我们使用Mybatis框架的时候,都会有如下步骤:
- Mapper接口提供一个抽象方法
- Mapper映射文件提供SQL标签和语句
- Service中注入Mapper对象
- 调用Mapper实例的方法
- Controller中注入Service对象
- 调用Service实例的方法
有一些操作是通用的:
- 对于DAO,可以由框架生成单表的Mapper抽象方法和对应的SQL实现,不需要我们去实现
- 对于Service,可以由框架提供一些Service的抽象方法和对应的实现,而不需要我们手动实现
MybatisPlus是对Mybatis的简化和封装。
使用MybatisPlus
两步:
- 引入依赖
- 继承BaseMapper,指定泛型为实体类
- 实体类添加注解
引入依赖
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.2</version>
</dependency>
MybatisPlus提供了starter,引入该依赖会传递入Mybatis依赖,并且完成了对Mybatis的自动装配,需要替换掉原有的Mybatis依赖(避免冲突)
<dependencies>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.2</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.11</version>
</dependency>
</dependencies>
继承BaseMapper
继承BaseMapper<T>
,指定泛型:
public interface UserMapper extends BaseMapper<User> {
}
注意:没有指定@Mapper
测试:
@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.insert(user);
}
}
没有指定@Mapper注解,也可以注入UserMapper,因为在启动类上使用了注解:
@MapperScan("com.itheima.mp.mapper")
@SpringBootApplication
public class MpDemoApplication {
public static void main(String[] args) {
SpringApplication.run(MpDemoApplication.class, args);
}
}
指定Mapper的扫描包,生成代理对象
常用注解
在上文的程序中,基于MybatisPlus我们并没有在Mapper接口中写SQL语句,也没有指定Mapper映射文件,MybatisPlus是如何知道我们要操作哪张表的?
public interface UserMapper extends BaseMapper<User> {
}
在UserMapper接口中,继承BaseMapper时指定了泛型User,MybatisPlus自动将User的首字母变为小写得到user作为数据库的表名
- MybatisPlus通过扫描实体类,并基于反射获取实体类信息作为数据库表信息
- 类名 驼峰 转 下划线 得到表名
- 名为id的字段作为主键
- 变量名 驼峰 转 下划线 作为字段名
但是实际开发中表名很多都是以tb_开始的,和实体类的名字对应不上,需要通过MybatisPlus提供的注解来指定别名:
- @TableName:指定表名
- @TableId:指定表的主键字段信息
- @TableField:指定表中的普通字段信息
@TableId
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.ANNOTATION_TYPE})
public @interface TableId {
/**
* 字段名(该值可无)
*/
String value() default "";
/**
* 主键类型
* {@link IdType}
*/
IdType type() default IdType.NONE;
}
value():指定主键字段名
type():指定主键类型,类型为IdType枚举:
public enum IdType {
AUTO(0),
NONE(1),
INPUT(2),
ASSIGN_ID(3),
ASSIGN_UUID(4);
private final int key;
private IdType(int key) {
this.key = key;
}
public int getKey() {
return this.key;
}
}
值 | 含义 |
---|---|
AUTO | 数据库 ID 自增 |
NONE | 无状态,该类型为未设置主键类型(注解里等于跟随全局,全局里约等于 INPUT) |
INPUT | insert 前自行 set 主键值 |
ASSIGN_ID | 分配 ID(主键类型为 Number(Long 和 Integer)或 String)(since 3.3.0),使用接口IdentifierGenerator的方法nextId(默认实现类为DefaultIdentifierGenerator雪花算法) |
ASSIGN_UUID | 分配 UUID,主键类型为 String(since 3.3.0),使用接口IdentifierGenerator的方法nextUUID(默认 default 方法) |
分布式全局唯一 ID 长整型类型(please use ASSIGN_ID) | |
32 位 UUID 字符串(please use ASSIGN_UUID) | |
分布式全局唯一 ID 字符串类型(please use ASSIGN_ID) |
这里比较常见的有三种:
-
AUTO
:利用数据库的id自增长 -
INPUT
:手动生成id -
ASSIGN_ID
:雪花算法生成Long
类型的全局唯一id,这是默认的ID策略
@TableField
使用场景:
- 成员变量名和数据库字段名不一致
实体类的实例变量名是name,而数据库表的字段名为username,需要使用@TableField指定字段名
- 实例变量以is开头,并且是布尔值
MybatisPlus识别字段时默认映射的数据库表字段名为married,但是数据库表的字段名实际上就是is_married
- 实例变量名和数据库关键字冲突
实例变量名 order 和SQL语法中排序关键字 order 冲突,需要使用 反引号 进行转义
- 实例变量不是数据库字段
实例变量address在数据库表中并没有这个字段,在查询操作时通过反射获取到实例变量名,再从ResultSet中获取这个字段值时就会出错
常用配置
MybatisPlus也支持基于yaml文件的自定义配置,详见官方文档:使用配置 | MyBatis-Plus
大多数的配置都有默认值,因此我们都无需配置。但还有一些是没有默认值的,例如:
-
实体类的别名扫描包
-
全局id类型
mybatis-plus:
type-aliases-package: com.itheima.domain.po
mapper-locations: classpath*:mapper/*.xml
configuration:
cache-enabled: false
global-config:
db-config:
id-type: auto
update-strategy: not_null #只更新非空值
mapper-locations:Mybatis-Plus支持自定义Mapper映射文件,此属性指定mapper的扫描位置,默认就是这个值,也可以不指定,只要mapper映射文件在这个位置一定会被扫描
classpath* 其中的 * 包含是扫描其他jar包下的配置文件
id-type:指定主键id的类型,此处指定后在 @TableId 中就不必再指定了,如果指定就会覆盖全局的id-type
指定雪花算法后再指定id递增就会在雪花的基础上进行递增
update-strategy:指定update操作时的策略
核心功能
条件构造器
通用Mapper提供了根据复杂条件Wrapper进行CRUD的方法:
selectOne:Ajax查询用户名是否重复
Wrapper继承结构:
Wrapper相当于where条件,实际上CRUD都是根据where条件来进行的,只是复杂更新会使用UpdateWrapper
AbstractWrapper:提供了where中的所有条件
QueryMapper:在AbstractWrapper的基础上扩展了select方法,可以指定要查询的字段
select(...)
:指定要查询的字段,默认是 select *
UpdateWrapper:在AbstractWrapper的基础上扩展了set方法,可以指定SQL语句中set
部分的操作
无论是修改、删除、查询,都可以使用QueryWrapper来构建查询条件,这些操作都需要where匹配到指定的数据,而复杂的更新需要使用UpdateWrapper来完成
案例
QueryWrapper
- 查询出名字带有 o 的,存款 ≥ 1000元的记录的id、username、info、balance
select
id,username,info,balance
from user
where username like '%o%' and balance >= ?
@Autowired
private UserMapper userMapper;
@Test
public void testQueryWrapper(){
QueryWrapper<User> userQueryWrapper = new QueryWrapper<>();
userQueryWrapper.select("id","username","info","balance")
.like("username","o")
.ge("balance",1000);
List<User> userList = userMapper.selectList(userQueryWrapper);
for (User user : userList) {
System.out.println(user);
}
}
- 更新用户名为jack的用户的余额为2000
//更新用户名为jack的用户的余额为 2000@Test
public void testQueryWrapper2(){
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("username","jack");
userMapper.update(User.builder().balance(2000).build(),queryWrapper);
}
此时QueryWrapper作为where查询条件,也可以使用UpdateWrapper作为查询条件:
@Test
public void testQueryWrapper3() throws Exception {
UpdateWrapper<User> userUpdateWrapper = new UpdateWrapper<User>()
.eq("username", "jack")
//设置余额为2000
.set("balance", 2000);
userMapper.update(null,userUpdateWrapper);
}
使用UpdateWrapper时,update方法第一个参数可以指定为null,在第二个参数UpdateWrapper中通过set指定要更新的值
UpdateWrapper
基于BaseMapper的update方法使用QueryWrapper更新时只能直接赋值,对于一些复杂的需求就难以实现,需要使用UpdateWrapper
- 需求:更新id为
1,2,4
的用户的余额,扣200
update user set balance = balance - 200 where id in (1,2,4)
此时的SET进行赋值操作需要基于字段现有值,此时就要使用UpdateWrapper的setSql功能:
@Test
public void testQueryWrapper4() throws Exception {
UpdateWrapper<User> userUpdateWrapper = new UpdateWrapper<User>();
userUpdateWrapper.setSql("balance = balance - 200")
.in("id",1,2,4);
userMapper.update(null,userUpdateWrapper);
}
LambdaQueryWrapper
在UpdateWrapper或QueryWrapper中,数据库表的字段名都是字符串魔法值,这在编码规范中显然是不推荐的,可以使用LambdaQueryWrapper
LambdaQueryWrapper就是基于变量的getter方法结合反射技术,只需要将条件对应的字段的getter方法传递给MybatisPlus,就能计算出对应的变量名了。传递方法可以使用JAVA8的方法引用和Lambda表达式,MybatisPlus又提供了一套基于Lambda的Wrapper,包含两个:
-
LambdaQueryWrapper对应QueryWrapper
-
LambdaUpdateWrapper对应UpdateWrapper
-
查询出名字带有 o 的,存款 ≥ 1000元的记录的id、username、info、balance
@Test
public void testLambdaQueryWrapper() throws Exception {
LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.select(User::getId, User::getUsername, User::getInfo, User::getBalance)
.ge(User::getBalance, 1000);
List<User> userList = userMapper.selectList(lambdaQueryWrapper);
userList.forEach(System.out::println);
}
如果要改变数据库表的字段名,只需要shift + F6改变对应po类的属性名,所有使用get方法的地方都会随之改变
但是此时查询会报错:
Caused by: java.lang.IndexOutOfBoundsException: Index 4 out of bounds for length 4
数组下标越界,关键信息是数组长度为4,我们查询的结果是4个字段,在上文中的更新案例中:
//更新用户名为jack的用户的余额为 2000@Test
public void testQueryWrapper2(){
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("username","jack");
userMapper.update(User.builder().balance(2000).build(),queryWrapper);
}
将User类添加了@Builder注解,这个注解会在User类中生成全参构造,没有无参构造了,但是MybatisPlus查询结果集映射时使用的是无参构造,需要添加@NoArgsConstructor,@AllArgsConstructor:
@Data
@TableName("user")
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class User {
/**
* 用户id
*/
@TableId(value = "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;
}
自定义拼接SQL
在更新余额时,我们在service中写了SQL语句:
@Test
public void testQueryWrapper4() throws Exception {
UpdateWrapper<User> userUpdateWrapper = new UpdateWrapper<User>();
userUpdateWrapper.setSql("balance = balance - 200")
.in("id",1,2,4);
userMapper.update(null,userUpdateWrapper);
}
这种写法也是不太规范的,SQL语句最好都维护在持久层,而不是业务层。对于当前案例来说,由于是批量更新,只能将SQL写在Mapper.xml文件中,使用foreach生成动态SQL,如果查询条件非常复杂,SQL语句也会变得更加复杂
MybatisPlus提供了自定义SQL的功能,利用Wrapper生成查询条件,结合Mapper.xml拼接SQL
@Test
public void testCustomerWrapper(){
//利用Wrapper构造查询条件
QueryWrapper<User> queryWrapper = new QueryWrapper();
queryWrapper.in("id",1,2,4);
//调用自定义的更新方法,传入更新数值和Wrapper对象
userMapper.updateBalance(200,queryWrapper);
}
@Test
public void testCustomerWrapper(){
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.in(User::getId,1,2,4);
int amount = 200;
userMapper.updateBalanceByIds(amount,queryWrapper);
}
对应的Mapper
@Update("update user set balance = balance - #{count} ${ew.customSqlSegment}")
public void updateBalance(@Param("count") int count, @Param("ew") QueryWrapper<User> queryWrapper);
注意:上述语句中的ew和customerSqlSegment都不能修改
-
queryWrapper查询条件对象相当于SQL语句的where子句
-
${ew.customSqlSegment}可以用在注解中,也可以使用在Mapper.xml文件中对SQL语句进行拼接
Service接口
MybatisPlus不仅提供了BaseMapper,还提供了通用的Service接口及默认实现,封装了一些常用的Service模板方法,通用接口为IService,默认实现为ServiceImpl,其中封装的方法可以大致分为几类:
- save 新增
- remove 删除
- update 更新
- get 查询单个结果
- list 查询结果集
- count 计数
- page 分页
基本方法说明
新增:
-
save
是新增单个元素 -
saveBatch
是批量新增 -
saveOrUpdate
是根据id判断,如果数据存在就更新,不存在则新增 -
saveOrUpdateBatch
是批量的新增或修改
删除:
-
removeById
:根据id删除 -
removeByIds
:根据id批量删除 -
removeByMap
:根据Map中的键值对为条件删除 -
remove(Wrapper)
:根据Wrapper条件删除
修改:
![image-20231206104024837](file:///D:/Desktop/%E8%AF%BE%E5%A0%82%E6%96%87%E4%BB%B6/%E5%BE%AE%E6%9C%8D%E5%8A%A1%E9%98%B6%E6%AE%B5/01-mybatisplus/%E8%AE%B2%E4%B9%89/assets/image-20231206104024837.png?lastModify=1706277492)
-
updateById
:根据id修改 -
update(Wrapper)
:根据UpdateWrapper
修改,Wrapper
中包含set
和where
部分 -
update(T,Wrapper)
:按照T
内的数据修改与Wrapper
匹配到的数据 -
updateBatchById
:根据id批量修改
Get:
-
getById
:根据id查询1条数据 -
getOne(Wrapper)
:根据Wrapper
查询1条数据 -
getBaseMapper
:获取Service
内的BaseMapper
实现,某些时候需要直接调用Mapper
内的自定义SQL
时可以用这个方法获取到Mapper
List:
-
listByIds
:根据id批量查询 -
list(Wrapper)
:根据Wrapper条件查询多条数据 -
list()
:查询所有
Count:
-
count()
:统计所有数量 -
count(Wrapper)
:统计符合Wrapper
条件的数据数量
lambda
-
lambdaQuery()
-
lambdaUpdate()
上文中我们进行LambdaQueryWrapper时需要手动new这个对象,但是IService提供了方法可以直接获取
使用IService
- 自定义Service接口继承IService接口,指定泛型 本次操作的实体类
public interface IUserService extends IService<User> {
}
- 自定义Service实现类,实现自定义接口并继承ServiceImpl实现类,指定泛型:操作的Mapper和实体类User
@Service
public class IUserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
}
案例
分析:前四个接口在IService中都有实现,而最后一个需要自己在Service中实现
- 引入web和knife4j依赖:
<!--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>
然后需要在 application.yaml
配置swagger信息如下:
knife4j:
enable: true
openapi:
title: 用户管理接口文档
description: 用户管理接口文档
version: 1.0
concat: 黑马
url: http://www.itheima.com
email: [email protected]
group:
default:
group-name: default
api-rule: package
#指定扫描的包
api-rule-resources:
- com.itheima.mp.controller
- 准备DTO、VO
- 开发
@Api(tags = "用户管理接口")
@RestController
@RequestMapping("/users")
@RequiredArgsConstructor
public class UserController {
private final IUserService iUserService;
@PostMapping
@ApiOperation("新增用户")
public void create(@RequestBody UserFormDTO userFormDTO){
iUserService.save(BeanUtil.copyProperties(userFormDTO, User.class));
}
@DeleteMapping("/{id}")
@ApiOperation("根据id删除用户")
public void remove(@PathVariable Long id){
iUserService.removeById(id);
}
@GetMapping("/{id}")
@ApiOperation("根据id查询用户")
public UserVO queryById(@PathVariable Long id){
return BeanUtil.copyProperties(iUserService.getById(id),UserVO.class);
}
@GetMapping()
@ApiOperation("根据ids批量查询用户")
public List<UserVO> queryByIds(@RequestParam("ids")List<Long> ids){
return BeanUtil.copyToList(iUserService.listByIds(ids),UserVO.class);
}
}
使用了IUserServiceImpl中已经实现的方法。
注意:此时没有使用@AutoWired注入,使用的是构造方法注入,需要指定@RequiredArgsConstructor注解
自定义Service方法
根据id更新扣减余额在IUserServiceImpl中没有该方法的实现,需要在IUserService中定义该方法。
controller
@ApiOperation("根据id扣减余额")
@PutMapping("/{id}/deduction/{amount}")
public void updateBalance(@PathVariable Long id, @PathVariable Integer amount){
iUserService.deductionBalance(id,amount);
return ;
}
service
- 使用IService实现的updateById方法
@Service
public class IUserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
@Override
public void deductionBalance(Long id, Integer amount) {
//1. 查询用户
User user = this.getById(id);
//2. 判断用户是否存在及状态是否正常
if (ObjectUtil.isNull(user) || user.getStatus().equals(2)){
return;
}
//3. 判断金额是否充足
if (user.getBalance() < amount){
return;
}
//4. 扣减余额
user.setBalance(user.getBalance() - amount);
this.updateById(user);
}
}
- 使用UserMapper进行更新
使用UserMapper进行更新就需要先获取到mapper对象,调用mapper对象的自定义方法:
mapper:
@Update("update user set balance = balance - #{amount} where id = #{id}")
void updateBalanceById(Long id, Integer amount);
service
@Service
public class IUserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
@Override
public void deductionBalance(Long id, Integer amount) {
//1. 查询用户
User user = this.getById(id);
//2. 判断用户是否存在及状态是否正常
if (ObjectUtil.isNull(user) || user.getStatus().equals(2)){
return;
}
//3. 判断金额是否充足
if (user.getBalance() < amount){
return;
}
//4. 扣减余额
baseMapper.updateBalanceById(id,amount);
}
}
但是在Service中并未注入baseMapper,这个属性是在父类ServiceImpl中定义的:
@SuppressWarnings("unchecked")
public class ServiceImpl<M extends BaseMapper<T>, T> implements IService<T> {
protected Log log = LogFactory.getLog(getClass());
@Autowired
protected M baseMapper;
@Override
public M getBaseMapper() {
return baseMapper;
}
protected Class<T> entityClass = currentModelClass();
@Override
public Class<T> getEntityClass() {
return entityClass;
}
protected Class<M> mapperClass = currentMapperClass();
}
在继承时指定了ServiceImpl的第一个泛型M,也就是其中实例变量baseMapper的类型M
IService的Lambda查询
IService提供了LambdaWrapper功能进行复杂查询。
根据复杂条件查询用户
查询条件如下:
-
name:用户名关键字,可以为空
-
status:用户状态,可以为空
-
minBalance:最小余额,可以为空
-
maxBalance:最大余额,可以为空
类似一个用户的后台管理界面,管理员可以自己选择条件来筛选用户,因此上述条件不一定存在,需要做判断。
@PostMapping("/list")
@ApiOperation("根据条件UserQuery查询用户列表")
public List<UserVO> queryList(@RequestBody UserQuery userQuery){
LambdaQueryWrapper<User> userLambdaQueryWrapper = new LambdaQueryWrapper<>();
userLambdaQueryWrapper.like(StrUtil.isNotEmpty(userQuery.getName()), User::getUsername, userQuery.getName())
.eq(ObjectUtil.isNotEmpty(userQuery.getStatus()), User::getStatus, userQuery.getStatus())
.ge(ObjectUtil.isNotEmpty(userQuery.getMinBalance()), User::getBalance, userQuery.getMinBalance())
.le(ObjectUtil.isNotEmpty(userQuery.getMaxBalance()), User::getBalance, userQuery.getMaxBalance());
List<User> list = iUserService.list(userLambdaQueryWrapper);
return BeanUtil.copyToList(list,UserVO.class);
}
在组织查询条件的时候,加入了 status != null
这样的参数,意思就是当条件成立时才会添加这个查询条件,类似Mybatis的mapper.xml文件中的标签。这样就实现了动态查询条件效果了。
这里需要手动new LambdaQueryWrapper对象,IService其实提供了lambdaQuery()方法
Service中对LambdaQueryWrapper
和LambdaUpdateWrapper
的用法进一步做了简化。我们无需自己通过new
的方式来创建Wrapper
,而是直接调用lambdaQuery
和lambdaUpdate
方法:
@PostMapping("/list")
@ApiOperation("根据条件UserQuery查询用户列表")
public List<UserVO> queryList(@RequestBody UserQuery userQuery){
List<User> list = iUserService.lambdaQuery().like(StrUtil.isNotEmpty(userQuery.getName()), User::getUsername, userQuery.getName())
.eq(ObjectUtil.isNotEmpty(userQuery.getStatus()), User::getStatus, userQuery.getStatus())
.ge(ObjectUtil.isNotEmpty(userQuery.getMinBalance()), User::getBalance, userQuery.getMinBalance())
.le(ObjectUtil.isNotEmpty(userQuery.getMaxBalance()), User::getBalance, userQuery.getMaxBalance())
.list();
return BeanUtil.copyToList(list,UserVO.class);
}
可以发现lambdaQuery方法中除了可以构建条件,还需要在链式编程的最后添加一个list()
,这是在告诉MP我们的调用结果需要是一个list集合。这里不仅可以用list()
,可选的方法有:
-
.one()
:最多1个结果 -
.list()
:返回集合结果 -
.count()
:返回计数结果
MybatisPlus会根据链式编程的最后一个方法来判断最终的返回结果。
IService的Lambda更新
与lambdaQuery类似,IService中的lambdaUpdate方法可以非常方便的实现复杂更新业务
需求:改造UserServiceImpl中 根据id修改用户余额的接口,要求如下:
-
完成对用户状态的校验
-
完成对用户余额的校验
-
扣减后余额为0,将用户status置为2(冻结)
也就是:在扣减用户余额后,对用户余额进行判断,如果剩余余额为0,应该将status置为2,这部分是动态SQL,需要使用LambdaUpdateWrapper
- lambdaUpdate():
@Service
public class IUserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
@Override
public void deductionBalance(Long id, Integer amount) {
//1. 查询用户
User user = this.getById(id);
//2. 判断用户是否存在及状态是否正常
if (ObjectUtil.isNull(user) || user.getStatus().equals(2)) {
return;
}
//3. 判断金额是否充足
if (user.getBalance() < amount) {
return;
}
//4. 扣减余额
user.setBalance(user.getBalance() - amount);
this.lambdaUpdate().set(User::getBalance, user.getBalance())
.set(user.getBalance() == 0, User::getStatus, 2)
.eq(User::getId, user.getId())
.update();
}
}
- LambdaUpdateWrapper
@Service
public class IUserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
@Override
public void deductionBalance(Long id, Integer amount) {
//1. 查询用户
User user = this.getById(id);
//2. 判断用户是否存在及状态是否正常
if (ObjectUtil.isNull(user) || user.getStatus().equals(2)) {
return;
}
//3. 判断金额是否充足
if (user.getBalance() < amount) {
return;
}
//4. 扣减余额
user.setBalance(user.getBalance() - amount);
LambdaUpdateWrapper<User> userLambdaUpdateWrapper = new LambdaUpdateWrapper<>();
userLambdaUpdateWrapper.set(User::getBalance, user.getBalance())
.set(user.getBalance() == 0, User::getStatus, 2)
.eq(User::getId, user.getId());
baseMapper.update(null,userLambdaUpdateWrapper);
}
}
使用IService提供的lambdaUpdate方法可以不获取baseMapper;使用LambdaUpdateWrapper就必须传入mapper对象的方法
但是此时是没有开启事务的,如果查询余额后判断充足,但是被其他线程修改了余额,可能会更新为负数,显然是不合适的。
解决办法:
-
for updated添加悲观锁
-
添加乐观锁
@Override
public void deductionBalance(Long id, Integer amount) {
//1. 查询用户
User user = this.getById(id);
//2. 判断用户是否存在及状态是否正常
if (ObjectUtil.isNull(user) || user.getStatus().equals(2)) {
return;
}
//3. 判断金额是否充足
if (user.getBalance() < amount) {
return;
}
//4. 扣减余额
int remainBalance = user.getBalance() - amount;
this.lambdaUpdate()
.set(User::getBalance, remainBalance)
.set(user.getBalance() == 0, User::getStatus, 2)
.eq(User::getId, user.getId())
.eq(User::getBalance,user.getBalance()) //乐观锁
.update();
}
乐观锁:查询时余额在user对象的balance属性中,更新时判断:当前数据库表中的余额和对象的balance相等,如果相等就是这个余额未被修改过
注意:此时就不能 user.setBalance(user.getBalance() - amount)
了,因为要记录刚查询到结果时的余额
IService批量新增
需求:批量插入10w条用户数据,进行对比
- 普通for循环
- IService的批量插入
- 开启rewriteBatchedStatements=true
普通for循环
执行了78117ms
@Test
public void saveOneByOne(){
long begin = System.currentTimeMillis();
for (int i = 0; i < 10; i++) {
for (int j = 0; j < 10000; j++) {
iService.save(buildUser(i * 10000 + j));
}
}
long end = System.currentTimeMillis();
//78117ms
System.out.println((end - begin) + "ms");
}
public User buildUser(int i){
User user = new User();
user.setUsername("user_" + 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;
}
IService批量插入
执行了8822ms
@Test
public void saveBatch(){
long begin = System.currentTimeMillis();
List<User> users = new ArrayList<>(10000);
for (int i = 0; i < 10; i++) {
for (int j = 0; j < 10000; j++) {
users.add(buildUser(i * 10000 + j));
}
iService.saveBatch(users);
users.clear();
}
long end = System.currentTimeMillis();
//8822ms
System.out.println((end - begin) + "ms");
}
开启rewriteBatchedStatements
url: jdbc:mysql://127.0.0.1:3306/mp2?useUnicode=true&characterEncoding=UTF-8&
autoReconnect=true&rewriteBatchedStatements=true&serverTimezone=Asia/Shanghai
执行了3413ms
@Test
public void saveBatch(){
long begin = System.currentTimeMillis();
List<User> users = new ArrayList<>(10000);
for (int i = 0; i < 10; i++) {
for (int j = 0; j < 10000; j++) {
users.add(buildUser(i * 10000 + j));
}
iService.saveBatch(users);
users.clear();
}
long end = System.currentTimeMillis();
//3413ms
System.out.println((end - begin) + "ms");
}
总结
-
实现IService接口:单表的基础业务不需要编写代码
-
IService接口提供lambda方法:不必获取mapper再执行SQL,编码更方便
扩展功能
代码生成
使用MybatisPlus之后,基础的Mapper、Service、PO代码相对固定,MybatisPlus官方提供了代码生成器,根据数据库表结构生成相关代码
方式一:在Idea
的plugins市场中搜索并安装MyBatisPlus
插件(插件不太稳定,建议按照官网方式)
点击上述的 Config Database
配置数据库连接如下:
点击 Code Generator
配置代码生成信息如下:
方式二:上述的图形界面插件,存在不稳定因素;所以建议使用代码方式生成。官网安装说明。在项目中 pom.xml
添加依赖如下:
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.5.3.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
<version>2.3.32</version>
<scope>test</scope>
</dependency>
使用方式:创建MybatisPlusGeneratorTest.java
package com.itheima.mp;
import com.baomidou.mybatisplus.generator.FastAutoGenerator;
import com.baomidou.mybatisplus.generator.config.OutputFile;
import com.baomidou.mybatisplus.generator.config.rules.DbColumnType;
import com.baomidou.mybatisplus.generator.engine.FreemarkerTemplateEngine;
import java.sql.Types;
import java.util.Collections;
public class MybatisPlusGeneratorTest {
public static void main(String[] args) {
String url = "jdbc:mysql://127.0.0.1:3306/mp?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true";
FastAutoGenerator.create(url , "root", "root")
.globalConfig(builder -> {
builder.author("JBL") // 设置作者
.enableSwagger() // 开启 swagger 模式
.outputDir("D:\\itcast\\generatedCode"); // 指定输出目录
})
.dataSourceConfig(builder -> builder.typeConvertHandler((globalConfig, typeRegistry, metaInfo) -> {
int typeCode = metaInfo.getJdbcType().TYPE_CODE;
if (typeCode == Types.SMALLINT) {
// 自定义类型转换
return DbColumnType.INTEGER;
}
return typeRegistry.getColumnType(metaInfo);
}))
.packageConfig(builder -> {
builder.parent("com.itheima.mp") // 设置父包名
.controller("controller")
.entity("domain.po") // 设置实体类包名
.service("service") // 设置service包名
.serviceImpl("service.impl") // 设置service实现类包名
.mapper("mapper") // 设置mapper包名
//.moduleName("address") // 设置父包模块名
.pathInfo(Collections.singletonMap(OutputFile.xml, "D:\\itcast\\generatedCode\\mapper")); //设置mapperXml生成路径
})
.strategyConfig(builder -> {
builder.addInclude("address") // 设置需要生成的表名
.addTablePrefix("t_", "c_") // 设置过滤表前缀
.controllerBuilder().enableRestStyle() // 开启restful风格控制器
.enableFileOverride() // 覆盖已生成文件
.entityBuilder().enableLombok(); // 开启lombok模型,默认是false
})
.templateEngine(new FreemarkerTemplateEngine()) // 使用Freemarker引擎模板,默认的是Velocity引擎模板
.execute();
}
}
-
注意:输出目录指定到src/main/java
-
这个方法需要放在Test目录下
静态工具类DB
枚举类型处理器
User类中有一个状态字段:
private Integer status;
类似于这种数据字典的字段一般都会定义为枚举,做业务判断时就可以直接基于枚举进行比较。但是我们数据库采用的是int类型,对应的PO也是Integer类型,在业务操作时必须手动将枚举和Integer进行转换,非常麻烦
MybatisPlus提供了一个处理枚举的类型转换器,可以帮助我们将枚举类型和数据库类型自动转换
-
@EnumValue:枚举中哪个属性值作为存入数据库的值
-
@JsonValue:枚举中哪个属性值作为序列化时展示的字段
使用步骤
- application.yml配置全局枚举处理器
mybatis-plus:
type-aliases-package: com.itheima.domain.po
mapper-locations: classpath*:mapper/*.xml
configuration:
cache-enabled: false
default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler
global-config:
db-config:
id-type: auto
update-strategy: not_null #只更新非空值
- 创建枚举类,修改属性类型
public enum UserStatus {
NORMAL(1,"正常"),
FREEZE(2,"冻结");
@EnumValue //存入数据库的值
private int code;
@JsonValue //序列化时显式转换的值
private String desc;
UserStatus(int code, String desc) {
this.code = code;
this.desc = desc;
}
}
注意:枚举类没有提供Getter方法
JSON类型处理器
当前数据库表中有字段类型就是JSON:
create table address
(
id bigint auto_increment
primary key,
user_id bigint null comment '用户ID',
province varchar(10) null comment '省',
city varchar(10) null comment '市',
town varchar(10) null comment '县/区',
mobile varchar(255) null comment '手机',
street varchar(255) null comment '详细地址',
contact varchar(255) null comment '联系人',
is_default bit default b'0' null comment '是否是默认 1默认 0否',
notes varchar(255) null comment '备注',
deleted bit default b'0' null comment '逻辑删除'
)
当前我们存储的都是字符串,前端展示的效果是:
{
"id": 2,
"username": "Rose",
"info": "{\"age\": 19, \"intro\": \"青涩少女\", \"gender\": \"female\"}",
"status": 1,
"balance": 0,
"addressVO": [
{
"id": 59,
"userId": 2,
"province": "北京",
"city": "北京",
"town": "朝阳区",
"mobile": "13900112222",
"street": "金燕龙办公楼",
"contact": "Rose",
"isDefault": true,
"notes": null
},
{
"id": 63,
"userId": 2,
"province": "广东",
"city": "佛山",
"town": "永春",
"mobile": "13301212233",
"street": "永春武馆",
"contact": "Rose",
"isDefault": false,
"notes": null
}
]
}
此时数据库的info字段中存储的是一个字符串 {\"age\": 19, \"intro\": \"青涩少女\", \"gender\": \"female\"}
,PO/VO中也是以字符串类型接收的,但是前端希望得到的就是一个JSON类型的对象
从数据库中查询时就要将这个JSON格式的字符串转换为对象
- 修改User的属性类型,并且指定类型处理器和自动映射:
@Data
@TableName(value = "user",autoResultMap = true) //自动映射
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class User {
@TableId(value = "id")
private Long id;
private String username;
private String password;
private String phone;
@TableField(typeHandler = JacksonTypeHandler.class) //类型处理器
private UserInfo info;
private UserStatus status;
}
@Data
@ApiModel(description = "用户VO实体")
public class UserVO {
@ApiModelProperty("用户id")
private Long id;
@ApiModelProperty("用户名")
private String username;
@ApiModelProperty("详细信息")
private UserInfo info;
@ApiModelProperty("使用状态(1正常 2冻结)")
private Integer status;
@ApiModelProperty("账户余额")
private Integer balance;
private List<AddressVO> addressVO;
}
UserVo中不需要指定类型处理器,只需要改变数据类型为UserInfo,因为此处的属性是直接拷贝的
此时的Info就是一个对象了:
"id": 2,
"username": "Rose",
"info": {
"age": 19,
"intro": "青涩少女",
"gender": "female"
},
"status": 1,
"balance": 0,
"addressVO": [
{
"id": 59,
"userId": 2,
"province": "北京",
"city": "北京",
"town": "朝阳区",
"mobile": "13900112222",
"street": "金燕龙办公楼",
"contact": "Rose",
"isDefault": true,
"notes": null
},
{
"id": 63,
"userId": 2,
"province": "广东",
"city": "佛山",
"town": "永春",
"mobile": "13301212233",
"street": "永春武馆",
"contact": "Rose",
"isDefault": false,
"notes": null
}
]
}
注意:UserMapper.xml
如果有内容的话;那么该文件改名字为 UserMapper.xml.bak
, 原有的xml文件中并没有配置json类型的处理器,所以会报错。如果要xml文件中也需要配置,参考:字段类型处理器 | MyBatis-Plus (baomidou.com)
商城类项目的规格就是使用JSON进行存储,这样在数据库中存储这个对象的规格就是任意的
MybatisPlus插件
MybatisPlus提供的内置拦截器:
-
多租户插件:在每次查询时拼接一个租户id
-
分页插件:ThreadLocal中存储一个标记,有标记就进行分页
分页插件
在未引入分页插件的情况下,MybatisPlus
是不支持分页功能的,IService
和BaseMapper
中的分页方法都无法正常起效,必须配置分页插件。
配置分页插件
@Configuration
public class MybatisConfig {
//配置Mybatis plus分页拦截器
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
分页API
Page对象是IPage接口的子类:
使用:
测试
@Test
public void testByPage(){
//构造分页对象
int pageNo = 2;
int pageSize = 3;
Page<User> userPage = Page.of(pageNo, pageSize);
//排序
userPage.addOrder(OrderItem.asc("balance"));
//分页查询
Page<User> result = iUserService.page(userPage, null);
System.out.println(result.getPages());
System.out.println(result.getTotal());
System.out.println(result.getRecords());
}
在同一个线程中,多次查询也是会进行分页的,之前的PageHelper只能分页一次
案例-黑马商城
启动nginx不要双击,使用cmd,start nginx.exe
标签:MybatisPlus,private,查询,User,id,public,user From: https://www.cnblogs.com/euneirophran/p/18073910