一、基本概念
MyBatis-Plus (opens new window)(简称 MP)是一个 MyBatis (opens new window)的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。
本文制作一些要点的说明,基本的使用参见官方文档
中文文档:https://mp.baomidou.com/guide/
特性
- 无侵入:只做增强不做改变,引入它不会对现有工程产生影响,如丝般顺滑
- 损耗小:启动即会自动注入基本 CURD,性能基本无损耗,直接面向对象操作
- 强大的 CRUD 操作:内置通用 Mapper、通用 Service,仅仅通过少量配置即可实现单表大部分 CRUD 操作,更有强大的条件构造器,满足各类使用需求
- 支持 Lambda 形式调用:通过 Lambda 表达式,方便的编写各类查询条件,无需再担心字段写错
- 支持主键自动生成:支持多达 4 种主键策略(内含分布式唯一 ID 生成器 - Sequence),可自由配置,完美解决主键问题
- 支持 ActiveRecord 模式:支持 ActiveRecord 形式调用,实体类只需继承 Model 类即可进行强大的 CRUD 操作
- 支持自定义全局通用操作:支持全局通用方法注入( Write once, use anywhere )
- 内置代码生成器:采用代码或者 Maven 插件可快速生成 Mapper 、 Model 、 Service 、 Controller 层代码,支持模板引擎,更有超多自定义配置等您来使用
- 内置分页插件:基于 MyBatis 物理分页,开发者无需关心具体操作,配置好插件之后,写分页等同于普通 List 查询
- 分页插件支持多种数据库:支持 MySQL、MariaDB、Oracle、DB2、H2、HSQL、SQLite、Postgre、SQLServer 等多种数据库
- 内置性能分析插件:可输出 Sql 语句以及其执行时间,建议开发测试时启用该功能,能快速揪出慢查询
- 内置全局拦截插件:提供全表 delete 、 update 操作智能分析阻断,也可自定义拦截规则,预防误操作
二、MyBatisPlus引入
1、pom
<!--mybatis-plus-->
<dependencies>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId> <version>3.0.5</version>
</dependency>
</dependencies>
<build>
<resources>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.xml</include>
</includes>
<filtering>false</filtering>
</resource>
</resources>
</build>
2、application.yml
当springboot的版本为2.1.0以上时,MySQL配置如下
driver加个cj,url添加时区属性
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/farming_test??useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT%2B8
username: root
password: root
mybatis-plus:
#mybatis日志 ,用于查看sql输出日志,sql语句
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
# xml文件所在路径
mapper-locations: classpath:/mapper/**/*.xml
# 全局主键生成策略
global-config:
db-config:
id-type: auto
logic-delete-value: 1
logic-not-delete-value: 0
#返回json的全局时间格式
spring.jackson.date-format=yyyy-MM-dd HH:mm:ss
spring.jackson.time-zone=GMT+8
#配置mapper xml文件的路径
mybatis-plus.mapper-locations=classpath:com/rewind/ucenter/mapper/xml/*.xml
3、主启动类
@SpringBootApplication
// 注意MapperScan的扫描范围不能覆盖其他接口,否则MP也会误认为其他接口也是Mapper接口,也对其进行包装
@MapperScan("com.rewind.mapper")
public class GraduationProjectApplication {
public static void main(String[] args) {
SpringApplication.run(GraduationProjectApplication.class, args);
}
}
4、实体类
数据库中的下划线_
改为驼峰命名法
@Data
public class User {
//order_id
private Long orderId;
}
5、mapper
与MyBatis不同,需要继承BaseMapper
注:接口与接口之间是继承,类与接口是实现。
IDEA在 userMapper 处报错,因为找不到注入的对象,因为类是动态创建的,但是程序可以正确的执行。
只需在UserMapper加上注解@Repository
。
@Repository
public interface UserMapper extends BaseMapper<User> {
}
6、service
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements DemoService {
}
三、主键策略
设置方式
在实体类的主键属性上添加注解
@TableId(type = IdType.AUTO)
private Long id;
TableId属性 | 类型 | 必须指定 | 默认值 | 描述 |
---|---|---|---|---|
value | String | 否 | “” | 主键字段名 |
type | Enum | 否 | IdType.NONE | 主键类型 |
IdType其它主键策略:(后三个已弃用)
值 | 描述 |
---|---|
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_WORKER | 分布式全局唯一ID 长整型类型(please use ASSIGN_ID ) |
UUID | 32位UUID字符串(please use ASSIGN_UUID ) |
ID_WORKER_STR | 分布式全局唯一ID 字符串类型(please use ASSIGN_ID ) |
更多注解参见官网
Long 精度丢失问题
后端大 Long 对象返回前端可能导致精度丢失,当返回的 Long 对象长度超过 17 位时,就会产生,如下
后端返回:1502161733782921217(19位雪花ID)
前端接收:1502161733782921200(超出17位的部分变为0)
解决方式1:
字段加上 @JsonSerialize(using = ToStringSerializer.class)
修改 long 字段的序列化方式为字符串
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
@TableId(type = IdType.ASSIGN_ID)
@JsonSerialize(using = ToStringSerializer.class)
private Long id;
解决方式2(推荐):
重新注册ObjectMapper的Long类型序列化方式,推荐使用
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.annotation.Configuration;
import javax.annotation.Resource;
import java.math.BigInteger;
@Configuration
public class LongClassMessageConverter implements InitializingBean {
@Resource
ObjectMapper objectMapper;
private SimpleModule getSimpleModule() {
SimpleModule simpleModule = new SimpleModule();
simpleModule.addSerializer(BigInteger.class, ToStringSerializer.instance);
simpleModule.addSerializer(Long.class, ToStringSerializer.instance);
// 暂时放弃对小long的转换,约定与前端交互数据时,大Long全部转换成字符串
// simpleModule.addSerializer(Long.TYPE, ToStringSerializer.instance);
objectMapper.registerModule(simpleModule);
return simpleModule;
}
@Override
public void afterPropertiesSet() {
SimpleModule simpleModule = getSimpleModule();
objectMapper.registerModule(simpleModule);
}
}
解决方式3(推荐):
重新构建 Jackson 序列化方式,与方式2类似的解决方式
@Configuration
public class JacksonConfig {
/**
* Jackson全局转化long类型为String,解决jackson序列化时传入前端Long类型缺失精度问题
*/
@Bean
public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer() {
Jackson2ObjectMapperBuilderCustomizer cunstomizer = new Jackson2ObjectMapperBuilderCustomizer() {
@Override
public void customize(Jackson2ObjectMapperBuilder jacksonObjectMapperBuilder) {
jacksonObjectMapperBuilder.serializerByType(BigInteger.class, ToStringSerializer.instance);
jacksonObjectMapperBuilder.serializerByType(Long.class, ToStringSerializer.instance);
// jacksonObjectMapperBuilder.serializerByType(Long.TYPE, ToStringSerializer.instance);
}
};
return cunstomizer;
}
}
以上方式针对springboot默认的Jackson序列化,fastjson等其他json组件类似处理。
四、自动填充
在添加和修改某一条数据时,对某个字段不手动赋值的话,将自动调用MyMetaObjectHandler类的方法为其赋值。
1、实体类属性注解
@Data
public class User {
/**
* 子节点,注解作用:表示在表中没有该字段
*/
@TableField(exist = false)
private List<User> children;
//create_time
@TableField(fill = FieldFill.INSERT)
private Date createTime;
//update_time
@TableField(fill = FieldFill.INSERT_UPDATE)
private Date updateTime;
}
//FieldFill取值
public enum FieldFill {
/**
* 默认不处理
*/
DEFAULT,
/**
* 插入填充字段
*/
INSERT,
/**
* 更新填充字段
*/
UPDATE,
/**
* 插入和更新填充字段
*/
INSERT_UPDATE
}
2、创建MyMetaObjectHandler
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
//使用mp实现添加操作,这个方法执行
@Override
public void insertFill(MetaObject metaObject) {
if( metaObject.hasSetter("createTime") ){
//参数:实体类属性名,属性值,元数据
this.setFieldValByName("createTime", new Date(), metaObject);
}
this.setFieldValByName("updateTime",new Date(),metaObject);
}
//使用mp实现修改操作,这个方法执行
@Override
public void updateFill(MetaObject metaObject) {
this.setFieldValByName("updateTime",new Date(),metaObject);
}
}
五、乐观锁
解决:丢失更新问题
描述:2人同时修改一条数据,后提交的覆盖先提交的,先提交的数据丢失。
例子:A,B同时修改一条数据中的工资500,A先提交500->800,B后提交500->400,期望是B提交时应为800->400。
1、悲观锁
即A在修改数据时,不允许其他人修改数据,缺点:性能较差
2、乐观锁
在表中添加字段version
,所有人在修改时获得版本号,在提交时对比获得的版本号和当前数据库中的版本号是否一致,一致:则修改数据,并且版本号加一;不一致,则不可修改数据。
(1)添加version
添加注解@Version
@Data
public class User {
private Long id;
@Version
@TableField(fill = FieldFill.INSERT) //设置默认值1
private Integer version;//版本号
}
(2)乐观锁插件
编写配置
@Configuration
//建议将主启动类上的注解写在配置类上,避免主启动类太乱
@MapperScan("com.atguigu.mpdemo1010.mapper")
public class MpConfig {
//乐观锁插件
@Bean
public OptimisticLockerInterceptor optimisticLockerInterceptor() {
return new OptimisticLockerInterceptor();
}
}
(3)设置默认值
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
//使用mp实现添加操作,这个方法执行
@Override
public void insertFill(MetaObject metaObject) {
//设置默认值为1
this.setFieldValByName("version",1,metaObject);
}
//使用mp实现修改操作,这个方法执行
@Override
public void updateFill(MetaObject metaObject) {
}
}
六、分页查询
1、添加分页插件
@Configuration
@MapperScan("com.atguigu.mpdemo1010.mapper")
public class MpConfig {
//分页插件
@Bean
public PaginationInterceptor paginationInterceptor() {
return new PaginationInterceptor();
}
}
新版本如下
@Configuration
public class MybatisPlusConfig {
/**
* 添加分页插件
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));//如果配置多个插件,切记分页最后添加
//interceptor.addInnerInterceptor(new PaginationInnerInterceptor()); 如果有多数据源可以不配具体类型 否则都建议配上具体的DbType
return interceptor;
}
}
如果爆红需要添加下面的依赖
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.4.0</version>
</dependency>
2、Page对象
1、编写分页查询的dao层接口时,传递参数 Page 即自动分页,必须放在第一位(可继承Page实现自己的分页对象)
2、分页返回的对象与传入的对象是同一个
//分页查询
@Test
public void testPage() {
//1 创建page对象
//传入两个参数:当前页 和 每页显示记录数
Page<User> page = new Page<>(1,3);
//调用mp分页查询的方法
//调用mp分页查询过程中,底层封装
//把分页所有数据封装到page对象里面
userMapper.selectPage(page,null);
//通过page对象获取分页数据
System.out.println(page.getCurrent());//当前页
System.out.println(page.getRecords());//每页数据list集合
System.out.println(page.getSize());//每页显示记录数
System.out.println(page.getTotal()); //总记录数
System.out.println(page.getPages()); //总页数
System.out.println(page.hasNext()); //下一页
System.out.println(page.hasPrevious()); //上一页
}
3、自定义
service
@Override
public PageUtils listByCatId(HashMap<String,Object> params) {
//1 创建page对象
//传入两个参数:当前页 和 每页显示记录数
Page<AttrGroupByCatIdVo> page = new Page<>(
// hutool工具类,参数:map集合,map的key,key对应的value类型,如果key不存在时的默认值
MapUtil.get(params, PageContent.PAGE,Long.class,1L),
MapUtil.get(params, PageContent.LIMIT,Long.class,10L)
);
//把分页所有数据封装到page对象里面
//分页返回的对象与传入的对象是同一个(iPage == page)
IPage<AttrGroupByCatIdVo> iPage = baseMapper.listByCatId(page,params);
return new PageUtils(iPage);
}
mapper
// page 分页对象,xml中可以从里面进行取值,传递参数 Page 即自动分页,必须放在第一位(你可以继承Page实现自己的分页对象)
IPage<AttrGroupByCatIdVo> listByCatId(Page<AttrGroupByCatIdVo> page, HashMap<String, Object> params);
七、逻辑删除
物理删除:真实删除,将对应数据从数据库中删除,之后查询不到此条被删除数据
逻辑删除:假删除,将对应数据中代表是否被删除字段状态修改为“被删除状态”,之后在数据库中仍 旧能看到此条数据记录
1、新增deleted字段
新增deleted字段,并添加配置
mybatis-plus:
global-config:
db-config:
logic-delete-field: flag # 全局逻辑删除的实体字段名(since 3.3.0,配置后可以不在实体类字段上加注解)
logic-delete-value: 1 # 逻辑已删除值(默认为 1)
logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
2、插件
since 3.3.0,后可不用添加插件
@Configuration
@MapperScan("com.atguigu.mpdemo1010.mapper")
public class MpConfig {
//逻辑删除插件
@Bean
public ISqlInjector sqlInjector() {
return new LogicSqlInjector();
}
}
3、实体类注解
@TableLogic
private Integer deleted;
4、使用
执行删除的方法即可,底层的sql调用的是update语句。
UPDATE USER SET deleted=1 WHERE id=1;
使用逻辑删除后,查询方法自动变为添加查询(自动添加WHERE deleted=0)
SELECT username,PASSWORD FROM USER WHERE deleted=0;
八、性能分析插件
性能分析拦截器,用于输出每条 SQL 语句及其执行时间
SQL 性能执行分析,开发环境使用,超过指定时间,停止运行。有助于发现问题
1、添加插件
参数:maxTime: SQL 执行最大时长,超过自动停止运行,有助于发现问题。
参数:format: SQL是否格式化,默认false
@Configuration
@MapperScan("com.atguigu.mpdemo1010.mapper")
public class MpConfig {
/**
* SQL 执行性能分析插件
* 开发环境使用,线上不推荐。 maxTime 指的是 sql 最大执行时长
*
* 三种环境
* * dev:开发环境
* * test:测试环境
* * prod:生产环境
*/
@Bean
@Profile({"dev","test"})// 设置 dev test 环境开启
public PerformanceInterceptor performanceInterceptor() {
PerformanceInterceptor performanceInterceptor = new PerformanceInterceptor();
performanceInterceptor.setMaxTime(500);//ms,超过此处设置的ms则sql不执行
performanceInterceptor.setFormat(true);//是否格式化
return performanceInterceptor;
}
}
2、application
#环境设置:dev、test、prod
spring.profiles.active=dev
九、Wrapper使用
Wrapper : 条件构造抽象类,最顶端父类
AbstractWrapper : 用于查询条件封装,生成 sql 的 where 条件
QueryWrapper : Entity 对象封装操作类,不是用lambda语法
UpdateWrapper : Update 条件封装,用于Entity对象更新操作
AbstractLambdaWrapper : Lambda 语法使用 Wrapper统一处理解析 lambda 获取 column。
LambdaQueryWrapper :看名称也能明白就是用于Lambda语法使用的查询Wrapper
LambdaUpdateWrapper : Lambda 更新封装Wrapper
1、QueryWrapper
条件查询
//创建QueryWrapper对象
QueryWrapper<User> wrapper = new QueryWrapper<>();
//通过QueryWrapper设置条件
//ge、gt、le、lt
//查询age>=30记录
//第一个参数字段名称,第二个参数设置值
wrapper.ge("age",30);
List<User> users = userMapper.selectList(wrapper);
System.out.println(users);
2、UpdateWrapper
UserEntity user = new UserEntity();
user.setUserAge(12);
UpdateWrapper<UserEntity> wrapper = new UpdateWrapper<>();
wrapper.eq("id",1L);
// 设置 set sql语句部分,语句拼接在set后面,直接拼接,不能防止sql注入
wrapper.setSql("user_name = '王五'");
// 只会更新实体类中不为null的字段
baseMapper.update(user,wrapper);
==> Preparing: UPDATE t_user SET user_age=?, user_name = '王五' WHERE (id = ?)
==> Parameters: 12(Integer), 1(Long)
3、LambdaWrapper
UserEntity user = new UserEntity();
//user.setUserAge(34);
LambdaUpdateWrapper<UserEntity> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(UserEntity::getId,1L)
.set(UserEntity::getUserAge,12);
baseMapper.update(user,wrapper);
==> Preparing: UPDATE t_user SET user_age=?, user_age=? WHERE (id = ?)
==> Parameters: 0(Integer), 12(Integer), 1(Long)
4、结合自定义sql
(1)service
QueryWrapper<UserEntity> wrapper = new QueryWrapper<>();
wrapper.eq("user_name","张三");
wrapper.orderByAsc(true,"user_age");
List<UserEntity> list = baseMapper.findByName(wrapper);
(2)mapper
// Constants.WRAPPER 的值为 ew
List<UserEntity> findByName(@Param(Constants.WRAPPER) QueryWrapper<UserEntity> wrapper);
(3)xml
<select id="findByName" resultType="com.rewind.datasbase.entity.UserEntity">
select * from t_user ${ew.customSqlSegment}
</select>
(5)QueryWrapper 参数
// 1、不带where的条件,xml用法${ew.sqlSegment}
// (user_name = ?) ORDER BY user_age ASC
wrapper.getSqlSegment();
// 2、带where的条件,xml用法${ew.customSqlSegment}
// WHERE (user_name = #{ew.paramNameValuePairs.MPGENVAL1}) ORDER BY user_age ASC
wrapper.getCustomSqlSegment();
// 3、where的条件的 map集合
// {MPGENVAL1=张三}
wrapper.getParamNameValuePairs();
// 4、where条件是否为空,xml用法${ew.isEmptyOfWhere}
// false
wrapper.isEmptyOfWhere();
// 5、where条件是否不为空,xml用法${ew.nonEmptyOfWhere}
// true
wrapper.nonEmptyOfWhere();
// 6、获取格式化后的执行sql
// (user_name = ?) ORDER BY user_age ASC
wrapper.getTargetSql();
wrapper.getCustomSqlSegment();
获取自定义SQL 简化自定义XML复杂情况
使用方法: select xxx from table + ${ew.customSqlSegment}
注意事项:
逻辑删除需要自己拼接条件 (之前自定义也同样)
不支持wrapper中附带实体的情况 (wrapper自带实体会更麻烦)
用法
${ew.customSqlSegment}
(不需要where标签包裹,切记!)ew是wrapper定义别名,不能使用其他的替换
(6)controller
这里写的是我在项目中遇到的接参方法,是某个项目自己封装的,并不是 MP 提供的!此处仅提供思路学习使用
前端传参
localhost:8080/list?pageNum=1&pageSize=20
body:[{
column: "id",
type: "eq",
value: "123"
},{
column: "name",
type: "like",
value: "张三"
}]
controller
@PostMapping("/list")
public Object getTestList(@RequestParam(name = "pageNum", required = false, defaultValue = "1") int pageNum,
@RequestParam(name = "pageSize", required = false, defaultValue = "20") int pageSize,
@RequestBody List<ConditionVo> conditionList) {
QueryWrapper queryWrapper = SearchUtil.parseWhereSql(conditionList);
queryWrapper.orderByDesc("CREATE_DATE");
return service.getPageTestList(queryWrapper,pageNum,pageSize);
}
ConditionVo 条件类
@Data
public class ConditionVo implements Serializable {
private static final long serialVersionUID = -5099378457111419832L;
/**
* 数据库字段名
*/
private String column;
/**
* 字段值
*/
private String value;
/**
* 连接类型,如llike,equals,gt,ge,lt,le
*/
private String type;
}
parseWhereSql 方法
public static QueryWrapper parseWhereSql(List<ConditionVo> conditionList){
for(ConditionVo conditionVo : conditionList){
switch (conditionVo.getType()){
case "eq": queryWrapper.eq(conditionVo.getColumn(),conditionVo.getValue());
break;
case "ne": queryWrapper.ne(conditionVo.getColumn(),conditionVo.getValue());
break;
case "like": queryWrapper.like(conditionVo.getColumn(),conditionVo.getValue());
break;
case "leftlike": queryWrapper.likeLeft(conditionVo.getColumn(),conditionVo.getValue());
break;
case "rightlike": queryWrapper.likeRight(conditionVo.getColumn(),conditionVo.getValue());
break;
case "notlike": queryWrapper.notLike(conditionVo.getColumn(),conditionVo.getValue());
break;
case "gt": queryWrapper.gt(conditionVo.getColumn(),conditionVo.getValue());
break;
case "lt": queryWrapper.lt(conditionVo.getColumn(),conditionVo.getValue());
break;
case "ge": queryWrapper.ge(conditionVo.getColumn(),conditionVo.getValue());
break;
case "le": queryWrapper.le(conditionVo.getColumn(),conditionVo.getValue());
break;
}
}
return queryWrapper;
}
十、枚举值处理
相信后端的同学都经历过一个情况,比如性别
这个字段,分别值和名称对应1男
、2女
,这个字段在数据库时是数值类型,而前端展示则是展示字符串的名称。有几种常见实现方案呢?
- 数据库查询 sql 通过 case 判断,返回名称,以前 oracle 经常这么做
- 数据库返回的值,重新遍历赋值进去,这时候还需要判断这个值到底是男是女。
- 前端写死,返回 1 就是男,返回 2 就是女。
相信无论哪种方法都有其缺点,所以我们可以使用 mybatis-plus 提供的方式。我们在返回给前端时:
- 只需要在遍历时 get 这个枚举,直接赋值其名称,不需要再次判断。
- 直接返回给前端,让前端去去枚举的 name
这样大家都不需要写死这个值。
下面看看如何实现这个功能:
- 兴义枚举,实现 IEnum 接口:
import com.baomidou.mybatisplus.annotation.IEnum;
import com.fasterxml.jackson.annotation.JsonFormat;
/**
* @description: 性别枚举
* @author:weirx
* @date:2022/1/17 16:26
* @version:3.0
*/
@JsonFormat(shape = JsonFormat.Shape.OBJECT)
public enum SexEnum implements IEnum<Integer> {
MAN(1, "男"),
WOMAN(2, "女");
private Integer code;
private String name;
SexEnum(Integer code, String name) {
this.code = code;
this.name = name;
}
@Override
public Integer getValue() {
return code;
}
public String getName() {
return name;
}
}
@JsonFormat
注解为了解决枚举类返回前端只展示构造器名称的问题。
- 实体类性别字段
@TableName(value = "user")
public class UserDO {
/**
* 主键
*/
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/**
* 昵称
*/
@TableField(value = "nickname",condition = SqlCondition.EQUAL)
private String nickname;
/**
* 性别
*/
@TableField(value = "sex")
private SexEnum sex;
/**
* 版本
*/
@TableField(value = "version",update = "%s+1")
private Integer version;
/**
* 时间字段,自动添加
*/
@TableField(value = "create_time",fill = FieldFill.INSERT)
private LocalDateTime createTime;
}
- 配置文件扫描枚举
mybatis-plus:
# 支持统配符 * 或者 ; 分割
typeEnumsPackage: com.wjbgn.*.enums
- 定义配置文件
@Bean
public MybatisPlusPropertiesCustomizer mybatisPlusPropertiesCustomizer() {
return properties -> {
GlobalConfig globalConfig = properties.getGlobalConfig();
globalConfig.setBanner(false);
MybatisConfiguration configuration = new MybatisConfiguration();
configuration.setDefaultEnumTypeHandler(MybatisEnumTypeHandler.class);
properties.setConfiguration(configuration);
};
}
序列化枚举值为数据库值
以下我是使用的 fastjson:
- 全局(添加在前面的配置文件中):
@Bean
public MybatisPlusPropertiesCustomizer mybatisPlusPropertiesCustomizer() {
// 序列化枚举值为数据库存储值
FastJsonConfig config = new FastJsonConfig();
config.setSerializerFeatures(SerializerFeature.WriteEnumUsingToString);
return properties -> {
GlobalConfig globalConfig = properties.getGlobalConfig();
globalConfig.setBanner(false);
MybatisConfiguration configuration = new MybatisConfiguration();
configuration.setDefaultEnumTypeHandler(MybatisEnumTypeHandler.class);
properties.setConfiguration(configuration);
};
}
- 局部
@JSONField(serialzeFeatures= SerializerFeature.WriteEnumUsingToString)
private SexEnum sex;
十一、多数据源
https://baomidou.com/pages/a61e1b/#%E6%96%87%E6%A1%A3-documentation
1、依赖
目前,多数项目会有多数据源的要求,或者是主从部署的要求,所以我们还需要引入 mybatis-plus 关于多数据源的依赖:
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.0</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.27</version>
</dependency>
<!-- mybatis-plus 多数据源 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
<version>3.5.0</version>
</dependency>
2、配置主从
数据源配置,此处配置一主一从的环境,当前我只有一台,所以此处配置一样的:
spring:
datasource:
dynamic:
primary: master #设置默认的数据源或者数据源组,默认值即为master
strict: false #严格匹配数据源,默认false. true未匹配到指定数据源时抛异常,false使用默认数据源
datasource:
master:
url: jdbc:mysql://127.0.0.1:3306/rob_necessities?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone =Asia/Shanghai
username: root
password: 123456
slave_1:
url: jdbc:mysql://127.0.0.1:3306/rob_necessities?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone =Asia/Shanghai
username: root
password: 123456
补充 :这里面因为默认使用的是HikariCP
数据源,目前也推荐使用这个,相比于druid
有更高的性能,但是不能忽略下面的配置,否则服务会不断抛出异常,原因是数据库的连接时常和连接池的配置没有做好。
spring:
datasource:
dynamic:
hikari:
max-lifetime: 1800000
connection-timeout: 5000
idle-timeout: 3600000
max-pool-size: 12
min-idle: 4
connection-test-query: /**ping*/
3、配置多主多从
前面提到过,配置文件当中配置了主从的方式,其实 mybatis-plus 还支持更多的方式:
- 多主多从
spring:
datasource:
dynamic:
primary: master #设置默认的数据源或者数据源组,默认值即为master
strict: false #严格匹配数据源,默认false. true未匹配到指定数据源时抛异常,false使用默认数据源
datasource:
master_1:
master_2:
slave_1:
slave_2:
slave_3:
- 多种数据库
spring:
datasource:
dynamic:
primary: mysql #设置默认的数据源或者数据源组,默认值即为master
strict: false #严格匹配数据源,默认false. true未匹配到指定数据源时抛异常,false使用默认数据源
datasource:
mysql:
oracle:
postgresql:
h2:
sqlserver:
- 混合配置
spring:
datasource:
dynamic:
primary: master #设置默认的数据源或者数据源组,默认值即为master
strict: false #严格匹配数据源,默认false. true未匹配到指定数据源时抛异常,false使用默认数据源
datasource:
master_1:
slave_1:
slave_2:
oracle_1:
oracle_2:
上面的三种方式,除了混合配置,我觉得都有肯能出现的吧。
4、使用方式
- @DS 注解
可以注解在方法上或类上,同时存在就近原则 【方法上注解】 优先于 【类上注解】 :
@DS("slave_1")
public class UserServiceImpl extends ServiceImpl<UserMapper, UserDO> implements IUserService {
@DS("salve_1")
@Override
public List<UserDO> getList() {
return this.getList();
}
@DS("master")
@Override
public int saveUser(UserDO userDO) {
boolean save = this.save(userDO);
if (save){
return 1;
}else{
return 0;
}
}
}
十二、更新字段为null
updateById
方法,想将查询结果中某个字段原本不为null的值更新为null(数据库设计允许为null),但结果该字段更新失败,执行更新方法后改字段值未变。
问题原因
mybatis-plus FieldStrategy 有三种策略:
- IGNORED:0 忽略
- NOT_NULL:1 非 NULL,默认策略
- NOT_EMPTY:2 非空
而默认更新策略是NOT_NULL:非 NULL;即通过接口更新数据时数据为NULL值时将不更新进数据库。
方案一,设置全局的field-strategy
#properties文件格式:
mybatis-plus.global-config.db-config.field-strategy=ignored
#yml文件格式:
mybatis-plus:
global-config:
#字段策略 0:"忽略判断",1:"非 NULL 判断",2:"非空判断"
field-strategy: 0
这样做是全局性配置,会对所有的字段都忽略判断,如果一些字段不想要修改,但是传值的时候没有传递过来,就会被更新为null,可能会影响其他业务数据的正确性。
方案二,对某个字段设置单独的field-strategy
根据具体情况,在需要更新的字段中调整验证注解,如验证非空:
@TableField(strategy=FieldStrategy.NOT_EMPTY)
这样的话,我们只需要在需要更新为null的字段上,设置忽略策略,如下:
@TableField(strategy = FieldStrategy.IGNORED)
private LocalDateTime offlineTime;
在更新代码中,我们直接使用 mybatis-plus
中的 updateById
方法便可以更新成功
使用上述方法,如果需要这样处理的字段较多,那么就需要涉及对各个字段上都添加该注解,显得有些麻烦了。
方案三,使用UpdateWrapper更新
在 mybatis-plus
中,除了updateById
方法,还提供了一个update
方法,直接使用update
方法也可以将字段设置为null,代码如下:
Article article = articleMapper.selectById(id);
LambdaUpdateWrapper<Article> updateWrapper = new LambdaUpdateWrapper<>();
updateWrapper.eq(Article::getId,article.getId());
// 将字段更新为null
updateWrapper.set(Article::getOfflineTime, null);
updateWrapper.set(Article::getContent,"try mybatis plus update null");
updateWrapper.set(Article::getPublishTime,LocalDateTime.now().plusHours(8));
int i = articleMapper.update(article, updateWrapper);
LAST、BUG 处理
1、无效列类型
MyBatis
结合 oracle
出现“无效列类型1111错误”
解决方法:配置文件新增
mybatis-plus:
configuration:
jdbc-type-for-null: 'null'