MyBatisPlus 用法详解
文章目录
- 一、快速入门
- 1.1 引入依赖:
- 1.2 定义 Mappper:
- 1.3 使用演示:
- 1.4 常见注解:
- 1.4.1 @TableName:
- 1.4.2 @TableId:
- 1.4.3 @TableField:
- 1.5 常见配置:
- 二、核心功能
- 2.1 条件构造器:
- 2.1.1 QueryWrapper:
- 2.1.2 UpdateWrapper:
- 2.1.3 LambdaQueryWrapper || LambdaUpdateWrapper:
- 2.2 自定义 SQL:
- 2.2.1 基本用法:
- 2.2.2 多表关联:
- 2.3 Service 接口:
- 2.3.1 CRUD:
- 2.3.2 基本用法:
- 2.3.3 Lambda:
- 2.3.4 批量新增:
- 三、扩展功能
- 3.1 代码生成插件:
- 3.1.1 安装插件:
- 3.1.2 使用演示:
- 3.2 静态工具类:
- 3.3 逻辑删除
- 3.4 通用枚举:
- 3.4.1 创建枚举对象并添加 @EnumValue 注解
- 3.4.2 配置枚举处理器:
- 3.4.3 @JsonValue 注解:
- 3.5 JSON 类型处理器:
- 3.5.1 定义存储实体:
- 3.5.2 使用类型处理器:
- 四、分页插件
- 4.1 配置分页插件:
- 4.2 使用演示:
一、快速入门
1.1 引入依赖:
MybatisPlus 提供了 starter,实现了自动 Mybatis 以及MybatisPlus 的自动装配功能,坐标如下:
<dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.5.3.1</version>
</dependency>
这个 starter 包含对 mybatis 的自动装配,因此完全可以替换掉 Mybatis 的starter。
1.2 定义 Mappper:
为了简化单表的 CRUD,MybatisPlus 提供了一个基础的BaseMapper
接口,其中已经实现了单表的 CRUD,具体方法如下图所示:
因此我们自定义的 Mapper 只要继承了这个BaseMapper<T>
,就无需自己实现单表 CRUD 了。
注意:继承 baseMapper 需要指定对应的实体类。
1.3 使用演示:
//Mapper:
public interface UserMapper extends BaseMapper<User> {
}
//实体类
@Data
public class User {private Long id;private String username;private String password;private String phone;private String info;private Integer status;private Integer balance;private LocalDateTime createTime;private LocalDateTime updateTime;
}//测试类
@SpringBootTest
class UserMapperTest {@Autowiredprivate UserMapper userMapper;@Testvoid 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);// MybatisPlus 提供的}@Testvoid testSelectById() {User user = userMapper.selectById(5L);// MybatisPlus 提供的System.out.println("user = " + user);}@Testvoid testQueryByIds() {List<User> users = userMapper.selectBatchIds(List.of(1L, 2L, 3L, 4L));// MybatisPlus 提供的users.forEach(System.out::println);}@Testvoid testUpdateById() {User user = new User();user.setId(5L);user.setBalance(20000);userMapper.updateById(user);// MybatisPlus 提供的}@Testvoid testDeleteUser() {userMapper.deleteById(5L);// MybatisPlus 提供的}
}
效果如下图所示:由于方法格式过多,所以下面这张图片只展示更新部分的。
1.4 常见注解:
MybatisPlus 就是根据 PO 实体(BaseMapper<T>
中的T
)的信息来推断出表的信息,从而生成 SQL 的。默认情况下:
- MybatisPlus 会把 PO 实体的类名驼峰转下划线作为表名。
- MybatisPlus 会把 PO 实体的所有变量名驼峰转下划线作为表的字段名,并根据变量类型推断字段类型。
- MybatisPlus 会把名为 id 的字段作为主键。
但很多情况下,默认的实现与实际场景不符,因此 MybatisPlus 提供了一些注解便于我们声明表信息。
1.4.1 @TableName:
使用方式:表名注解,标识实体类对应的表。
使用位置:实体类。
使用场景:当实体类的名称和表名的命名不一致时(驼峰和下划线的区别另外),使用该注解。
TableName 注解除了指定表名以外,还可以指定很多其它属性:
属性 | 类型 | 必须指定 | 默认值 | 描述 |
---|---|---|---|---|
value | String | 否 | “” | 表名 |
schema | String | 否 | “” | schema |
keepGlobalPrefix | boolean | 否 | false | 是否保持使用全局的 tablePrefix 的值(当全局 tablePrefix 生效时) |
resultMap | String | 否 | “” | xml 中 resultMap 的 id(用于满足特定类型的实体类对象绑定) |
autoResultMap | boolean | 否 | false | 是否自动构建 resultMap 并使用(如果设置 resultMap 则不会进行 resultMap 的自动构建与注入) |
excludeProperty | String[] | 否 | {} | 需要排除的属性名 @since 3.3.1 |
1.4.2 @TableId:
使用方式:主键注解,标识实体类中的主键字段。
使用位置:实体类的主键字段。
使用场景:需要指定主键时,如果是按照规范进行操作,不需要使用该注解指定,有默认值。
TableId
注解支持两个属性:
属性 | 类型 | 必须指定 | 默认值 | 描述 |
---|---|---|---|---|
value | String | 否 | “” | 主键字段名 |
type | Enum | 否 | IdType.NONE | 指定主键类型 |
IdType
支持的类型有:
值 | 描述 |
---|---|
AUTO | 数据库 ID 自增 |
NONE | 无状态,该类型为未设置主键类型(注解里等于跟随全局,全局里约等于 ASSIGN_ID) |
INPUT | insert 前自行 set 主键值 |
ASSIGN_ID | 分配 ID(主键类型为 Number(Long 和 Integer)或 String)(since 3.3.0),使用接口IdentifierGenerator的方法nextId(默认实现类为DefaultIdentifierGenerator雪花算法),简单理解为程序自动利用雪花算法,帮助我们生成 id。 |
ASSIGN_UUID | 分配 UUID,主键类型为 String(since 3.3.0),使用接口IdentifierGenerator的方法nextUUID(默认 default 方法) |
这里比较常见的有三种:
AUTO
:利用数据库的id自增长INPUT
:手动生成idASSIGN_ID
:雪花算法生成Long
类型的全局唯一id,这是默认的ID策略。
1.4.3 @TableField:
使用方式:普通字段注解,标识实体类中的普通属性。
使用位置:实体类的属性。
使用场景:当实体类的命名和数据库的字段名有出路时,使用该注解。
一般情况下我们并不需要给字段添加@TableField
注解,一些特殊情况除外(非常重要):
- 成员变量名与数据库字段名不一致
- 成员变量是以
isXXX
命名,按照JavaBean
的规范,MybatisPlus
识别字段时会把is
去除,这就导致与数据库不符。 - 成员变量名与数据库一致,但是与数据库的关键字冲突。使用
@TableField
注解给字段名添加转义字符:``。
支持的其它属性如下:
属性 | 类型 | 必填 | 默认值 | 描述 |
---|---|---|---|---|
value | String | 否 | “” | 数据库字段名 |
exist | boolean | 否 | true | 是否为数据库表字段 |
condition | String | 否 | “” | 字段 where 实体查询比较条件,有值设置则按设置的值为准,没有则为默认全局的 %s=#{%s},参考(opens new window) |
update | String | 否 | “” | 字段 update set 部分注入,例如:当在version字段上注解update=“%s+1” 表示更新时会 set version=version+1 (该属性优先级高于 el 属性) |
insertStrategy | Enum | 否 | FieldStrategy.DEFAULT | 举例:NOT_NULL insert into table_a(column) values (#{columnProperty}) |
updateStrategy | Enum | 否 | FieldStrategy.DEFAULT | 举例:IGNORED update table_a set column=#{columnProperty} |
whereStrategy | Enum | 否 | FieldStrategy.DEFAULT | 举例:NOT_EMPTY where column=#{columnProperty} |
fill | Enum | 否 | FieldFill.DEFAULT | 字段自动填充策略 |
select | boolean | 否 | true | 是否进行 select 查询 |
keepGlobalFormat | boolean | 否 | false | 是否保持使用全局的 format 进行处理 |
jdbcType | JdbcType | 否 | JdbcType.UNDEFINED | JDBC 类型 (该默认值不代表会按照该值生效) |
typeHandler | TypeHander | 否 | 类型处理器 (该默认值不代表会按照该值生效) | |
numericScale | String | 否 | “” | 指定小数点后保留的位数 |
1.5 常见配置:
MybatisPlus 也支持基于 yaml 文件的自定义配置。
大多数的配置都有默认值,因此我们都无需配置。但还有一些是没有默认值的,例如:
-
实体类的别名扫描包(用来简化,后续引用实体类的路径。这个即使配置,我还是建议大家把路径写全,因为这个有时会不生效,而且编译器会更加支持写全)。
-
全局 id 类型。
mybatis-plus:type-aliases-package: xxxxxx # 填写实体类对应的包名global-config:db-config:id-type: auto # 全局id类型为自增长
需要注意的是,MyBatisPlus 也支持手写 SQL 的,而 mapper 文件的读取地址可以自己配置:
mybatis-plus:mapper-locations: "classpath*:/mapper/**/*.xml" # Mapper.xml文件地址,当前这个是默认值。
二、核心功能
刚才的案例中都是以 id 为条件的简单 CRUD,一些复杂条件的 SQL 语句就要用到一些更高级的功能了。
2.1 条件构造器:
除了新增以外,修改、删除、查询的 SQL 语句都需要指定 where 条件。因此 BaseMapper 中提供的相关方法除了以id
作为where
条件以外,还支持更加复杂的where
条件。
参数中的Wrapper
就是条件构造的抽象类,其下有很多默认实现,继承关系如图:
Wrapper
的子类AbstractWrapper
提供了where中包含的所有条件构造方法:(下面的这些方法中,一般来说,第一个参数是 boolean 类型的,是用来做出动态 SQL 中 if 标签同样的效果,如果为 true,表示该条件会添加到 SQL 中,如果为 false,就不会在 SQL 中显示该条件。)
而 QueryWrapper 在 AbstractWrapper 的基础上拓展了一个 select 方法,允许指定查询字段:
而 UpdateWrapper 在 AbstractWrapper 的基础上拓展了一个 set 方法,允许指定 SQL 中的 SET 部分:
2.1.1 QueryWrapper:
无论是修改、删除、查询,都可以使用 QueryWrapper 来构建查询条件。
下面举个例子:
查询:查询出名字中带o
的,存款大于等于 1000 元的人的 id,username,info,balance。代码如下:
@Test
void testQueryWrapper() {//1. 构造查询条件QueryWrapper<User> queryWrapper = new QueryWrapper<User>().select("id","username","info","balance").like("username", "o").ge("balance", 1000);//2. 进行查询List<User> users = userMapper.selectList(queryWrapper);users.forEach(System.out::println);
}
更新:更新用户名为 jack(Mysql 中,是不区分大小写的) 的用户的余额为 2000,代码如下:
@Test
void testUpdateByQueryWrapper(){//1. 构造更新条件//1.1 构造更新参数User user = new User();user.setBalance(2000);//1.2 构造更新条件QueryWrapper<User> queryWrapper = new QueryWrapper<User>().eq("username","jack");//2. 进行更新userMapper.update(user, queryWrapper);
}
2.1.2 UpdateWrapper:
上面 BaseMapper 中的 update 方法传入 QueryWrapper 的条件更新只能直接进行复制,对于一些复杂的需求就难以实现。
例如:更新id为1,2,4
的用户的余额,扣 200,对应的 SQL 应该是:
UPDATE user SET balance = balance - 200 WHERE id in (1, 2, 4)
SET 的赋值结果是基于字段现有值的,这个时候就要利用 UpdateWrapper 中的 setSql 功能了:
@Test
void testUpdateWrapper(){//1. 构造更新条件UpdateWrapper<User> updateWrapper = new UpdateWrapper<User>().setSql("balance = balance - 200").in("id",List.of(1L,2L,4L));//2. 进行更新userMapper.update(null, updateWrapper);
}
2.1.3 LambdaQueryWrapper || LambdaUpdateWrapper:
无论是 QueryWrapper 还是 UpdateWrapper 在构造条件的时候都需要写死字段名称,这在编程规范中显然是不推荐的。
那怎么样才能不写字段名,又能知道字段名呢?
一种办法是基于变量的gettter
方法结合反射技术。因此我们只要将条件对应的字段的getter
方法传递给 MybatisPlus,它就能计算出对应的变量名,从而知道字段名。而传递方法可以使用 JDK8 中的方法引用
和Lambda
表达式。 因此 MybatisPlus 又提供了一套基于Lambda 的 Wrapper ,包含两个:
- LambdaQueryWrapper
- LambdaUpdateWrapper
分别对应 QueryWrapper 和 UpdateWrapper。
LambdaQueryWrapper 的使用演示如下:
@Test
void testLambdaQueryWrapper() {//1. 构造查询条件LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<User>().select(User::getId, User::getUsername, User::getInfo, User::getBalance).like(User::getUsername, "o").ge(User::getBalance, 1000);//2. 进行查询List<User> users = userMapper.selectList(lambdaQueryWrapper);users.forEach(System.out::println);
}
LambdaUpdateWrapper 的使用演示如下:(需要注意的是,如果是使用 setSql 的话,是不能使用 Lambda 表达式的,如果需要达成在原来的基础上进行扣减,且不使用 setSql 的话,可以从 Java 代码的业务逻辑出发,先查出当前 user 的信息,在对该 user 的余额进行扣减,再更新回去即可,实现起来并不难,所以下面演示的代码就简单的进行赋值)。
@Test
void testLambdaUpdateWrapper() {// 1. 构造更新条件LambdaUpdateWrapper<User> lambdaUpdateWrapper = new LambdaUpdateWrapper<User>().set(User::getBalance, 1000).in(User::getId, List.of(1L, 2L, 3L));//2. 进行更新userMapper.update(null, lambdaUpdateWrapper);
}
2.2 自定义 SQL:
在演示 UpdateWrapper 的案例中,我们在代码中编写了更新的 SQL 语句:
这种写法在某些企业也是不允许的,因为 SQL 语句最好都维护在持久层,而不是业务层。 就当前案例来说,由于条件是 in 语句,只能将SQL 写在 Mapper.xml 文件,利用 foreach 来生成动态 SQL。 这实在是太麻烦了。假如查询条件更复杂,动态 SQL 的编写也会更加复杂。
所以,MybatisPlus 提供了自定义 SQL 功能,可以让我们利用 Wrapper 生成查询条件,再结合 Mapper.xml 编写 SQL。这样的做法算是综合了一下利弊,将 where 之前的 sql 语句放在 Mapper 层,进行编写,where 后面的语句在业务层进行编写,虽然还是不符合规范,但是较之前全部 sql 都放在业务层,也算是有了提升,MybatisPlus 的优势在于编写 where 条件非常的方便,使用 xml 文件进行动态 sql 编写,太繁琐了。
2.2.1 基本用法:
@Test
void testCustomWrapper(){//1. 构造 Wrapper 条件QueryWrapper<User> queryWrapper = new QueryWrapper<User>().in("id", List.of(1L, 2L, 4L));//2. 进行扣除余额userMapper.deductBalanceByIds(queryWrapper,200);
}
// Mapper 层
void deductBalanceByIds(@Param("ew") QueryWrapper<User> queryWrapper, @Param("balance") int balance);
// xml 文件
<update id="deductBalanceByIds">update user set balance = balance - #{balance} ${ew.customSqlSegment}
</update>
注意:@Param(“ew”) QueryWrapper queryWrapper,中 @Param(“ew”) 是固定写法,${ew.customSqlSegment} 也是固定写法。
2.2.2 多表关联:
理论上来讲 MyBatisPlus 是不支持多表查询的,不过我们可以利用 Wrapper 中自定义条件,结合自定义 SQL 来实现多表查询的效果。例如,我们要查询出所有收货地址在北京的并且用户 id 在1、2、4之中的用户,要是自己基于 mybatis 实现 SQL,大概是这样的:
<select id="queryUserByIdAndAddr" resultType="User">SELECT *FROM user uINNER JOIN address a ON u.id = a.user_idWHERE u.id<foreach collection="ids" separator="," item="id" open="IN (" close=")">#{id}</foreach>AND a.city = #{city}</select>
可以看出其中 where 的编写还是挺复杂的,如果业务复杂一些,这里的 SQL 会更变态。
但是基于自定义 SQL 结合 Wrapper 的玩法,我们就可以利用 Wrapper 来构建查询条件,然后手写 SELECT 及 FROM 部分,实现多表查询。
对应的代码如下:
// VO 实体类
@Data
public class UserVO extends User {private String city;
}
// Test 测试类
@Test
void testCustomJoinWrapper(){//1. 构造 Wrapper 条件QueryWrapper<UserVO> queryWrapper = new QueryWrapper<UserVO>().in("u.id",List.of(1L,2L,4L)).eq("a.city","北京");//2. 进行查询List<UserVO> users = userMapper.queryUserAndAddressByWrapper(queryWrapper);
}
// Mapper 层代码
List<UserVO> queryUserAndAddressByWrapper(@Param("ew") QueryWrapper<UserVO> queryWrapper); // xml 文件中的对应代码
<select id="queryUserAndAddressByWrapper" resultType="com.gobeyye.mp.domain.vo.UserVO">select u.*,a.city from user u inner join address aon u.id = a.user_id ${ew.customSqlSegment}
</select>
对于上面这段 Mybatis plus 使用自定义 SQL 写多表查询,我还是更加建议使用原来的 Mybatis,配合使用 AI ,也不会太繁琐,且代码的可读性更好,不用一条 SQL 多个文件里面去找。
2.3 Service 接口:
MybatisPlus 不仅提供了 BaseMapper,还提供了通用的 Service 接口及默认实现,封装了一些常用的 service 模板方法。
通用接口为IService
,默认实现为ServiceImpl
,其中封装的方法可以分为以下几类:
save
:新增remove
:删除update
:更新get
:查询单个结果list
:查询集合结果count
:计数page
:分页查询
2.3.1 CRUD:
这里带着大家一起看看 Mybatis plus 提供的方法都有什么,并解释一些常用的方法。
新增:
save
是新增单个元素saveBatch
是批量新增saveOrUpdate
是根据id判断,如果数据存在就更新,不存在则新增saveOrUpdateBatch
是批量的新增或修改
删除:
removeById
:根据id删除removeByIds
:根据id批量删除removeByMap
:根据Map中的键值对为条件删除remove(Wrapper<T>)
:根据Wrapper条件删除removeBatchByIds
:也是根据 id 批量进行删除,但是删除的实现方式不一样(removeByIds:使用一条 SQL 语句,配合 in 子句一次性执行删除操作,性能比较高。removeBatchByIds:会生成多条的 SQL 语句,每条 SQL 语句逐个执行,性能比 removeByIds 差,但是适用于数据量非常大的时候,可以有效避免一条 SQL 语句过长的情况)。
修改:
updateById
:根据id修改update(Wrapper<T>)
:根据UpdateWrapper
修改,Wrapper
中包含set
和where
部分update(T,Wrapper<T>)
:按照T
内的数据修改与Wrapper
匹配到的数据updateBatchById
:根据id批量修改
Get:
getById
:根据id查询1条数据getOne(Wrapper<T>)
:根据Wrapper
查询1条数据getBaseMapper
:获取Service
内的BaseMapper
的子类,某些时候需要直接调用Mapper
内的自定义SQL
时可以用这个方法获取到Mapper
List:
listByIds
:根据id批量查询list(Wrapper<T>)
:根据Wrapper条件查询多条数据list()
:查询所有
Count:
count()
:统计所有数量count(Wrapper<T>)
:统计符合Wrapper
条件的数据数量
getBaseMapper:
当我们在 service 中要调用 Mapper 中自定义 SQL 时,就必须获取 service 对应的 Mapper,就可以通过这个方法:
2.3.2 基本用法:
由于Service
中经常需要定义与业务有关的自定义方法,因此我们不能直接使用IService
,而是自定义Service
接口,然后继承IService
的拓展方法,由于都是接口,继承了之后,根据 Java 语法,需要我们自己实现,这显然不符合 Mybatis plus 的创建初衷,于是 Mybatis plus 自己提供了一个 IService 的实现类 ServiceImpl,我们自己的实现类只要继承该实现类,就可以直接使用方法了。
上面的这段文字可以抽象成下面的这张图片:
// UserService 的接口
public interface IUserService extends IService<User> {
}// UserService 的实现类
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService{
}
由于这部分的代码调用演示和上面的都差不多,无非就是一个是在 Controller 层调用,一个是在 Service 层进行调用,所以这里就不再进行演示。
2.3.3 Lambda:
IService 中还提供了 Lambda 功能来简化我们的复杂查询及更新功能。
下面通过两个两个案例分别学习 lambdaQuery() 和 lambdaUpdate()。
案例一:
实现一个根据复杂条件查询用户的接口,查询条件如下:
- name:用户名关键字,可以为空
- status:用户状态,可以为空
- minBalance:最小余额,可以为空
- maxBalance:最大余额,可以为空
代码如下:
// UserQuery 实体
@Data
@ApiModel(description = "用户查询条件实体")
public class UserQuery {@ApiModelProperty("用户名关键字")private String name;@ApiModelProperty("用户状态:1-正常,2-冻结")private Integer status;@ApiModelProperty("余额最小值")private Integer minBalance;@ApiModelProperty("余额最大值")private Integer maxBalance;
}// Controller 层的代码
@GetMapping("/list")@ApiOperation("通过 UserQuery 查询用户")public List<User> queryUser(UserQuery query) {//1. 准备工作String username = query.getName();Integer status = query.getStatus();Integer minBalance = query.getMinBalance();Integer maxBalance = query.getMaxBalance();//2. 构造条件并进行查询List<User> users = userService.lambdaQuery().like(username != null, User::getUsername, username).eq(status != null, User::getStatus, status).ge(minBalance != null, User::getBalance, minBalance).le(maxBalance != null, User::getBalance, maxBalance).list();//3. 返回结果return users;}
可以发现 lambdaQuery 方法中除了构建条件,还需要在链式编程的最后添加一个list()
,这是在告诉 MybatisPlus,我们的调用结果需要是一个 list 集合。这里不仅可以用list()
,可选的方法有:
.one()
:最多1个结果.list()
:返回集合结果.count()
:返回计数结果
MybatisPlus 会根据链式编程的最后一个方法来判断最终的返回结果。
案例二:
根据 id 修改用户余额,如果扣减后余额为 0,则将用户 status 修改为冻结状态。
代码实现如下:
@Transactional
@Override
public void deductBalance(Long id, Integer money) {//1. 检查用户是否存在User user = this.getById(id);if(user == null){throw new RuntimeException("用户不存在");}//2. 检查用户状态if(user.getStatus() == 2){throw new RuntimeException("用户状态异常");}//3. 检查用户余额是否足够if(user.getBalance() - money < 0){throw new RuntimeException("用户余额不足");}// 剩余的金额int newMoney = user.getBalance() - money;//4. 扣减余额this.lambdaUpdate().set(User::getBalance,newMoney).set(newMoney == 0, User::getStatus,2)// 动态判断是否要更新状态.eq(User::getId,id).eq(User::getBalance,user.getBalance())// 乐观锁,防止在多线程的情况下出现异常。.update();
}
2.3.4 批量新增:
IService 中的批量新增功能使用起来非常方便,但有一点注意事项,我们先来测试一下。
MybatisPlus 的批处理(插入 10 万条数据):
@Test
void testSaveBatch() {// 准备10万条数据List<User> list = new ArrayList<>(1000);long b = System.currentTimeMillis();for (int i = 1; i <= 100000; i++) {list.add(buildUser(i));// 每1000条批量插入一次if (i % 1000 == 0) {userService.saveBatch(list);list.clear();}}long e = System.currentTimeMillis();System.out.println("耗时:" + (e - b));
}
private User buildUser(int i) {User user = new User();user.setUsername("user_" + i);user.setPassword("123");user.setPhone("" + (18688190000L + i));user.setBalance(2000);user.setInfo("{\"age\": 24, \"intro\": \"英文老师\", \"gender\": \"female\"}");user.setCreateTime(LocalDateTime.now());user.setUpdateTime(user.getCreateTime());return user;
}
执行最终耗时如下:
MybatisPlus
的批处理是基于PrepareStatement
的预编译模式,然后批量提交,最终在数据库执行时还是会有多条 insert 语句,逐条插入数据。SQL 类似这样:
Preparing: INSERT INTO user ( username, password, phone, info, balance, create_time, update_time ) VALUES ( ?, ?, ?, ?, ?, ?, ? )
Parameters: user_1, 123, 18688190001, "", 2000, 2023-07-01, 2023-07-01
Parameters: user_2, 123, 18688190002, "", 2000, 2023-07-01, 2023-07-01
Parameters: user_3, 123, 18688190003, "", 2000, 2023-07-01, 2023-07-01
而如果想要得到最佳性能,最好是将多条 SQL 合并为一条,像这样:
INSERT INTO user ( username, password, phone, info, balance, create_time, update_time )
VALUES
(user_1, 123, 18688190001, "", 2000, 2023-07-01, 2023-07-01),
(user_2, 123, 18688190002, "", 2000, 2023-07-01, 2023-07-01),
(user_3, 123, 18688190003, "", 2000, 2023-07-01, 2023-07-01),
(user_4, 123, 18688190004, "", 2000, 2023-07-01, 2023-07-01);
这要怎么进行转化呢?
MySQL的客户端连接参数中有这样的一个参数:rewriteBatchedStatements
。顾名思义,就是重写批处理的statement
语句。
这个参数的默认值是 false,我们需要修改连接参数,将其配置为 true。
spring:datasource:url: jdbc:mysql://127.0.0.1:3306/mp?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=truedriver-class-name: com.mysql.cj.jdbc.Driverusername: rootpassword: root
再次测试插入10万条数据,可以发现速度有非常明显的提升:
三、扩展功能
3.1 代码生成插件:
在使用MybatisPlus以后,基础的Mapper
、Service
、PO
代码相对固定,重复编写也比较麻烦。因此MybatisPlus官方提供了代码生成器根据数据库表结构生成PO
、Mapper
、Service
等相关代码。只不过代码生成器同样要编码使用,也很麻烦。
这里推荐大家使用一款MybatisPlus
的插件,它可以基于图形化界面完成MybatisPlus
的代码生成,非常简单。
3.1.1 安装插件:
在Idea
的 plugins 市场中搜索并安装MyBatisPlus
插件:
然后记得重启 IDEA。
3.1.2 使用演示:
首先需要配置数据库地址,在 Idea 顶部菜单中,找到tools
,选择Config Database
:
在弹出的窗口中填写数据库连接的基本信息:
点击 OK 保存。
然后再次点击 IDEA 顶部菜单中的 tools,然后选择Code Generator
:
在弹出的表单中填写信息:
最终,代码自动生成到指定的位置了。
3.2 静态工具类:
有的时候 Service 之间也会相互调用,为了避免出现循环依赖问题,MybatisPlus 提供一个静态工具类:Db
,其中的一些静态方法与IService
中方法签名基本一致(大多数需要多传递一个,数据库对应实体类的字节码),也可以帮助我们实现 CRUD 功能:
示例:
@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");
}
3.3 逻辑删除
对于一些比较重要的数据,我们往往会采用逻辑删除的方案,即:
- 在表中添加一个字段标记数据是否被删除
- 当删除数据时把标记置为 true
- 查询时过滤掉标记为 true 的数据
一旦采用了逻辑删除,所有的查询和删除逻辑都要跟着变化(原先没有采用逻辑删除),非常麻烦。
为了解决这个问题,MybatisPlus 就添加了对逻辑删除的支持。
注意:只有MybatisPlus生成的SQL语句才支持自动的逻辑删除,自定义SQL需要自己手动处理逻辑删除。
具体操作如下:
首先需要给表和对应的实体类添加对应的逻辑删除字段。
接下来,我们要在application.yml
中配置逻辑删除字段:(对 Mybatis 的版本有要求,大于 3.3.0)
mybatis-plus:global-config:db-config:logic-delete-field: deleted # 全局逻辑删除的实体字段名(since 3.3.0,配置后可以忽略不配置步骤2)logic-delete-value: 1 # 逻辑已删除值(默认为 1)logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
测试代码如下:
@Test
void testDeleteById(){Db.removeById(60L, Address.class);
}
代码执行结果如下:
综上, 开启了逻辑删除功能以后,我们就可以像普通删除一样做 CRUD,基本不用考虑代码逻辑问题。还是非常方便的。
注意:
逻辑删除本身也有自己的问题,比如:
- 会导致数据库表垃圾数据越来越多,从而影响查询效率。
- SQL 中全都需要对逻辑删除字段做判断,影响查询效率。
因此,如果数据不能删除,可以采用把数据迁移到其它表的方法。
3.4 通用枚举:
MybatisPlus 提供了一个处理枚举的类型转换器,可以帮我们把枚举类型与数据库类型自动转换。
3.4.1 创建枚举对象并添加 @EnumValue 注解
要让MybatisPlus
处理枚举与数据库类型自动转换,我们必须告诉MybatisPlus
,枚举中的哪个字段的值作为数据库值。
MybatisPlus
提供了@EnumValue
注解来标记枚举属性:
@Getter
public enum UserStatus {NORMAL(1,"正常"),FREEZE(2,"冻结"),;@EnumValueprivate final Integer code;private final String desc;UserStatus(Integer code, String desc) {this.code = code;this.desc = desc;}
}
3.4.2 配置枚举处理器:
在application.yaml文件中添加配置:
mybatis-plus:configuration:default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler
测试代码:
@Test
void testService(){List<User> list = userService.list();list.forEach(System.out::println);
}
因此我们可以知道,在配置完枚举处理器和添加 @EnumValue 注解后,MybatisPlus 会自动帮助我们完成 Java 枚举对象和数据库列字段的转化。
3.4.3 @JsonValue 注解:
如果我们想要枚举对象在前端展示对象其中的一个属性,可以通过 @JsonValue 添加在对应的属性上,MybatisPlus 会自动帮助我们进行转化。
未添加 @JsonValue 注解之前。
添加 @JsonValue 注解之后。
3.5 JSON 类型处理器:
数据库的 user 表中有一个info
字段,是 JSON 类型:
格式类似这样:
{"age": 20, "intro": "佛系青年", "gender": "male"}
但是我们 Java 当中没有 Json 这个类型,所以我们只能采用 String 来进行接收。
这样一来,我们要读取 info 中的属性时就非常不方便。如果要方便获取,info 的类型最好是一个Map
或者实体类。
而一旦我们把info
改为对象
类型,就需要在写入数据库时手动转为String
,再读取数据库时,手动转换为对象
,这会非常麻烦。
因此 MybatisPlus 提供了很多特殊类型字段的类型处理器,解决特殊字段类型与数据库类型转换的问题。例如处理 JSON 就可以使用JacksonTypeHandler
处理器。
3.5.1 定义存储实体:
@Data
public class UserInfo {private Integer age;private String intro;private String gender;
}
3.5.2 使用类型处理器:
接下来,将 User 类的 info 字段修改为 UserInfo 类型,并声明类型处理器:
注意:由于这样是对象套对象,我们需要处理映射关系(resultMap),这样才能把数据传输到 info 属性上。
MybatisPlus 帮助我们处理了对象套对象的映射关系,我们只需要在对应类上添加注解 @TableName(autoResultMap = true)。即可。
效果如下:
四、分页插件
MybatisPlus 提供了很多的插件功能,进一步拓展其功能。目前已有的插件有:
PaginationInnerInterceptor
:自动分页TenantLineInnerInterceptor
:多租户DynamicTableNameInnerInterceptor
:动态表名OptimisticLockerInnerInterceptor
:乐观锁IllegalSQLInnerInterceptor
:sql 性能规范BlockAttackInnerInterceptor
:防止全表更新与删除
这里我们以分页插件来学习插件的用法。
在未引入分页插件的情况下,MybatisPlus
是不支持分页功能的,IService
和BaseMapper
中的分页方法都无法正常起效。所以,我们必须配置分页插件。
4.1 配置分页插件:
在项目中新建一个配置类:
其代码如下:
@Configuration
public class MybatisConfig {@Beanpublic MybatisPlusInterceptor mybatisPlusInterceptor(){//1. 初始化核心插件MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();//2. 添加分页插件interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));//3. 返回核心插件return interceptor;}
}
4.2 使用演示:
核心在于 Page 类,下面是 page 类的所有属性,既有分页查询所需的条件,也有分页查询返回的结果。
注意:分页查询传递的参数 page 和接收的 page 是同一个对象。
@Test
void testPageQuery(){// 1.分页查询,new Page()的两个参数分别是:页码、每页大小Page<User> p = userService.page(new Page<>(2, 2));// 2.总条数System.out.println("total = " + p.getTotal());// 3.总页数System.out.println("pages = " + p.getPages());// 4.数据List<User> records = p.getRecords();records.forEach(System.out::println);
}
效果如下:
这里用到了分页参数,Page,即可以支持分页参数,也可以支持排序参数。常见的 API 如下:
其中 OrderItem 的第二个参数,如果为 true 表示为升序,如果为 false 表示为降序。page 可以添加多个排序规则,当第一个排序规则相等时,采用第二个排序规则,以此类推。
int pageNo = 1, pageSize = 5;
// 分页参数
Page<User> page = Page.of(pageNo, pageSize);
// 排序参数, 通过OrderItem来指定
page.addOrder(new OrderItem("balance", false));
//.....可以有很多个
page.addOrder(....)
// 进行分页查询
userService.page(page);
参考文献:
- 黑马程序员
结语:
其实写博客不仅仅是为了教大家,同时这也有利于我巩固知识点,和做一个学习的总结,由于作者水平有限,对文章有任何问题还请指出,非常感谢。如果大家有所收获的话,还请不要吝啬你们的点赞收藏和关注,这可以激励我写出更加优秀的文章。