代码生成
在使用MybatisPlus以后,基础的Mapper
、Service
、PO
代码相对固定,重复编写也比较麻烦。因此MybatisPlus官方提供了代码生成器根据数据库表结构生成PO
、Mapper
、Service
等相关代码。只不过代码生成器同样要编码使用,也很麻烦。
这里推荐大家使用一款MybatisPlus
的插件,它可以基于图形化界面完成MybatisPlus
的代码生成,非常简单。
安装插件
在Idea
的plugins市场中搜索并安装MyBatisPlus
插件:
然后重启你的Idea即可使用。
使用
刚好数据库中还有一张address表尚未生成对应的实体和mapper等基础代码。我们利用插件生成一下。 首先需要配置数据库地址,在Idea顶部菜单中,找到Tools
,选择Config Database
:
在弹出的窗口中填写数据库连接的基本信息:
点击OK保存。
然后再次点击Idea顶部菜单中的other,然后选择Code Generator
:
在弹出的表单中填写信息:
最终,代码自动生成到指定的位置了:
静态工具
有的时候Service之间也会相互调用,比如某些业务在UserService
中实现,查adderss信息需要注入AddressService
。有些业务在AddressService
中实现,查询user信息需要注入UserService
,两个Service相互注入就会出现循环依赖的情况。为了避免出现循环依赖
问题,MybatisPlus提供一个静态工具类
:Db
,其中的一些静态方法与IService
中方法签名基本一致,也可以帮助我们实现CRUD功能。与IService接口的主要区别是,删除和查询除了传id集合还需要传Class掩码
指定对象类型完成反射
。只有save和update不需要传,因为save和update直接传送对象,对象都有了不需要字节码了:
示例:
@Test
void testDbGet() {User user = Db.getById(1L, User.class);System.out.println(user);
}@Test
void testDbList() {// 利用Db实现复杂条件查询List<User> list = Db.lambdaQuery(User.class).like(User::getUsername, "o").ge(User::getBalance, 1000).list();list.forEach(System.out::println);
}@Test
void testDbUpdate() {Db.lambdaUpdate(User.class).set(User::getBalance, 2000).eq(User::getUsername, "Rose");
}
需求:
改造根据id用户查询的接口,查询用户的同时返回用户收货地址列表。
首先,我们要添加一个收货地址的VO对象:
package com.itheima.mp.domain.vo;import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;@Data
@ApiModel(description = "收货地址VO")
public class AddressVO{@ApiModelProperty("id")private Long id;@ApiModelProperty("用户ID")private Long userId;@ApiModelProperty("省")private String province;@ApiModelProperty("市")private String city;@ApiModelProperty("县/区")private String town;@ApiModelProperty("手机")private String mobile;@ApiModelProperty("详细地址")private String street;@ApiModelProperty("联系人")private String contact;@ApiModelProperty("是否是默认 1默认 0否")private Boolean isDefault;@ApiModelProperty("备注")private String notes;
}
然后,改造原来的UserVO,添加一个地址属性:
接下来,修改UserController中根据id查询用户的业务接口:
@GetMapping("/{id}")
@ApiOperation("根据id查询用户")
public UserVO queryUserById(@PathVariable("id") Long userId){// 基于自定义service方法查询return userService.queryUserAndAddressById(userId);
}
由于查询业务复杂,所以要在service层来实现。首先在IUserService
中定义方法:
package com.itheima.mp.service;import com.baomidou.mybatisplus.extension.service.IService;
import com.itheima.mp.domain.po.User;
import com.itheima.mp.domain.vo.UserVO;public interface IUserService extends IService<User> {void deduct(Long id, Integer money);UserVO queryUserAndAddressById(Long userId);
}
然后,在UserServiceImpl
中实现该方法:
@Override
public UserVO queryUserAndAddressById(Long userId) {// 1.查询用户User user = getById(userId);if (user == null || user.getStatus() == 2) {throw new RuntimeException("用户状态异常!");}// 2.查询收货地址List<Address> addresses = Db.lambdaQuery(Address.class).eq(Address::getUserId, userId).list();// 3.处理vo// 转user的PO为VOUserVO userVO = BeanUtil.copyProperties(user, UserVO.class);// 转地址的VOif(Collutil.isNotEmpty(addresses)){userVO.setAddresses(BeanUtil.copyToList(addresses, AddressVO.class));}return userVO;
}
在查询地址时,我们采用了Db的静态方法,因此避免了注入AddressService
,减少了循环依赖的风险。
再来实现一个功能:
根据id批量查询用户,并查询出用户对应的所有地址。
UserController:
@ApiOperation("根据id批量查询用户")@GetMappingpublic List<UserVo> queryUserById(@ApiParam("用户id集合") @RequestParam("ids") List<Long> ids){// 根据service方法实现查询return userService.queryUserAndAddressByIds(ids);}
IUserService:
List<UserVo> queryUserAndAddressByIds(List<Long> ids);
UserServiceImpl:
@Overridepublic List<UserVo> queryUserAndAddressByIds(List<Long> ids) {// 1.批量查询用户信息List<User> users = listByIds(ids);if(users.isEmpty(users)){return null;}// 2.根据id查询用户的收货地址信息List<UserVo> userVos = users.stream().map(user -> {// 将user拷贝到voUserVo userVO = BeanUtil.copyProperties(user, UserVo.class);List<Address> addressList = Db.lambdaQuery(Address.class).eq(Address::getUserId, user.getId()).list();userVO.setAddress(BeanUtil.copyToList(addressList, AddressVO.class));return userVO;}).collect(Collectors.toList());return userVos;}
在循环里面查询的效率很低,可以批量分开查询。
@Overridepublic List<UserVo> queryUserAndAddressByIds(List<Long> ids) {// 1.批量查询用户信息List<User> users = listByIds(ids);if(users.isEmpty(users)){return Collections.emptyList();}// 2.查询地址// 2.1 获取用户id集合List<Long> ids = users.stream().map(User::getId).collect(Collectors.toList());// 2.2 根据用户id查询地址List<Address> address = Db.lambdaQuery(Adress.class).in(Address::getUserId, userIds).list();// 2.3 转换地址VOList<AdressVO> addressVOList = BeanUtil.copyTolist(addresss,AddressVO.class);// 2.4用户地址集合分组处理,相同用户的放入一个集合(组)中// 创建一个空的Map集合Map<Long,List<AddressVO>> addressMap = new HashMap<>(0);// 如果地址VO集合不为空,需要根据id分组转换到Map集合中。if(CollUtil.isNotEmpty(addressVOList)){// 地址Map中,用户id为键,对应的地址集合为值。addressMap = addressVOList.stream().collect(Collectors.groupingBy(AdressVO::getUserId));}// 3.转换VO返回List<UserVO> list = new ArrayList<>(users,size());for(User user : users){// 3.1 转换User的PO为VOUserVO vo = BeanUtil.copyProperties(user, UserVO.class);list.add(vo);// 3.2 转换地址VOvo.setAddresses(addressMap.get(user.getId());}return userVos;}
逻辑删除
对于一些比较重要的数据,我们往往会采用逻辑删除的方案,即:
- 在表中添加一个字段标记数据是否被删除
- 当删除数据时把标记置为true
- 查询时过滤掉标记为true的数据
逻辑删除字段的为deleted:
- 删除操作为
UPDATE user SET deleted = 1 WHERE id = 1 AND deleted = 0
- 查询操作:
SELECT * FROM user WHERE deleted = 0
一旦采用了逻辑删除,所有的查询和删除逻辑都要跟着变化,非常麻烦。
为了解决这个问题,MybatisPlus
就添加了对逻辑删除的支持。
注意,只有MybatisPlus
生成的SQL语句才支持自动的逻辑删除,自定义SQL需要自己手动处理逻辑删除。
添加逻辑删除字段
例如,我们给address
表添加一个逻辑删除字段:
alter table address add deleted bit default b'0' null comment '逻辑删除';
然后给Address
实体添加deleted
字段:
配置逻辑删除字段
接下来,我们要在application.yml
中配置逻辑删除字段:
mybatis-plus:type-aliases-package: com.itheima.mp.domain.poglobal-config:db-config:id-type: autologic-delete-field: deleted #配置逻辑删除字段
测试
创建IAddressService
接口的测试类,并执行一个删除操作:
@SpringBootTest
class IAddressServiceTest {@Autowiredprivate IAddressService addressService;// 逻辑删除@Testpublic void testLogicDelete() {// 1.执行逻辑删除操作addressService.removeById(60L);// 2. 执行查询操作addressService.getById(60L);}
}
方法与普通删除一模一样,但是底层的SQL逻辑变了:
综上, 开启了逻辑删除功能以后,我们就可以像普通删除一样做CRUD,基本不用考虑代码逻辑问题。还是非常方便的。
注意: 逻辑删除本身也有自己的问题,比如:
- 会导致数据库表垃圾数据越来越多,从而影响查询效率
- SQL中全都需要对逻辑删除字段做判断,影响查询效率
因此,我不太推荐采用逻辑删除功能,如果数据不能删除,可以采用把数据迁移到其它表的办法。
通用枚举
User类中有一个用户状态字段:
像这种字段我们一般会定义一个枚举,做业务判断的时候就可以直接基于枚举做比较。但是我们数据库采用的是int
类型,对应的PO也是Integer
。因此业务操作时必须手动把枚举
与Integer
转换,非常麻烦。
因此,MybatisPlus提供了一个处理枚举
的类型转换器,可以帮我们把枚举类型与数据库类型自动转换。
定义枚举
我们定义一个用户状态的枚举:
代码如下:
package com.itheima.mp.enums;import com.baomidou.mybatisplus.annotation.EnumValue;
import lombok.Getter;@Getter
public enum UserStatus {NORMAL(1, "正常"),FROZEN(2, "冻结");private final int value;private final String desc;UserStatus(int value, String desc) {this.value = value;this.desc = desc;}
}
然后把User
类中的status
字段改为UserStatus
类型:
枚举是直接可以使用==
比较的,比较status值的时候就可以看user的status是否为NORMAL
或者FREEZE
。这样就代码中不会有数字代表状态,使用英文单词代表,代码的可读性也就更好。
数据库表中存储的status依然存储的是INT类型
,但是PO是枚举类型
。假如有个枚举对象,里面状态是NORMAL
,在save
的时候往数据库写的时候不能写NORMAL
。反之,查询数据库查到的是INT类型
,但是转成PO的时候要用枚举
。这就存在Java中的枚举类型与数据库中INT类型的相互转换,不光是枚举类型,Java中的所有类型都需要与数据库中的类型进行转换。这些所有的转换底层都是由Mybatis
做的,在Mybatis中有个TypeHandler类型处理器
,在ibatis
包下,里面由很多的数据类型转换的实现。MP在mybatis的基础上做了拓展,加入了MybatisEnumTypeHandler
枚举类型处理器以及AbstractJsonTypeHandler
的JSON类型处理器。
要让MybatisPlus
处理枚举与数据库类型自动转换,我们必须告诉MybatisPlus
,枚举中的哪个字段的值作为数据库值。 MybatisPlus
提供了@EnumValue
注解来标记枚举属性:
配置枚举处理器
在application.yaml
文件中添加配置,使枚举处理器生效:
mybatis-plus:configuration:default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler
测试
需要将其他判断状态的字段都改为枚举类型,同时将返回类型UserVO也改为枚举类型。重新启动程序,前端查看status的返回值。
JsonValue注解
查到的结果是NORMAL
和FROZEN
,证明成功查询,枚举的处理也自动处理好了。如何让其返回1、2
或者正常、冻结
。这与返回值有关,目前返回值改为枚举类型了,枚举在JSON处理
的时候,默认以英文名
返回。要想让其以value
或者desc
返回,就需要告诉SpringMVC
因为程序的数据往前端返回时是SpringMVC
处理的。SpringMVC
在处理JSON
时,底层用的是jackson json
包。jackson json
提供一些注解用于标记枚举里面的值,把谁返回。添加注解@JsonValue
在desc
上就返回desc的值
,加在value
上就返回value的值
。
@Test
void testService() {List<User> list = userService.list();list.forEach(System.out::println);
}
总结
如何实现PO类中的枚举类型变量与数据库字段的转换?
- 给枚举中的与数据库对应value值添加
@EnumValue
注解
- 在配置文件中配置通用的枚举处理器,实现类型转换
枚举在给前端返回的时候默认返回枚举项的名字,不够友好,可以通过@JsonValue
自定义返回的值。
JSON类型处理器
MP除了提供枚举类型处理器外,还提供了JSON类型处理器,它就是解决数据库中JSON类型与Java类型的转换的。
数据库的user表中有一个info
字段,是JSON类型:
格式像这样:
{"age": 20, "intro": "佛系青年", "gender": "male"}
在Java中没有JSON
这种数据类型,一般在Java中都是用String类型
接收。
而目前User
实体类中却是String
类型:
在这种情况下不需要做任何操作,Mybatis
就能自动进行Java中字符串
与MySQL
里面的JSON
处理。但是在处理业务的时候就比较困难,比如从数据库查出Json
数据之后,想要取出某个字段信息就不行,因为在java中是字符串接收的。这样一来,我们要读取info中的属性时就非常不方便。如果要方便获取,info的类型最好是一个Map
或者实体类。
而一旦我们把info
改为对象
类型,就需要在写入数据库时手动转为String
,再读取数据库时,手动转换为对象
,这会非常麻烦。
因此MybatisPlus提供了很多特殊类型字段的类型处理器,解决特殊字段类型与数据库类型转换的问题。例如处理JSON就可以使用JacksonTypeHandler
处理器。
接下来,我们就来看看这个处理器该如何使用。
定义实体
首先,我们定义一个单独实体类来与info字段的属性匹配:
代码如下:
package com.itheima.mp.domain.po;import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;@Data
@AllArgsConstructor(staticName = "of")
@NoArgsConstructor
public class UserInfo {private Integer age;private String intro;private String gender;
}
使用JSON类型处理器
MP并没有提供在Application.yaml
文件里面的全局配置,需要用@TableField
注解来处理,在表字段里面设置一个typeHandle
r属性,在这里指定JacksonTypeHandler
,这种配置只针对当前字段有效,如果有好多字段就需要分别指定。同时需要注意将info的类型从String
改为UserInfo
。
开启自动ResultMap映射
这是会出现对象的嵌套,User对象里面嵌套了另外一个对象UserInfo
,一般对象嵌套都需要定义复杂的resultMap。如果不想定义,就需要在TableName注解上加入属性autoResultMap,默认是关闭的需要开启。
此时对info赋值需要将之前的json
赋值改为用UserInfo
里面的静态方法
构建。主要主要使用的UserInfo
不是糊涂包里的,而是自定义的。
注意UserVO
里面也有info属性,需要将其从String
类型转为UserInfo
类型。
测试
修改之前的查询结果为json格式
修改后的查询结果为UserInfo
对象