1 MybatisPlus
MybatisPlus 是基于 MyBatis 的增强工具,它简化了 MyBatis 的开发,并提供了一些常用的自动化功能,如 CRUD 操作的自动生成
MybatisPlus 的目标是使得开发者 不再编写重复的 SQL 语句,同时保留 MyBatis 的原有功能,使得 MyBatis 更加轻量、简洁、高效
设计表结构:
balance n.用户余额
常规写mybatis的增删改查:
@Insert("insert into spring_cloud.user" +"(username, password, phone, balance, info)" +"values" +"(#{username},#{password},#{phone},#{balance},#{info})")void saveUser(User user);
很繁琐
1.1 使用 MP
使用 mybatis plus(MP):
1.引入依赖
注:mp 依赖包含了 mp 和 mybatis,所以可以删除 mybatis 的依赖
<dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.5.3.1</version>
</dependency>
2.mapper 层继承 BaseMapper<T>
这样 mapper 就拥有了 BaseMapper 内置的增删改查方法,简单的方法可以从 BaseMapper 直接继承使用
3.service 实现类中直接调用
1.2 常用注解
MybatisPlus 可以自动增删改查的原理:
通过扫描实体类,并基于反射获取实体类信息作为数据库表信息,然后通过 约定 获得数据库字段信息
约定大于配置:
- 类名驼峰转下划线作为表名
- 名为 id 的字段作为主键
- 变量名驼峰转下划线作为字段名
如果不遵守约定,可用注释:
1.3 配置
1.4 条件构造器
BaseMapper 接口含有一些方法,形参 Wrapper 是条件构造器
wrapper 包装器
Wrapper 的继承关系:
1.4.1 QueryWrapper 查询
查找名字带"o",余额大于等于 1000 元的 id、usernam、info、balance 字段
- eq:等于
ne
:不等于ge
:大于等于gt
:大于le
:小于等于lt
:小于
1.4.2 UpdateWrapper 更新
更新 jack 的余额为 2000
更新方法的传参有两个:一个是 用户实体,一个是 updateWrapper
更新 id 为 1, 2, 3 的用户,余额扣 200
用了 setsql 和 in
注意可以把实体对象的传参写为 null
1.5 自定义 SQL
更新 id 为 1, 2, 3 的用户,余额扣 200
上一节的用法需要在 service 中写 sql 语句,不符合规范
改进:
1.6 IService 接口
Mybatis 提供了 IService 和 其实现类 ServiceImpl
自定义的 UserService 继承 IService
自定义的 UserServiceImpl 继承 ServiceImpl
UserService 继承 IService ,要加上泛型
UserServiceImpl 先继承 Service 然后实现 UserService,对于 IService 的实现在 ServiceImpl 中继承过来了
UserMapper 必须继承 BaseMapper
然后就可以直接通过注入 UserService ,直接调用方法
1.7 案例
使用 构造函数,而不是使用字段注入:
private final UserService userService;public UserController(UserService userService) {this.userService = userService;
}
使用 lombok简化:
使用 @RequiredArgsConstructor 注解时,Lombok 会自动生成一个包含所有
final
字段和使用@NonNull
注解标记的字段的构造函数
新增用户
传递的是 DTO 对象,通过 BeanUtil 复制到 user,然后往 service 传递的是实体类对象 user
这里用的BeanUtil工具类需要引入依赖:
<dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>5.8.20</version> </dependency>
@PostMapping
@ApiOperation("新增用户")
public Result saveUser(@RequestBody UserDTO userDTO) {User user = new User();BeanUtil.copyProperties(userDTO, user);userService.save(user);return Result.success();
}
在 service 和 mapper 层都不需要写任何东西,因为 IService 已提供具体实现
根据 id 扣减余额(自定义)
controller
@ApiOperation("根据id扣减余额")
@PutMapping("/{id}/deduction/{money}")
public Result balanceMinusById(@PathVariable("id") Long id,@PathVariable("money") Double money){userService.balanceMinusById(id,money);return Result.success();
}
service
先用内置的 getById 获得 user,然后进行非法判断
用 updateWrapper 封装条件
update 有两种重载方法:
其中一种方法直接传入装饰器 updateWrapper,这种方法需要在构造的时候使用 set,指定要更新的操作,通过 User 的 get 方法获得余额
public void balanceMinusById(Long id, Double money) {User user = getById(id);if (user == null) {throw new RuntimeException("用户不存在");}if (user.getBalance() < money) {throw new RuntimeException("用户余额不足");}UpdateWrapper<User> updateWrapper = new UpdateWrapper<User>().set("balance", user.getBalance() - 200).eq("id", id);update(updateWrapper);}
1.8 条件查询
controller
@GetMapping("/query")@ApiOperation("条件查询")public Result<List<User>> queryCondition(UserQueryDTO userQueryDTO){List<User> userList = userService.queryCondition(userQueryDTO);return Result.success(userList);}
service
这里使用 lambdaQuery
public List<User> queryCondition(UserQueryDTO userQueryDTO) {String username = userQueryDTO.getUsername();Integer status = userQueryDTO.getStatus();Integer minBalance = userQueryDTO.getMinBalance();Integer maxBalance = userQueryDTO.getMaxBalance();return lambdaQuery().like(username != null, user -> user.getUsername(), username).eq(status != null, User::getStatus, status).ge(minBalance != null, User::getBalance, minBalance).le(maxBalance!=null, User::getBalance, maxBalance).list();}
1.9 DB 静态工具类
DB 是 mp 的静态工具类,为了防止 循环依赖
使用时可以不注入 mapper
循环依赖(Circular Dependency)是指两个或多个对象(类、模块、Bean 等)相互依赖,形成一个循环的依赖关系。具体来说,如果对象 A 依赖对象 B,而对象 B 又依赖对象 A,这种情况就会导致循环依赖问题。循环依赖可能导致系统无法正确初始化,尤其是在依赖注入框架(如 Spring)中,会抛出异常,无法实例化相关的对象
加入一张 address 表
在 UserVO 中加入
private List<AddressVO> addressList;
1.9.1 查询单个用户及其地址
现在对代码进行改造,根据id查询用户,返回用户信息,也包括对应的 地址 信息
注意,这里很重要
需要操作什么实体类,从而访问对应的表,需要 在mapper 创建一个对应的接口
这个接口要继承 BaseMapper,这样才具有 mp 的方法
这里不用加 @Mapper 的原因是在启动类上加了扫描Mapper的注解:
controller
@GetMapping("/{id}")@ApiOperation("根据id查询用户")public Result<UserVO> getUserById(@PathVariable Long id) {User user = userService.getById(id);UserVO userVO = new UserVO();
// log.info("UserVO.class:\n"+String.valueOf(UserVO.class)); // class com.wyn.springcloud.pojo.vo.UserVOreturn Result.success(BeanUtil.copyProperties(user, UserVO.class)); // 反射 UserVO.class}
service
这里使用 Db 的 lambdaQuery 方法,传参是查询的实体类的 字节码(反射)
根据 userid 查询,并且转为 list
public UserVO queryUserAndAddress(Long id) {User user = getById(id);if (user == null || user.getStatus() == 0) {throw new RuntimeException("用户异常!");}List<Address> addressList = Db.lambdaQuery(Address.class).eq(Address::getUserId, id).list();UserVO userVO = BeanUtil.copyProperties(user, UserVO.class);userVO.setAddressList(BeanUtil.copyToList(addressList, AddressVO.class));return userVO;}
1.9.2 查询多个用户及其地址
思路:
根据ids获得users,然后遍历users,根据id查询address,复制到addressvo,user复制到uservo,然后 uservo.setAddress
/*** batch query [users] and [addresses] by ids** @param ids* @return*/public List<UserVO> queryUsersAndAddress(List<Long> ids) {List<User> users = listByIds(ids);if (users.isEmpty()) {throw new RuntimeException("user table is null!");}users.forEach(user -> {if (user.getStatus() == 0) {users.remove(user);}});if (users.isEmpty()) {throw new RuntimeException("users are frozen");}// HashMap<Long,List<Address>> addressListMap = new HashMap<>();// We don't use HashMap here, because the hash table is 1 to 1, and here it's 1 to manyList<UserVO> userVOList = new ArrayList<>();for (User user : users) {List<Address> addressList = Db.lambdaQuery(Address.class).eq(Address::getUserId, user.getId()).list();UserVO userVO = BeanUtil.copyProperties(user, UserVO.class);List<AddressVO> addressVOList = BeanUtil.copyToList(addressList, AddressVO.class);userVO.setAddressList(addressVOList);userVOList.add(userVO);}return userVOList;}
1.10 逻辑删除
逻辑删除 是一种软删除策略,即在数据库中并不直接物理删除数据行,而是通过设置某个 标志位 来标记该数据已被删除。这样做的好处是,数据不会从数据库中被永久删除,仍然可以恢复或用于历史查询
在 user 中加入 deleted 字段(注意:delete 是保留字,不可以作为字段)
在 user 实体类,指定逻辑字段
1.11 枚举处理器
配置枚举处理器:
枚举类
@EnumValue
是 MyBatis-Plus 提供的注解,用于在将枚举类型的值存储到数据库时,自定义枚举的存储和映射行为。具体来说,@EnumValue
可以指定将枚举类中的某个字段作为存储到数据库的值,而不是默认使用枚举的名称或 ordinal 值
@JsonValue
是 Jackson 序列化和反序列化过程中使用的注解之一,它用于指定某个类的方法或字段的返回值作为该类的 JSON 表示。也就是说,使用@JsonValue
可以将对象序列化为一个单一的值,而不是通常的对象格式
需要进行 status 判断时,使用枚举类
1.12 JSON 处理器
1.13 分页查询
Method: GET
Path: /users/page
args: pageNum, pageSize, soryBy, isAsc, name, status
这里[再次]总结一下三种传参
1. 查询参数(Query Parameters)@RequestParm
请求示例:/users?name=John&age=25
2.JSON 方法体参数 @RequestBody
3.路径参数(Path Parameters)@PathVariable (用于参数名和变量名不一致的情况)
请求示例:/users/{id}
// 通用分页查询实体类 @Data @ApiModel(description = "分页查询实体类") public class PageQuery {@ApiModelProperty("页码")private Integer pageNo;@ApiModelProperty("页数")private Integer pageSize;@ApiModelProperty("排序字段")private String sortBy;@ApiModelProperty("是否升序")private Boolean isAsc; } // 用户查询实体类 @ApiModel(description = "user condition select entity") @Data public class UserQuery extends PageQuery{ // 继承通用类private String name;private Integer status;private Integer minBalance;private Integer maxBalance; } // 分页传输类 @Data @ApiModel(description = "分页结果") public class PageDTO<T> {@ApiModelProperty("总条数")private Integer total;@ApiModelProperty("总页数")private Integer pages;@ApiModelProperty("数据集合")private List<T> list; }
// Controller @ApiOperation("条件分页查询") @GetMapping("/page") public Result<PageDTO<UserVO>> queryUserPage(UserQuery userQuery){return Result.success(userService.queryUserPage(userQuery)); } // Service public PageDTO<UserVO> queryUserPage(UserQuery userQuery) {String name = userQuery.getName();Integer status = userQuery.getStatus();Integer minBalance = userQuery.getMinBalance();Integer maxBalance = userQuery.getMaxBalance(); Integer pageNo = userQuery.getPageNo();Integer pageSize = userQuery.getPageSize();String sortBy = userQuery.getSortBy();Boolean isAsc = userQuery.getIsAsc(); Page<User> page = Page.of(pageNo, pageSize); OrderItem orderItem = new OrderItem();// 排序字段非空才可以排序if (!sortBy.isEmpty()) {orderItem.setColumn(sortBy);orderItem.setAsc(isAsc);}page.addOrder(orderItem); Page<User> p = lambdaQuery().like(name != null, User::getUsername, name).eq(status != null, User::getStatus, status).ge(minBalance != null, User::getBalance, minBalance).ge(maxBalance != null, User::getBalance, maxBalance).page(page); // 构造返回结果PageDTO<UserVO> dto = new PageDTO<>();dto.setTotal(p.getTotal());dto.setPages(p.getPages());List<User> userList = p.getRecords();if (userList.isEmpty()) {dto.setList(null);} else {List<UserVO> userVOList = BeanUtil.copyToList(userList, UserVO.class);dto.setList(userVOList);}return dto; }
代码可以抽取,作为 PageQuery
和 PageDTO
的方法 -> 复用
这里为什么要返回
PageDTO
而不是Page
——mybatis 的分页类因为 有些字段不需要
public class Page<T> implements IPage<T> {private static final long serialVersionUID = 8545996863226528798L;protected List<T> records;protected long total;protected long size;protected long current;protected List<OrderItem> orders;protected boolean optimizeCountSql;protected boolean searchCount;protected boolean optimizeJoinOfCountSql;protected Long maxLimit;protected String countId;// ... }