MyBatisPlus详解(三)lambdaQuery、lambdaUpdate、批量新增、代码生成、Db静 ...

打印 上一主题 下一主题

主题 818|帖子 818|积分 2454

前言

MyBatisPlus详解系列文章:
MyBatisPlus详解(一)项目搭建、@TableName、@TableId、@TableField注解与常见设置
MyBatisPlus详解(二)条件构造器Wrapper、自定义SQL、Service接口
2 核心功能

2.3 Service接口

2.3.3 Lambda

IService接口中还提供了Lambda功能来简化复杂查询及更新功能。
2.3.3.1 lambdaQuery

例如,要实现一个根据复杂条件查询用户信息的接口,接口文档如下:
接口哀求方式哀求路径哀求参数返回值根据条件查询用户列表GET/user/listUserQueryList<User> 查询条件UserQuery的字段如下:


  • username:用户名关键字,可以为空
  • status:用户状态,可以为空
  • minBalance:最小余额,可以为空
  • maxBalance:最大余额,可以为空
上述条件有可能为空,因此在查询时必要做判断。
首先定义一个查询实体类UserQuery:
  1. // com.star.learning.pojo.UserQuery
  2. @Data
  3. public class UserQuery {
  4.     private String username;
  5.     private Integer status;
  6.     private Integer minBalance;
  7.     private Integer maxBalance;
  8. }
复制代码
接着在UserController类中编写一个list()方法:
  1. // com.star.learning.controller.UserController
  2. @GetMapping("/list")
  3. public List<User> list(UserQuery userQuery) {
  4.     // 1.组织条件
  5.     String username = userQuery.getUsername();
  6.     Integer status = userQuery.getStatus();
  7.     Integer minBalance = userQuery.getMinBalance();
  8.     Integer maxBalance = userQuery.getMaxBalance();
  9.     System.out.println("根据条件查询用户列表 => " + userQuery);
  10.     LambdaQueryWrapper<User> wrapper = new QueryWrapper<User>().lambda()
  11.             .like(username != null, User::getUsername, username)
  12.             .eq(status != null, User::getStatus, status)
  13.             .ge(minBalance != null, User::getBalance, minBalance)
  14.             .le(maxBalance != null, User::getBalance, maxBalance);
  15.     // 2.查询用户
  16.     List<User> users = userService.list(wrapper);
  17.     System.out.println("查询结果 => " + userQuery);
  18.     return users;
  19. }
复制代码
在上述代码进行构造条件时,使用了username != null如许的判断语句,它的效果就和Mapper文件的<if>标签一样,只有当条件建立时才会添加这个查询条件,从而实现动态查询。
调用/user/list?username=o&minBalance=500,控制台打印信息如下:
  1. 根据条件查询用户列表 => UserQuery(username=o, status=null, minBalance=500, maxBalance=null)
  2. ==>  Preparing: SELECT id,username,password,phone,info,status,balance,create_time,update_time FROM t_user WHERE (username LIKE ? AND balance >= ?)
  3. ==> Parameters: %o%(String), 500(Integer)
  4. <==      Total: 1
  5. 查询结果 => [User(id=3, username=Hope, password=123, phone=13900112222, info={"age": 25, "intro": "上进青年", "gender": "male"}, status=1, balance=19800, createTime=2024-04-21T10:13:35, updateTime=2024-04-21T18:48:28)]
复制代码
可见,在上述SQL语句中只有username和minBalance是查询条件,其余两个字段均没有作为查询条件。
上述代码还可以继续简化,我们无需通过new的方式来创建Wrapper,而是直接调用lambdaQuery方法:
  1. // com.star.learning.controller.UserController
  2. @GetMapping("/list")
  3. public List<User> list(UserQuery userQuery) {
  4.     // 1.组织条件
  5.     String username = userQuery.getUsername();
  6.     Integer status = userQuery.getStatus();
  7.     Integer minBalance = userQuery.getMinBalance();
  8.     Integer maxBalance = userQuery.getMaxBalance();
  9.     System.out.println("根据条件查询用户列表 => " + userQuery);
  10.     // 2.查询用户
  11.     List<User> users = userService.lambdaQuery()
  12.             .like(username != null, User::getUsername, username)
  13.             .eq(status != null, User::getStatus, status)
  14.             .ge(minBalance != null, User::getBalance, minBalance)
  15.             .le(maxBalance != null, User::getBalance, maxBalance)
  16.             .list();
  17.     System.out.println("查询结果 => " + users);
  18.     return users;
  19. }
复制代码
可以发现,lambdaQuery方法中除了可以构建条件,还可以在链式编程的末了添加一个list(),告诉MP调用效果必要是一个List集合。
再次调用/user/list?username=o&minBalance=500接口,执行效果是同等的。
MybatisPlus会根据链式编程的末了一个方法来判断终极的返回效果,除了使用list()返回集合效果,还可以使用one()返回1个效果,使用count()返回计数效果。
2.3.3.2 lambdaUpdate

与lambdaQuery()方法类似,IService中的lambdaUpdate()方法可以非常方便的实现复杂更新业务。
例如有如许一个需求:根据id修改用户余额时,如果扣减后余额为0,则将用户status修改为冻结状态(2)。
也就是说,在扣减用户余额时,必要对用户剩余余额做出判断,如果发现剩余余额为0,则应该将status修改为2,这就是说update语句的set部分是动态的。
修改UserServiceImpl实现类中的deductBalance()方法:
  1. // com.star.learning.service.impl.UserServiceImpl
  2. @Override
  3. public void deductBalance(Long userId, Integer money) {
  4.     // 1.查询用户
  5.     User user = getById(userId);
  6.     System.out.println(user);
  7.     // 2.判断用户状态
  8.     if (user == null || user.getStatus() == 2) {
  9.         throw new RuntimeException("用户状态异常");
  10.     }
  11.     // 3.判断用户余额
  12.     if (user.getBalance() < money) {
  13.         throw new RuntimeException("用户余额不足");
  14.     }
  15.     // 4.扣减余额
  16.     int remainBalance = user.getBalance() - money;
  17.     lambdaUpdate()
  18.             // 更新余额
  19.             .set(User::getBalance, remainBalance)
  20.             // 动态判断是否更新status
  21.             .set(remainBalance == 0, User::getStatus, 2)
  22.             // 条件
  23.             .eq(User::getId, userId)
  24.             // 乐观锁
  25.             .eq(User::getBalance, user.getBalance())
  26.             .update();
  27.     // UpdateWrapper<User> wrapper = new UpdateWrapper<User>()
  28.     //        .eq("id", userId);
  29.     // userMapper.deductBalanceByIds(money, wrapper);
  30. }
复制代码
调用/user/4/deduction/400接口,控制台打印信息如下:
  1. 扣减id=4的用户的余额400
  2. ==>  Preparing: SELECT id,username,password,phone,info,status,balance,create_time,update_time FROM t_user WHERE id=?
  3. ==> Parameters: 4(Long)
  4. <==      Total: 1
  5. User(id=4, username=Thomas, password=123, phone=17701265258, info={"age": 29, "intro": "伏地魔", "gender": "male"}, status=1, balance=400, createTime=2024-04-21T10:13:35, updateTime=2024-04-21T16:10:20)
  6. ==>  Preparing: UPDATE t_user SET balance=?,status=? WHERE (id = ? AND balance = ?)
  7. ==> Parameters: 0(Integer), 2(Integer), 4(Long), 400(Integer)
  8. <==    Updates: 1
复制代码
可以看到,SQL语句中同时将status字段也更新了。
2.3.4 批量新增

现有以下单元测试,逐条向数据库插入10000条数据:
  1. @Test
  2. public void testSaveOneByOne() {
  3.     long b = System.currentTimeMillis();
  4.     for (int i = 1; i <= 10000; i++) {
  5.         userService.save(buildUser(i));
  6.     }
  7.     long e = System.currentTimeMillis();
  8.     System.out.println("耗时:" + (e - b));
  9. }
  10. private User buildUser(int i) {
  11.     User user = new User();
  12.     user.setUsername("user_" + i);
  13.     user.setPassword("123");
  14.     user.setPhone("" + (18688190000L + i));
  15.     user.setBalance(2000);
  16.     user.setInfo("{"age": 24, "intro": "英文老师", "gender": "female"}");
  17.     return user;
  18. }
复制代码
执行单元测试,控制台打印信息如下:
  1. 耗时:28069
复制代码
然后再测试一下使用MybatisPlus的批量新增saveBatch()方法:
  1. @Test
  2. public void testBatch() {
  3.     List<User> userList = new ArrayList<>(1000);
  4.     long b = System.currentTimeMillis();
  5.     for (int i = 1; i <= 10000; i++) {
  6.         userList.add(buildUser(i));
  7.         // 每1000条插入一次数据
  8.         if(i % 1000 == 0) {
  9.             userService.saveBatch(userList);
  10.             userList.clear();
  11.         }
  12.     }
  13.     long e = System.currentTimeMillis();
  14.     System.out.println("耗时:" + (e - b));
  15. }
复制代码
执行单元测试,控制台打印信息如下:
  1. 耗时:4675
复制代码
可以看到,使用了MybatisPlus的批量新增saveBatch()方法后,比逐条新增所用的时间缩短了7倍左右,性能大大提拔。
实际上,MybatisPlus的批处置惩罚是基于PrepareStatement的预编译模式,将批量insert语句预备好后批量进行提交,因此终极在数据库执行时还是会有执行多条insert语句。
而如果想要得到最佳性能,最好是将多条SQL合并为一条。在MySQL的客户端连接参数中有如许的一个参数:rewriteBatchedStatements,用于设置是否重写批处置惩罚的statement语句,默认值为false。
因此,想要将多条SQL合并为一条,只必要修改项目中的application.yml文件,在jdbc的url后面添加参数&rewriteBatchedStatements=true。

修改完成后,再次测试插入10000条数据,控制台打印信息如下:
  1. 耗时:2263
复制代码
可见,性能得到进一步地提拔,且SQL语句被合并成了一条:

   提示:测试项目中原本使用的MySQL驱动版本是5.1.48,无法实现合并SQL语句的功能;
而将其版本更新到8.0.16之后,就可以实现了。
  3 扩展功能

3.1 代码生成

MybatisPlus官方提供了代码生成器根据数据库表结构生成POJO、Mapper、Service等相干代码。只不外代码生成器同样要编码使用,相对麻烦。
但在IDEA中,有一款MybatisPlus的插件,它可以基于图形化界面完成MybatisPlus的代码生成,非常简单。

下面就以数据库中的t_address表来演示代码自动生成。
在IDEA顶部菜单栏中,找到Tools菜单,选择Config Database:

在弹出的窗口中填写数据库连接的基本信息,并生存:

然后再次在IDEA顶部菜单栏中,找到Tools菜单,选择Code Generator:

在弹出的窗口中填写要生成的代码的基本信息:

点击code generatro,即可在指定包下生成对应的类:

当然生成的代码并一定完全符合要求,这时只必要根据要求做简单地修改即可,比全部手写省事多了。
3.2 静态工具

3.2.1 基本用法

有的时候Service之间也会相互调用,但很有可能会出现循环依赖问题。
为了制止这个问题,MybatisPlus提供一个静态工具类:Db类,它其中的一些静态方法与IService中的方法基本同等,也可以实现CRUD功能:

其基本使用方法如下:
  1. @Test
  2. public void testDb() {
  3.     // 利用Db实现根据ID查询
  4.     User user = Db.getById(1L, User.class);
  5.     System.out.println(user);
  6.     System.out.println("==========");
  7.     // 利用Db实现复杂条件查询
  8.     List<User> userList = Db.lambdaQuery(User.class)
  9.             .like(User::getUsername, "o")
  10.             .ge(User::getBalance, 1000)
  11.             .list();
  12.     userList.forEach(System.out::println);
  13.     System.out.println("==========");
  14.     // 利用Db实现条件更新
  15.     Db.lambdaUpdate(User.class)
  16.             .set(User::getBalance, 2000)
  17.             .eq(User::getUsername, "Rose")
  18.             .update();
  19. }
复制代码
执行以上单元测试,控制台打印信息如下:
  1. ==>  Preparing: SELECT id,username,password,phone,info,status,balance,create_time,update_time FROM t_user WHERE id=?
  2. ==> Parameters: 1(Long)
  3. <==      Total: 1
  4. User(id=1, username=Jack, password=123, phone=13900112224, info={"age": 20, "intro": "佛系青年", "gender": "male"}, status=1, balance=1600, createTime=2024-04-22T19:40:36, updateTime=2024-04-22T19:40:36)
  5. ==========
  6. ==>  Preparing: SELECT id,username,password,phone,info,status,balance,create_time,update_time FROM t_user WHERE (username LIKE ? AND balance >= ?)
  7. ==> Parameters: %o%(String), 1000(Integer)
  8. <==      Total: 1
  9. User(id=3, username=Hope, password=123, phone=13900112222, info={"age": 25, "intro": "上进青年", "gender": "male"}, status=1, balance=100000, createTime=2024-04-22T19:40:36, updateTime=2024-04-22T19:40:36)
  10. ==========
  11. ==>  Preparing: UPDATE t_user SET balance=? WHERE (username = ?)
  12. ==> Parameters: 2000(Integer), Rose(String)
  13. <==    Updates: 1
复制代码
3.2.2 代码实例

下面实现一个需求:改造根据id查询用户信息的接口,在查询用户的同时返回用户收货地点列表。


  • 1)修改User对象,添加收货地点列表属性:
  1. // com.star.learning.pojo.User
  2. @Data
  3. @TableName("t_user")
  4. public class User {
  5.     // ...
  6.     /**
  7.      * 收货地址列表
  8.      */
  9.     @TableField(exist = false)
  10.     private List<Address> addressList;
  11. }
复制代码


  • 2)修改UserController中根据id查询用户信息的getById()方法:
  1. // com.star.learning.controller.UserController
  2. @GetMapping("/{id}")
  3. public User getById(@PathVariable("id") Long userId) {
  4.     // User user = userService.getById(userId);
  5.     // System.out.println("根据id查询用户 => " + user);
  6.     // 基于自定义Service方法查询
  7.     User user = userService.queryUserAndAddressById(userId);
  8.     System.out.println("根据id查询用户及其收货地址信息 => " + user);
  9.     return user;
  10. }
复制代码


  • 3)在IService接口中定义queryUserAndAddressById()方法,并在UserServiceImpl类中详细实现:
  1. // com.star.learning.service.IUserService
  2. User queryUserAndAddressById(Long userId);
复制代码
  1. // com.star.learning.service.impl.UserServiceImpl
  2. @Override
  3. public User queryUserAndAddressById(Long userId) {
  4.     // 1.查询用户
  5.     User user = getById(userId);
  6.     if (user == null) {
  7.         return null;
  8.     }
  9.     // 2.使用Db来查询收货地址
  10.     List<Address> addressList = Db.lambdaQuery(Address.class)
  11.             .eq(Address::getUserId, userId)
  12.             .list();
  13.     // 3.处理返回
  14.     user.setAddressList(addressList);
  15.     return user;
  16. }
复制代码


  • 4)调用/user/1接口,控制台打印信息如下:
  1. ==>  Preparing: SELECT id,username,password,phone,info,status,balance,create_time,update_time FROM t_user WHERE id=?
  2. ==> Parameters: 1(Long)
  3. <==      Total: 1
  4. ==>  Preparing: SELECT id,user_id,province,city,town,mobile,street,contact,is_default,notes,deleted FROM t_address WHERE (user_id = ?)
  5. ==> Parameters: 1(Long)
  6. <==      Total: 2
  7. 根据id查询用户及其收货地址信息 => User(id=1, username=Jack, password=123, phone=13900112224, info={"age": 20, "intro": "佛系青年", "gender": "male"}, status=1, balance=1600, createTime=2024-04-22T19:40:36, updateTime=2024-04-22T19:40:36, addressList=[Address(id=2, userId=1, province=北京, city=北京, town=朝阳区, mobile=13700221122, street=修正大厦, contact=Jack, isDefault=false, notes=null, deleted=false), Address(id=3, userId=1, province=上海, city=上海, town=浦东新区, mobile=13301212233, street=航头镇航头路, contact=Jack, isDefault=true, notes=null, deleted=false)])
复制代码
可见,在查询地点时,采用了Db类的静态方法,制止了注入AddressService,从而减少了循环依赖的风险。
3.3 逻辑删除

对于一些比较重要的数据,每每会采用逻辑删除的方案,即在表中添加一个字段标志数据是否被删除,当删除数据时把标志置为true,当查询时过滤掉标志为true的数据。
但是一旦采用逻辑删除,查询和删除逻辑就会变得更加复杂。为此,MybatisPlus添加了对逻辑删除的支持。
但是要注意,只有MybatisPlus生成的SQL语句才支持自动的逻辑删除,自定义SQL必要自己手动处置惩罚逻辑删除。
例如,在t_address表及其对应的实体Address类中有一个逻辑删除字段deleted,默认值为0:

要开启MyBatisPlus的逻辑删除功能,必要在application.yml中设置逻辑删除字段:
  1. # src/main/resources/application.yaml
  2. mybatis-plus:
  3.   global-config:
  4.     db-config:
  5.       logic-delete-field: deleted # 全局逻辑删除的实体字段名
  6.       logic-delete-value: 1 # 逻辑已删除值(默认为 1)
  7.       logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
复制代码
接下来编写测试代码:
  1. @Test
  2. public void testDeleteByLogic() {
  3.     addressService.removeById(1);
  4. }
复制代码
执行以上单元测试,控制台打印信息如下:
  1. ==>  Preparing: UPDATE t_address SET deleted=1 WHERE id=? AND deleted=0
  2. ==> Parameters: 1(Integer)
  3. <==    Updates: 1
复制代码
可见,SQL语句并不是DELETE语句,而是UPDATE语句,将deleted字段设置为1。
再来测试以下查询使用:
  1. @Test
  2. void testQuery() {
  3.     List<Address> list = addressService.list();
  4.     list.forEach(System.out::println);
  5. }
复制代码
执行以上单元测试,控制台打印信息如下:
  1. ==>  Preparing: SELECT id,user_id,province,city,town,mobile,street,contact,is_default,notes,deleted FROM t_address WHERE deleted=0
  2. ==> Parameters:
  3. <==      Total: 10
复制代码
可见,SQL语句自动添加了一个条件:WHERE deleted=0。
总的来说,开启了逻辑删除功能以后,我们就可以像普通删除一样做CRUD,基本不用思量代码逻辑问题。
同时,逻辑删除自己也是有问题的,比如:


  • 会导致数据库表垃圾数据越来越多,从而影响查询效率;
  • SQL中全都必要对逻辑删除字段做判断,影响查询效率。

本节完,更多内容请查阅分类专栏:MyBatisPlus详解
本文涉及代码下载地点:https://gitee.com/weidag/mybatis_plus_learning.git
感兴趣的读者还可以查阅我的另外几个专栏:


  • SpringBoot源码解读与原理分析(已完结)
  • MyBatis3源码深度解析(已完结)
  • Redis从入门到精通(已完结)
  • 再探Java为面试赋能(持续更新中…)

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

知者何南

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表