MybatisPlus
文档
官方文档:https://baomidou.com/
简介
MyBatis-Plus (opens new window)(简称 MP)是一个 MyBatis (opens new window)的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。
特性
- 无侵入:只做增强不做改变,引入它不会对现有工程产生影响,如丝般顺滑
- 损耗小:启动即会自动注入基本 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 操作智能分析阻断,也可自定义拦截规则,预防误操作
入门案例
创建数据库
CREATE DATABASE `mybatis_plus` /*!40100 DEFAULT CHARACTER
SET utf8mb4 */;
USE `mybatis_plus`;
CREATE TABLE `user` (
`id` INT ( 20 ) NOT NULL COMMENT '主键ID',
`name` VARCHAR ( 30 ) DEFAULT NULL COMMENT '姓名',
`age` INT ( 11 ) DEFAULT NULL COMMENT '年龄',
`email` VARCHAR ( 50 ) DEFAULT NULL COMMENT '邮箱',
PRIMARY KEY ( `id` )
) ENGINE = INNODB DEFAULT CHARSET = utf8;
添加数据
INSERT INTO USER ( id, NAME, age, email )
VALUES
( 1, 'Jone', 18, '[email protected]' ),
( 2, 'Jack', 20, '[email protected]' ),
( 3, 'Tom', 28, '[email protected]' ),
( 4, 'Sandy', 21, '[email protected]' ),
( 5, 'Billie', 24, '[email protected]' );
新建一个工程
引入依赖
<!--简化pojo类开发-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!--mybatis-plus-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.1</version>
</dependency>
<!--mysql驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
安装lombok插件
第一个查询案例
首先新建一个实体类 pojo.User
@Data
public class User {
Integer id;
String name;
Integer age;
String email;
}
然后添加对应的mapper映射文件 mapper.UserMapper
/**
* BaseMapper 是 MybatisPlus 提供的一个基础接口类,有泛型
* @author songzx
* @create 2022-09-08 14:27
*/
public interface UserMapper extends BaseMapper<User> {
}
在启动类上添加 @MapperScan 自动扫描 mapper 接口
@SpringBootApplication
@MapperScan("com.szx.mybatisplusproduct.mapper")
public class MybatisPlusProductApplication {
public static void main(String[] args) throws UnknownHostException {
SpringApplication.run(MybatisPlusProductApplication.class, args);
}
}
添加测试文件
@SpringBootTest
public class UserTest1 {
@Autowired
UserMapper userMapper;
@Test
public void test1(){
List<User> userList = userMapper.selectList(null);
userList.forEach(item-> System.out.println("item = " + item));
}
}
运行测试方法查看返回
添加日志打印
在 application.yml
配置文件中添加如下配置即可
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
添加完成后会在控制台输入查询的sql语句信息
BaseMapper
BaseMapper提供了很多基础方法,我们可以使用提供的方法来实现一个表基本的增删改查
新增方法
@Test
public void testInsert(){
User user = new User();
user.setName("张三");
user.setAge(18);
user.setEmail("[email protected]");
// 返回的是受影响的行数
int insert = userMapper.insert(user); //=> 1
// 自动生成的id是根据雪花算法算出的
System.out.println(user.getId());
}
查看插入表中的结果
删除方法
根据id删除
// 根据id删除
@Test
public void testDeleteById(){
// 返回的是被删除的数量
int resDelCount = userMapper.deleteById(1);
System.out.println("resDelCount = " + resDelCount);
}
根据条件删除
// 根据条件删除
@Test
public void testDeleteByMap(){
// 将条件放在一个map中传给userMapper
HashMap<String, Object> map = new HashMap<>();
map.put("name","张三");
map.put("age",18);
int i = userMapper.deleteByMap(map);
System.out.println("i = " + i);
}
根据id集合批量删除数据
// 根据id集合批量删除数据
@Test
public void testBatchDelete(){
List<Integer> ids = Arrays.asList(2, 3);
int i = userMapper.deleteBatchIds(ids);
System.out.println("i = " + i);
}
修改方法
@Test
public void testUpdate(){
User user = new User();
user.setId(2);
user.setName("张三");
// 根据id修改数据
int i = userMapper.updateById(user);
System.out.println("i = " + i);
}
查询方法
根据id查询单条数据
User user = userMapper.selectById(2);
// SELECT id,name,age,email FROM user WHERE id=?
System.out.println("user = " + user);
根据id批量查询多条数据
List<Integer> ids = Arrays.asList(2, 3, 4);
// SELECT id,name,age,email FROM user WHERE id IN ( ? , ? , ? )
List<User> users = userMapper.selectBatchIds(ids);
System.out.println("users = " + users);
根据map查询
HashMap<String, Object> searchMap = new HashMap<>();
searchMap.put("name","Jack");
searchMap.put("age",20);
// SELECT id,name,age,email FROM user WHERE name = ? AND age = ?
List<User> users1 = userMapper.selectByMap(searchMap);
System.out.println("users1 = " + users1);
根据条件查询
这里要传入一个条件构造器,没有时可以传入一个null
List<User> userList = userMapper.selectList(null);
// SELECT id,name,age,email FROM user
userList.forEach(System.out::println);
查询单个值
这里也是传入条件构造器,没有则传入null
例如:查询数据的总数
Long total = userMapper.selectCount(null);
// SELECT COUNT( * ) FROM user
System.out.println("total = " + total);
自定义SQL方法
首先添加自定义的mapper映射文件,在MyBatisPlus中已经帮我们定义了mapper映射文件的位置,就是在配置文件目录中添加 mapper
文件夹,然后自动会读取以任何目录下,任何名字的 xml 结尾的映射文件
所以首先在 mapper 目录中新建一个映射文件,映射文件的名字和接口名字一样
然后在接口中添加自定义查询方法
@Repository
public interface UserMapper extends BaseMapper<User> {
// 自定义的查询方法,根据id查询返回一个map
Map<String,Object> mySelectMapById(Integer id);
}
添加映射sql
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.szx.mybatisplusproduct.mapper.UserMapper">
<!--Map<String,Object> mySelectMapById(Integer id);-->
<select id="mySelectMapById" resultType="map">
select * from user where id = #{id}
</select>
</mapper>
测试自定义的方法
@Test
public void testSelectMap(){
Map<String, Object> userMap = userMapper.mySelectMapById(2);
System.out.println("userMap = " + userMap);
}
测试查看运行结果
IService 和 ServiceImpl
- IService 是 mybatis-plus 提供的一个封装了常用CRUD的一个接口,ServiceImpl 是它的实现类
- 进一步封装CRUD采用如下的方式为前缀来区分各个方法,避免和 mapper 中的方法混淆
- get 单行查询
- remove 删除
- list 查询集合
- page 分页
创建Service接口和实现类
新建 UserService 接口,继承 IService
/**
* 通用server,自定义一个接口,使之继承 IService<T>
* @author songzx
* @create 2022-09-14 15:04
*/
public interface UserService extends IService<User> {
}
然后添加实现类,继承 ServiceImpl 类
/**
* 创建service实现类,继承mybatis-plus提供的实现类,然后实现UserService
* @author songzx
* @create 2022-09-14 15:06
*/
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService{
}
查询数据总条数
@Autowired
UserServiceImpl userService;
/**
* userService.count();
* 查询数据总数
*/
@Test
public void testGetCount(){
long count = userService.count();
System.out.println("count = " + count);
}
批量插入
由于sql语句有长度限制,海量的数据插入无法放在单条的sql中实现,所以在Service接口中实现
@Autowired
UserServiceImpl userService;
/**
* 测试批量插入
*/
@Test
public void testBatchSave(){
ArrayList<User> users = new ArrayList<>();
for (int i = 0; i < 5; i++) {
User user = new User();
user.setName("abc" + i);
user.setAge(20 + i);
users.add(user);
}
// 返回是否批量插入成功
boolean b = userService.saveBatch(users);
System.out.println("b = " + b);
}
批量删除
@Autowired
UserServiceImpl userService;
/**
* 测试批量删除
*/
@Test
public void testBatchRemove(){
List<Integer> ids = Arrays.asList(6, 7, 8, 9);
boolean b = userService.removeBatchByIds(ids);
System.out.println("b = " + b);
}
常用注解
@TableName
作用:指定实体类对应的表名
在上面的例子中,我们没有指定数据库的表名,只是在继承 BaseMapper 时指定了一个泛型 User,从而mybatis-plus就知道我们操作的是 user 表,如果我们数据库中的表名和实例类类名不一致,会怎么样呢?
我们修改数据库的表名为 t_user
然后执行查询方法
@Autowired
UserServiceImpl userService;
/**
* userService.count();
* 查询数据总数
*/
@Test
public void testGetCount(){
long count = userService.count();
System.out.println("count = " + count);
}
会出现如下错误
这时候我们可以在实体类上用 @TableName
来指明要操作的表名
@TableName("t_user")
@Data
public class User {
Integer id;
String name;
Integer age;
String email;
}
此时再来查询就可以正常查询了
全局配置表名前缀
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
# 配置表名的默认前缀
table-prefix: t_
@TableId
作用:指定字段为主键id
在上面的插入数据测试中,mybatis-plus会默认吧id当做主键,并根据雪花算法自动生成id,如果数据库中的主键不叫 id,则是否出出现问题呢?
现在修改表中的 id为 t_id
修改实体类中的 id 也为 t_id
@TableName("t_user")
@Data
public class User {
Integer t_id;
String name;
Integer age;
String email;
}
此时来执行一个新增操作
@Test
public void testSave(){
User user = new User();
user.setName("Jack");
user.setAge(18);
user.setEmail("[email protected]");
boolean save = userService.save(user);
System.out.println("save = " + save);
}
出现如下错误
这时我们使用 @TableId
来指定一个属性为主键
@TableName("t_user")
@Data
public class User {
Integer id;
@TableId
Integer t_id;
String name;
Integer age;
String email;
}
再次执行新建就会新建成功了
@TableId的value属性
若表中的主键名为 t_id,实体类中对应的还是 id,则会抛出如下异常,表示 mybatis-plus 依然吧 id 当做主键
这时就用 @TableId("t_id")
来指定主键的名称
@TableName("t_user")
@Data
public class User {
@TableId("t_id")
Integer id;
String name;
Integer age;
String email;
}
再次测试即可添加成功
@TableId的type属性
type属性主要用来定义主键策略
常用的策略
值 | 描述 |
---|---|
IdType.ASSIGN_ID(默认) | 基于雪花算法的策略生成数据id,与数据库id是否设置自增无关 |
IdType.AUTO | 使用数据库的自增策略,注意,该类型请确保数据库设置了id自增, |
测试使用数据库的主键递增策略
@TableName("t_user")
@Data
public class User {
@TableId(value = "t_id",type = IdType.AUTO)
Integer id;
String name;
Integer age;
String email;
}
现在数据库的最大id为6
执行一次新增方法
再次查看数据库记录,最新的记录id为7
@TableField
定义实体类中的属性对应的是表中的那个列。例如,我表中的有一个列为 user_name,而实体类中对应的却是 name,那么这种情况下新建会不会有问题呢?
现在吧数据库表中的列名改一下
实体类还是这样
@TableName("t_user")
@Data
public class User {
@TableId(value = "t_id",type = IdType.AUTO)
Integer id;
String name;
Integer age;
String email;
}
运行新增方法,出现如下错误,找不到 name 列
这时,可以用 @TableField
注解来声明这个属性对应哪一个列
@TableName("t_user")
@Data
public class User {
@TableId(value = "t_id",type = IdType.AUTO)
Integer id;
@TableField("user_name")
String name;
Integer age;
String email;
}
此时再来新建就可以了
驼峰命名规则
如果数据库中使用的是下划线的方式来命名字段,例如 user_name
,实体类中使用的是 userName
,则新建不会出错,这时因为 mybatis 会按照驼峰命名去匹配字段名称
@TableLogic
表示逻辑删除数据
- 物理删除:真正的从数据表中删除这条数据,无法恢复
- 逻辑删除:不是真正的从表中删除这个数据,只是根据一个状态来表示这个数据被删除,查询时不会查询状态是删除的数据
实现在表中新增一个字段,用来表示是否删除,注意要设置一个默认值为0,0表示未删除,1表示已删除
然后再实体类中添加对应的属性,并添加 @TableLogic
注解
@TableName("t_user")
@Data
public class User {
@TableId(value = "t_id",type = IdType.AUTO)
Integer id;
@TableField("user_name")
String name;
Integer age;
String email;
// 逻辑删除
@TableLogic
Integer isDeleted;
}
然后执行删除操作
@Test
public void testRemoveById(){
boolean b = userService.removeById(8);
System.out.println("b = " + b);
}
执行结果
通过执行结果可以看到,实际运行的SQL时修改操作,此时来看表中id为8的数据是否还在
通过结果可以看出id等于8的数据并没有被删除,只是吧状态改成了1
然后再来一下查询方法,是否会把已删除的数据查到
@Test
public void testGetUserList(){
List<User> userList = userService.list();
userList.forEach(System.out::println);
}
运行结果
会自动添加一个 is_deleted = 0 的查询条件
条件构造器和常用接口
wapper介绍
-
Wrapper : 条件构造抽象类,最顶端父类
-
AbstractWrapper : 用于查询条件封装,生成 sql 的 where 条件
-
QueryWrapper : 查询条件封装
-
UpdateWrapper : Update 条件封装
-
AbstractLambdaWrapper : 使用Lambda 语法
-
LambdaQueryWrapper :用于Lambda语法使用的查询Wrapper
-
LambdaUpdateWrapper : Lambda 更新封装Wrapper
-
-
-
QueryWrapper
组装查询条件
例如:
查询姓名包含 a,并且年龄在 20 到 30 岁之间,并且邮箱不为空的数据
@Test
public void test1(){
QueryWrapper<User> qw = new QueryWrapper<>();
// 第一个参数填数据库中对应的列名
qw.like("user_name","a")
.between("age",20,30)
.isNotNull("email");
List<User> userList = userService.list(qw);
userList.forEach(System.out::println);
}
查询结果中的SQL语句
SELECT t_id AS id,user_name AS name,age,email,is_deleted FROM t_user WHERE is_deleted=0 AND (user_name LIKE ? AND age BETWEEN ? AND ? AND email IS NOT NULL)
成功的查询到两条数据
组装排序条件
按照用户年龄降序排序,如果年龄相同,则按照id升序排序
@Test
public void test2(){
QueryWrapper<User> qw = new QueryWrapper<>();
// orderByDesc 降序,orderByAsc 升序
qw.orderByDesc("age").orderByAsc("t_id");
List<User> userList = userService.list(qw);
userList.forEach(System.out::println);
}
查询结果中的SQL语句
SELECT t_id AS id,user_name AS name,age,email,is_deleted FROM t_user WHERE is_deleted=0 ORDER BY age DESC,t_id ASC
返回结果
组装删除条件
删除邮箱为空的数据
@Test
public void test3(){
QueryWrapper<User> qw = new QueryWrapper<>();
qw.isNull("email");
boolean remove = userService.remove(qw);
System.out.println("remove = " + remove);
}
查询结果中的SQL语句
UPDATE t_user SET is_deleted=1 WHERE is_deleted=0 AND (email IS NULL)
返回结果
条件的优先级
将年龄大于20并且姓名中包含有a的或者邮箱为空的数据修改
@Test
public void test4(){
// 将年龄大于20并且姓名中包含有a的或者邮箱为空的数据修改
QueryWrapper<User> qw = new QueryWrapper<>();
qw.gt("age",20)
.like("user_name","a")
.or()
.isNull("email");
User user = new User();
user.setAge(18);
boolean update = userService.update(user, qw);
System.out.println("update = " + update);
}
运行的SQL
UPDATE t_user SET age=? WHERE is_deleted=0 AND (age > ? AND user_name LIKE ? OR email IS NULL)
使用 lambda 表达式
@Test
public void test5(){
QueryWrapper<User> qw = new QueryWrapper<>();
qw.like("user_name","a")
.and(i->i.gt("age",20).or().isNull("email"));
User user = new User();
user.setAge(18);
boolean update = userService.update(user, qw);
System.out.println("update = " + update);
}
运行的SQL,lambda 表达式内的逻辑优先运算
UPDATE t_user SET age=? WHERE is_deleted=0 AND (user_name LIKE ? AND (age > ? OR email IS NULL))
组装select子句
比如我只查询 user_name 和 age
@Test
public void test6(){
QueryWrapper<User> qw = new QueryWrapper<>();
qw.select("user_name","age");
// getMap 返回map集合,通常配合 select() 使用
Map<String, Object> map = userService.getMap(qw);
System.out.println("map = " + map);
}
SQL语句
SELECT user_name,age FROM t_user WHERE is_deleted=0
实现子查询
查询年龄小于等于20的数据
@Test
public void test7(){
String sql = "select age from t_user where age <= 20";
QueryWrapper<User> qw = new QueryWrapper<>();
qw.inSql("age",sql);
List<User> list = userService.list(qw);
list.forEach(System.out::println);
}
生成的SQL
SELECT t_id AS id,user_name AS name,age,email,is_deleted FROM t_user WHERE is_deleted=0 AND (age IN (select age from t_user where age <= 20))
查询结果
UpdateWrapper
@Test
public void test1(){
UpdateWrapper<User> up = new UpdateWrapper<>();
up.like("user_name","a")
.and(i->i.gt("age",20).or().isNull("email"))
.set("user_name","张三")
.set("email","[email protected]");
// 不需要再声明实体类
boolean update = userService.update(null, up);
System.out.println("update = " + update);
}
生成的SQL
UPDATE t_user SET user_name=?,email=? WHERE is_deleted=0 AND (user_name LIKE ? AND (age > ? OR email IS NULL))
运行结果
模拟开发场景中的条件查询
思路一:复杂版
@Test
public void test2(){
String user_name = "";
Integer ageStart = 15;
Integer ageEnd = 30;
QueryWrapper<User> qw = new QueryWrapper<>();
// 判断user_name不是null,不是空字符串,不是空白符
if(StringUtils.isNotBlank(user_name)){
qw.like("user_name",user_name);
}
if(ageStart != null){
// ge() 大于等于
qw.ge("age",ageStart);
}
if(ageEnd != null){
// le() 小于等于
qw.le("age",ageEnd);
}
List<User> list = userService.list(qw);
list.forEach(System.out::println);
}
生成的sql语句
SELECT t_id AS id,user_name AS name,age,email,is_deleted FROM t_user WHERE is_deleted=0 AND (age >= ? AND age <= ?)
返回的结果
这种方法过于繁琐,下面我们有更简单的方式来查询
思路二:condition
我们可以使用带condition参数的重载方法构建查询条件,简化代码的编写
@Test
public void test3(){
// 查询姓名包含a,年龄大于等于15小于等于30的数据
String user_name = "";
Integer ageStart = 15;
Integer ageEnd = 30;
QueryWrapper<User> qw = new QueryWrapper<>();
qw.like(StringUtils.isNotBlank(user_name),"user_name",user_name)
.ge(ageStart != null,"age",ageStart)
.le(ageEnd != null,"age",ageEnd);
List<User> list = userService.list(qw);
list.forEach(System.out::println);
}
LambdaQueryWrapper
使用 lambda 表达式来表示操作的列名,避免写时字符串导致出错
@Test
public void test(){
String user_name = "a";
Integer ageStart = 15;
Integer ageEnd = 30;
LambdaQueryWrapper<User> qw = new LambdaQueryWrapper<>();
qw.like(StringUtils.isNotBlank(user_name),User::getName,user_name)
.ge(ageStart != null,User::getAge,ageStart)
.le(ageEnd != null,User::getAge,ageEnd);
List<User> list = userService.list(qw);
list.forEach(System.out::println);
}
对应的sql
SELECT t_id AS id,user_name AS name,age,email,is_deleted FROM t_user WHERE is_deleted=0 AND (user_name LIKE ? AND age >= ? AND age <= ?)
查询结果
LambdaUpdateWrapper
@Test
public void test(){
// UPDATE t_user SET age=?,user_name=? WHERE is_deleted=0 AND (user_name LIKE ?)
// 将姓名中包含 a 的年龄改成22,姓名改成 updateWrapper
LambdaUpdateWrapper<User> up = new LambdaUpdateWrapper<>();
up.set(User::getAge,22)
.set(User::getName,"updateWrapper")
.like(User::getName,"a");
boolean update = userService.update(up);
System.out.println("update = " + update);
}
执行的SQL
UPDATE t_user SET age=?,user_name=? WHERE is_deleted=0 AND (user_name LIKE ?)
插件
分页插件
mybatis-plus 自带分页插件,通过简单的配置即可
首先新建配置类
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor(){
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
测试分页加条件查询
@Test
public void test(){
// 1.声明一个Page类,构造器有两个参数,第一个表示第几页,第二个表示每页显示几条数据
Page<User> userPage = new Page<>(1, 5);
// 2.创建查询wrapper
LambdaQueryWrapper<User> qw = new LambdaQueryWrapper<>();
qw.like(User::getName,"a");
// 3.传入分页查询和查询wrapper
Page<User> page = userService.page(userPage,qw);
// getRecords() 获取表中记录
List<User> userList = page.getRecords();
userList.forEach(System.out::println);
System.out.println("********分页信息********");
System.out.println("表中总数:" + page.getTotal());
System.out.println("是否有上一页:" + page.hasPrevious());
System.out.println("是否有下一页:" + page.hasNext());
System.out.println("当前第几页:" + page.getCurrent());
System.out.println("一共有几页:" + page.getPages());
System.out.println("每页显示的条数:" + page.getSize());
}
生成的SQL
SELECT t_id AS id,user_name AS name,age,email,is_deleted FROM t_user WHERE is_deleted=0 AND (user_name LIKE ?) LIMIT ?
查询结果
自定义分页查询方法
在 UserMapper 接口中新增接口
@Repository
public interface UserMapper extends BaseMapper<User> {
// 自定义分页查询方法
Page<User> selectMyPageVo(Page<User> page,Integer age);
}
在 UserMapper.xml 中添加接口映射
<!--Page<User> selectMyPageVo(Page<User> page,Integer age);-->
<sql id="baseColunm">t_id,user_name,email,age</sql>
<select id="selectMyPageVo" resultType="com.szx.mybatisplusproduct.pojo.User">
select <include refid="baseColunm"></include> from t_user where age >= #{age}
</select>
测试
@Autowired
UserMapper userMapper;
@Test
public void test2(){
Page<User> page = new Page<>(1, 5);
Page<User> userPage = userMapper.selectMyPageVo(page, 20);
List<User> records = userPage.getRecords();
records.forEach(System.out::println);
}
运行结果
图中的 id 和 name 都是null,这是因为 user 实体类中的属性和表中的字段不一致导致
乐观锁
现有如下场景:一件商品,成本价是80元,售价是100元。老板先是通知小李,说你去把商品价格增加50元。小李正在玩游戏,耽搁了一个小时。正好一个小时后,老板觉得商品价格增加到150元,价格太高,可能会影响销量。又通知小王,你把商品价格降低30元。此时,小李和小王同时操作商品后台系统。小李操作的时候,系统先取出商品价格100元;小王也在操作,取出的商品价格也是100元。小李将价格加了50元,并将100+50=150元存入了数据库;小王将商品减了30元,并将100-30=70元存入了数据库。是的,如果没有锁,小李的操作就完全被小王的覆盖了。现在商品价格是70元,比成本价低10元。几分钟后,这个商品很快出售了1千多件商品,老板亏1万多。
上面的故事,如果是乐观锁,小王保存价格前,会检查下价格是否被人修改过了。如果被修改过了,则重新取出的被修改后的价格,150元,这样他会将120元存入数据库。如果是悲观锁,小李取出数据后,小王只能等小李操作完之后,才能对价格进行操作,也会保证最终的价格是120元。
模拟上面的冲突
首先新建一个商品表并插入一条数据
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for t_product
-- ----------------------------
DROP TABLE IF EXISTS `t_product`;
CREATE TABLE `t_product` (
`id` int(0) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`name` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '商品名称',
`price` int(0) NULL DEFAULT 0 COMMENT '价格',
`version` int(0) NULL DEFAULT 0 COMMENT '乐观锁版本号',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of t_product
-- ----------------------------
INSERT INTO `t_product` VALUES (1, '外星人笔记本', 100, 0);
SET FOREIGN_KEY_CHECKS = 1;
新建对应实体类
package com.szx.mybatisplusproduct.pojo;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.Data;
/**
* 这里在配置文件中统一加了t_前缀,所以这里不用在声明 @TableName
* @author songzx
* @create 2022-09-16 13:34
*/
@Data
public class Product {
@TableId(type = IdType.AUTO)
Integer id;
String name;
Integer price;
Integer version;
}
添加 Mapper
public interface ProductMapper extends BaseMapper<Product> {
}
模拟上面的场景
@SpringBootTest
public class ProductTest {
@Autowired
ProductMapper productMapper;
@Test
public void test(){
// 小李从数据库中获取的商品
Product p1 = productMapper.selectById(1);
// 小王也在同一时间获取商品
Product p2 = productMapper.selectById(1);
// 小李将价格调高50元
p1.setPrice(p1.getPrice() + 50);
productMapper.updateById(p1);
// 小王将价格调低30元
p2.setPrice(p2.getPrice() - 30);
productMapper.updateById(p2);
// 最后的结果
Product p3 = productMapper.selectById(1);
System.out.println(p3);
}
}
最后获取的结果商品价格变成了 70 元
乐观锁的实现流程
数据库中添加version字段
取出记录时,获取当前version
SELECT id,`name`,price,`version` FROM product WHERE id=1
更新时,version + 1,如果where语句中的version版本不对,则更新失败
UPDATE product SET price=price+50, `version`=`version` + 1 WHERE id=1 AND `version`=1
MybatisPlus实现乐观锁
修改实体类,在版本号属性上添加 @Version
@Data
public class Product {
@TableId(type = IdType.AUTO)
Integer id;
String name;
Integer price;
// 在版本号属性上添加 @Version 注解
@Version
Integer version;
}
在配置类中添加乐观锁插件
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor(){
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 分页插件
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
// 乐观锁插件
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
return interceptor;
}
}
再次测试价格修改流程
首先小李查询价格
SELECT id,name,price,version FROM t_product WHERE id=?
查到价格为100
然后小王查询价格
SELECT id,name,price,version FROM t_product WHERE id=?
查到价格也是100
然后小李修改价格
UPDATE t_product SET name=?, price=?, version=? WHERE id=? AND version=?
并成功修改,同时版本号从1改成2
然后小王再去改版本号是1的价格
UPDATE t_product SET name=?, price=?, version=? WHERE id=? AND version=?
由于上面小李已经把版本号改成2了,这时候小王再去修改版本号是1的数据,自然没有修改成功
最后老板再去查看价格
SELECT id,name,price,version FROM t_product WHERE id=?
价格为150
优化价格修改流程
添加一个修改失败再次请求的机制
@SpringBootTest
public class ProductTest {
@Autowired
ProductMapper productMapper;
@Test
public void test(){
// 小李从数据库中获取的商品
Product p1 = productMapper.selectById(1);
// 小王也在同一时间获取商品
Product p2 = productMapper.selectById(1);
// 小李将价格调高50元
p1.setPrice(p1.getPrice() + 50);
productMapper.updateById(p1);
// 小王将价格调低30元
p2.setPrice(p2.getPrice() - 30);
int i = productMapper.updateById(p2);
if(i == 0){
// 如果没有修改成功则再次查询再次修改
p2 = productMapper.selectById(1);
p2.setPrice(p2.getPrice() - 30);
productMapper.updateById(p2);
}
// 最后的结果
Product p3 = productMapper.selectById(1);
System.out.println(p3);
}
}
查询返回结果为120元
注意:每次测试前都重新吧数据库中的价格还原成100
通用枚举
数据库中有些值的字段是固定的,例如性别,我们希望在修改这类值的时候可以从枚举类中来获取值并存入数据库
首先在 t_user 表中增加 sex 列
对应的实体类中添加映射sex属性
@TableName("t_user")
@Data
public class User {
@TableId(value = "t_id",type = IdType.AUTO)
Integer id;
@TableField("user_name")
String name;
Integer age;
String email;
// 逻辑删除
@TableLogic
Integer isDeleted;
Integer sex;
}
添加枚举类
@Getter
public enum SexEnum {
MALE(1,"男"),
FEMALE(2,"女");
@EnumValue // 标注了 @EnumValue 的注解会作为值保存在数据库中
Integer sex;
String sexName;
SexEnum(Integer sex,String sexName){
this.sex = sex;
this.sexName = sexName;
}
}
配置扫描通用枚举
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
# 配置表名的默认前缀
table-prefix: t_
# 配置扫描通用枚举
type-enums-package: com.szx.mybatisplusproduct.enums
测试修改方法
@Autowired
UserServiceImpl userService;
@Test
public void test(){
// 将姓名中包含a的数据的性别改成女
LambdaUpdateWrapper<User> up = new LambdaUpdateWrapper<>();
up.set(User::getSex, SexEnum.FEMALE)
.like(User::getName,"a");
boolean update = userService.update(up);
System.out.println(update);
}
生成的sql
UPDATE t_user SET sex=? WHERE is_deleted=0 AND (user_name LIKE ?)
运行结果
多数据源
适用于多个表的情况。我们创建两个库,分别为:mybatis_plus(以前的库不动)与mybatis_plus_1(新建),将mybatis_plus库的product表移动到mybatis_plus_1库,这样每个库一张表,通过一个测试用例分别获取用户数据与商品数据,如果获取到说明多库模拟成功
首先新建一个数据
引入依赖
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
<version>3.5.0</version>
</dependency>
重新配置数据源,要把原来的配置注释掉
server:
port: 8899
spring:
application:
# 当前项目的名称
name: MyBatisPlus学习项目
datasource:
dynamic:
# 设置默认的数据源或者数据源组,默认值即为master
primary: master
# 严格匹配数据源,默认为false,true为匹配到指定数据源是抛异常,false使用默认的数据源
strict: false
datasource:
master:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/mybatis_plus?serverTimezone=GMT%2B8&characterEncoding=utf-8
username: root
password: abc123
slave_1:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/mybatis_plus_1?serverTimezone=GMT%2B8&characterEncoding=utf-8
username: root
password: abc123
创建 UserService
@DS("master") // 指定要操作的数据源
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService{
}
创建 ProductService
@DS("slave_1")
@Service
public class ProductServiceImp extends ServiceImpl<ProductMapper, Product> implements ProductService {
}
测试查询
@SpringBootTest
public class TestStarter {
@Autowired
UserServiceImpl userService;
@Autowired
ProductServiceImp productService;
@Test
public void test(){
// 查询用户信息
List<User> userList = userService.list();
userList.forEach(System.out::println);
System.out.println("********");
// 查询商品信息
List<Product> productList = productService.list();
productList.forEach(System.out::println);
}
}
返回的结果
可以看出两个数据库中的数据都被成功的查询到
代码生成器
首先安装插件
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.5.1</version>
</dependency>
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
<version>2.3.31</version>
</dependency>
然后直接运行下面的代码即可自动根据表来生成代码
package com.szx.mybatisplusproduct;
import com.baomidou.mybatisplus.generator.FastAutoGenerator;
import com.baomidou.mybatisplus.generator.config.OutputFile;
import com.baomidou.mybatisplus.generator.engine.FreemarkerTemplateEngine;
import java.util.Collections;
public class FastAutoGeneratorTest {
public static void main(String[] args) {
FastAutoGenerator.create("jdbc:mysql://127.0.0.1:3306/mybatis_plus? characterEncoding=utf-8&userSSL=false", "root", "abc123")
.globalConfig(builder -> {
builder.author("szx") // 设置作者
// .enableSwagger() // 开启 swagger 模式
.fileOverride() // 覆盖已生成文件
.outputDir("D://0000学习文件//MyBatisPlus"); // 指定输出目录
})
.packageConfig(builder -> {
builder.parent("com.baomidou.mybatisplus.samples.generator") // 设置父包名
.moduleName("system") // 设置父包模块名
.pathInfo(Collections.singletonMap(OutputFile.mapperXml, "D://0000学习文件//MyBatisPlus")); // 设置mapperXml生成路径
})
.strategyConfig(builder -> {
builder.addInclude("t_user") // 设置需要生成的表名
.addTablePrefix("t_", "c_"); // 设置过滤表前缀
})
.templateEngine(new FreemarkerTemplateEngine()) // 使用Freemarker引擎模板,默认的是Velocity引擎模板
.execute();
}
}
运行结果
MyBatisX插件
MyBatis-Plus为我们提供了强大的mapper和service模板,能够大大的提高开发效率但是在真正开发过程中,MyBatis-Plus并不能为我们解决所有问题,例如一些复杂的SQL,多表联查,我们就需要自己去编写代码和SQL语句,我们该如何快速的解决这个问题呢,这个时候可以使用MyBatisX插件MyBatisX一款基于 IDEA 的快速开发插件,为效率而生。
官方用法:https://baomidou.com/pages/ba5b24/
插件安装
搜索 mybatisx 并安装
配置数据源
快速生成实体类,Mapper
自动生成的文件