MyBatisPlus简介

MyBatisPlus是MyBatis增强工具,在MyBatis基础上只做增强不做改变,为简化开发、提高效率而生。MyBatisPlus提供通用的mapper和service,可以在不编写任何SQL语句的情况下,快速实现对单表的CRUD、批量、逻辑删除、分页等操作。

HelloWorld

开发环境

IDE:idea 2021

JDK:JDK8+

构建工具:maven 3.5.4

MySQL版本:MySQL 8

Spring Boot:2.7.1

MyBatis-Plus:3.5.1

创建工程

image-20220710135924623

image-20220710140410253

安装插件

image-20220710140943059

引入依赖

1
2
3
4
5
6
7
8
9
10
11
12
<!-- 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>
<version>8.0.11</version>
</dependency>

配置application.yml

1
2
3
4
5
6
spring:
datasource:
url: jdbc:mysql://localhost:3306/db_mybatisplus?useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver

测试

image-20220710155330549

1
2
3
4
5
6
7
8
9
10
11
// 实体类
@Data
@NoArgsConstructor
@AllArgsConstructor
@TableName("t_user")
public class User {
private Long id;
private String name;
private Integer age;
private String email;
}
1
2
3
4
// mapper接口
@Mapper
public interface UserMapper extends BaseMapper<User> {
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 测试类
@SpringBootTest
class MybatisplusApplicationTests {

@Autowired
private UserMapper userMapper;

@Test
void test1() {
List<User> users = userMapper.selectList(null);
System.out.println(users);
// [User(id=1, name=张三, age=11, email=11@qq.com), User(id=2, name=李四, age=22, email=22@qq.com), User(id=3, name=王五, age=33, email=33@qq.com)]
}
}

加入日志功能

1
2
3
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

image-20220710144411579

基本CRUD

BaseMapper

MyBatis-Plus中的基本CRUD在内置的BaseMapper中都已得到了实现,可以直接使用。

插入

1
2
3
4
5
6
7
8
@Test
public void testInsert(){
// INSERT INTO t_user ( id, name, age, email ) VALUES ( ?, ?, ?, ? )
User user = new User(null, "赵六", 23, "zhaoliu@qq.com");
int result = userMapper.insert(user);
System.out.println("受影响行数:"+result);
System.out.println("id自动获取:"+user.getId()); // 1546026996496371714
}

最终执行的结果,所获取的id为1546026996496371714

这是因为MyBatis-Plus在实现插入数据时,会默认基于雪花算法的策略生成id

删除

  1. 通过id删除记录

    1
    2
    3
    4
    5
    6
    @Test
    public void testDeleteById(){
    // DELETE FROM t_user WHERE id=?
    int result = userMapper.deleteById(1546026996496371714L); // 注意要加L
    System.out.println("受影响行数:"+result); // 1
    }
  2. 通过id批量删除记录

    1
    2
    3
    4
    5
    6
    7
    @Test
    public void testDeleteBatchIds(){
    List<Long> idList = Arrays.asList(2L, 3L);
    // DELETE FROM t_user WHERE id IN ( ? , ? )
    int result = userMapper.deleteBatchIds(idList);
    System.out.println("受影响行数:"+result); // 2
    }
  3. 通过map条件删除记录

    1
    2
    3
    4
    5
    6
    7
    8
    9
    @Test
    public void testDeleteByMap(){
    Map<String, Object> map = new HashMap<>();
    map.put("name", "张三");
    map.put("age", 23);
    // DELETE FROM t_user WHERE name = ? AND age = ?
    int result = userMapper.deleteByMap(map);
    System.out.println("受影响行数:"+result); // 1
    }

修改

1
2
3
4
5
6
7
@Test
public void testUpdateById(){
User user = new User(4L, "赵六", 22, null);
// UPDATE t_user SET name=?, age=? WHERE id=?
int result = userMapper.updateById(user);
System.out.println("受影响行数:"+result); // 1
}

查询

  1. 根据id查询记录

    1
    2
    3
    4
    5
    6
    @Test
    public void testSelectById(){
    // SELECT id,name,age,email FROM t_user WHERE id=?
    User user = userMapper.selectById(4L);
    System.out.println(user); // User(id=4, name=赵六, age=22, email=zhangsan@qq.com)
    }
  2. 根据多个id查询多条记录

    1
    2
    3
    4
    5
    6
    7
    8
    9
    @Test
    public void testSelectBatchIds(){
    List<Long> idList = Arrays.asList(4L, 5L);
    // SELECT id,name,age,email FROM t_user WHERE id IN ( ? , ? )
    List<User> list = userMapper.selectBatchIds(idList);
    list.forEach(System.out::println);
    // User(id=4, name=赵六, age=22, email=zhangsan@qq.com)
    // User(id=5, name=张三, age=20, email=zhangsan@qq.com)
    }
  3. 通过map条件查询记录

    1
    2
    3
    4
    5
    6
    7
    8
    9
    @Test
    public void testSelectByMap(){
    Map<String, Object> map = new HashMap<>();
    map.put("name", "赵六");
    map.put("age", 22);
    // SELECT id,name,age,email FROM t_user WHERE name = ? AND age = ?
    List<User> list = userMapper.selectByMap(map);
    list.forEach(System.out::println); // User(id=4, name=赵六, age=22, email=zhangsan@qq.com)
    }
  4. 查询所有记录

    1
    2
    3
    4
    5
    6
    7
    8
    9
    @Test
    public void testSelectList(){
    // SELECT id,name,age,email FROM t_user
    List<User> list = userMapper.selectList(null);
    list.forEach(System.out::println);

    // User(id=4, name=赵六, age=22, email=zhangsan@qq.com)
    // User(id=5, name=张三, age=20, email=zhangsan@qq.com)
    }

通用Service

  • 通用 Service CRUD 封装IService接口,进一步封装 CRUD 采用 get 查询单行 remove 删除 list 查询集合 page 分页 前缀命名方式区分 Mapper 层避免混淆
  • 泛型 T 为任意实体对象
  • 建议如果存在自定义通用 Service 方法的可能,请创建自己的 IBaseService 继承 Mybatis-Plus 提供的基类
  • 官网地址

IService

MyBatis-Plus中有一个接口 IService和其实现类 ServiceImpl,封装了常见的业务层逻辑 详情查看源码IService和ServiceImpl

创建Service接口和实现类

1
2
3
4
5
/**
* UserService继承IService模板提供的基础功能
*/
public interface UserService extends IService<User> {
}
1
2
3
4
5
6
7
/**
* ServiceImpl实现了IService,提供了IService中基础功能的实现
* 若ServiceImpl无法满足业务需求,则可以使用自定的UserService定义方法,并在实现类中实现
*/
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
}

测试

  1. 查询记录数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    @Autowired
    private UserService userService;

    @Test
    public void testGetCount(){
    // SELECT COUNT( * ) FROM t_user
    long count = userService.count();
    System.out.println("总记录数:" + count);
    }
  2. 测试批量插入

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    @Test
    public void testSaveBatch(){
    // SQL长度有限制,海量数据插入单条SQL无法实行,
    // 因此MP将批量插入放在了通用Service中实现,而不是通用Mapper
    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);
    }
    // INSERT INTO t_user ( id, name, age ) VALUES ( ?, ?, ? )
    userService.saveBatch(users);
    }

常用注解

@TableName

在实体类类型上添加@TableName(“t_user”),标识实体类对应的表。

1
2
3
4
5
6
7
@TableName("t_user")
public class User {
private Long id;
private String name;
private Integer age;
private String email;
}

也可以通过全局配置文件解决

1
2
3
4
5
mybatis-plus:
global-config:
db-config:
# 配置MyBatis-Plus操作表的默认前缀
table-prefix: t_

@TableId

经过以上的测试,MyBatis-Plus在实现CRUD时,会默认将id作为主键列,并在插入数据时,默认基于雪花算法的策略生成id

在实体类中属性上通过@TableId标识为主键

1
2
3
4
5
6
7
8
@TableName("t_user")
public class User {
@TableId
private Long id;
private String name;
private Integer age;
private String email;
}

value属性

若实体类中主键对应的属性为id,而表中表示主键的字段为uid,此时需要通过@TableId注解的value属性,指定表中的主键字段,@TableId(“uid”) 或 @TableId(value=”uid”)

type属性

type属性用来定义主键策略

描述
IdType.ASSIGN_ID(默 认) 基于雪花算法的策略生成数据id,与数据库id是否设置自增无关
IdType.AUTO 使用数据库的自增策略,注意,该类型请确保数据库设置了id自增, 否则无效

配置全局主键策略:

1
2
3
4
5
mybatis-plus:
global-config:
db-config:
# 配置MyBatis-Plus的主键策略
id-type: auto

@TableField

MyBatis-Plus在执行SQL语句时,要保证实体类中的属性名和表中的字段名一致。

注意:

  • 若实体类中的属性使用的是驼峰命名风格,而表中的字段使用的是下划线命名风格 例如实体类属性userName,表中字段user_name 此时MyBatis-Plus会自动将下划线命名风格转化为驼峰命名风格
  • 若实体类中的属性和表中的字段不满足上述情况,例如实体类属性name,表中字段username 此时需要在实体类属性上使用@TableField(“username”)设置属性所对应的字段名
1
2
3
4
5
6
7
8
9
@TableName("t_user")
public class User {
@TableId
private Long id;
@TableField("username")
private String name;
private Integer age;
private String email;
}

@TableLogic

逻辑删除

  • 物理删除:真实删除,将对应数据从数据库中删除,之后查询不到此条被删除的数据
  • 逻辑删除:假删除,将对应数据中代表是否被删除字段的状态修改为“被删除状态”,之后在数据库 中仍旧能看到此条数据记录
  • 使用场景:可以进行数据恢复

实现逻辑删除

  1. 数据库中创建逻辑删除状态列,设置默认值为0

    image-20220710160341458

  2. 实体类中添加逻辑删除属性

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    @TableName("t_user")
    public class User {
    @TableId
    private Long id;
    private String name;
    private Integer age;
    private String email;
    @TableLogic // 逻辑删除
    private Integer isDeleted;
    }
  3. 测试

    • 测试删除功能,真正执行的是修改,将is_deleted改成1

    • 测试查询功能,被逻辑删除的数据默认不会被查询,结果为null

条件构造器和常用接口

wapper介绍

  • Wrapper : 条件构造抽象类,最顶端父类
    • AbstractWrapper : 用于查询条件封装,生成 sql 的 where 条件
      • QueryWrapper : 查询条件封装
      • UpdateWrapper : Update 条件封装
      • AbstractLambdaWrapper : 使用Lambda 语法
        • LambdaQueryWrapper :用于Lambda语法使用的查询Wrapper
        • LambdaUpdateWrapper : Lambda 更新封装Wrapper

QueryWrapper

组装查询条件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Test
public void test01(){
//查询用户名包含三,年龄在20到30之间,并且邮箱不为null的用户信息
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.like("name", "三")
.between("age", 20, 30)
.isNotNull("email");

// SELECT id,name,age,email,is_deleted FROM t_user WHERE (name LIKE ? AND age BETWEEN ? AND ? AND email IS NOT NULL)
List<User> list = userMapper.selectList(queryWrapper);

// User(id=5, name=张三, age=20, email=zhangsan@qq.com, isDeleted=0)
list.forEach(System.out::println);
}

组装排序条件

1
2
3
4
5
6
7
8
9
10
11
@Test
public void test02(){
//按年龄降序查询用户,如果年龄相同则按id升序排列
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper
.orderByDesc("age")
.orderByAsc("id");
// SELECT id,name,age,email,is_deleted FROM t_user ORDER BY age DESC,id ASC
List<User> users = userMapper.selectList(queryWrapper);
users.forEach(System.out::println);
}

组装删除条件

1
2
3
4
5
6
7
8
9
@Test
public void test03(){
//删除email为空的用户
//DELETE FROM t_user WHERE (email IS NULL)
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.isNull("email");
int result = userMapper.delete(queryWrapper);
System.out.println("受影响的行数:" + result);
}

条件的优先级

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Test
public void test0401() {
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
//将(年龄大于10并且用户名中包含有三)或 邮箱为null的用户信息修改
queryWrapper
.like("name", "三")
.gt("age", 10)
.or()
.isNull("email");
User user = new User();
user.setAge(18);
user.setEmail("user@qq.com");
// UPDATE t_user SET age=?, email=? WHERE (name LIKE ? AND age > ? OR email IS NULL)
int result = userMapper.update(user, queryWrapper);
System.out.println("受影响的行数:" + result);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Test
public void test0402() {
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
//将用户名中包含有三并且(年龄大于10或邮箱为null)的用户信息修改

//lambda表达式内的逻辑优先运算
queryWrapper
.like("name", "三")
.and(i -> i.gt("age", 10).or().isNull("email"));
User user = new User();
user.setAge(20);
user.setEmail("zhangsan@qq.com");
// UPDATE t_user SET age=?, email=? WHERE (name LIKE ? AND (age > ? OR email IS NULL))
int result = userMapper.update(user, queryWrapper);
System.out.println("受影响的行数:" + result);
}

组装select子句

1
2
3
4
5
6
7
8
9
10
@Test
public void test05() {
//查询用户信息的name和age字段
//SELECT name,age FROM t_user
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.select("name", "age");
//selectMaps()返回Map集合列表,通常配合select()使用,避免User对象中其它列值为null
List<Map<String, Object>> maps = userMapper.selectMaps(queryWrapper);
maps.forEach(System.out::println);
}

实现子查询

1
2
3
4
5
6
7
8
9
@Test
public void test06() {
//查询id小于等于4的用户信息
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.inSql("id", "select id from t_user where id <= 4");
// SELECT id,name,age,email FROM t_user WHERE (id IN (select id from t_user where id <= 4))
List<User> list = userMapper.selectList(queryWrapper);
list.forEach(System.out::println);
}

UpdateWrapper

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Test
public void test07() {
// 将(年龄大于20或邮箱为null)并且用户名中包含有六的用户信息修改
// 组装set子句以及修改条件
UpdateWrapper<User> updateWrapper = new UpdateWrapper<>();
// lambda表达式内的逻辑优先运算
updateWrapper
.set("age", 18)
.set("email", "user@qq.com")
.like("name", "六")
.and(i -> i.gt("age", 20).or().isNull("email"));
// UPDATE t_user SET age=?,email=? WHERE (name LIKE ? AND (age > ? OR email IS NULL))
// 如果没有自动填充,可以设置为null
int result = userMapper.update(null, updateWrapper);
System.out.println(result);
}

condition

在真正开发的过程中,组装条件是常见的功能,而这些条件数据来源于用户输入,是可选的,因此我们在组装这些条件时,必须先判断用户是否选择了这些条件,若选择则需要组装该条件

思路一

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Test
public void test08() {
//定义查询条件,有可能为null(用户未输入或未选择)
String name = null;
Integer ageBegin = 10;
Integer ageEnd = 24;
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
//StringUtils.isNotBlank()判断某字符串是否不为空且长度不为0且不由空白符(whitespace)构成
if(StringUtils.isNotBlank(name)){
queryWrapper.like("name","sss");
}
if(ageBegin != null){
queryWrapper.ge("age", ageBegin);
}
if(ageEnd != null){
queryWrapper.le("age", ageEnd);
}
// SELECT id,name,age,email FROM t_user WHERE (age >= ? AND age <= ?)
List<User> users = userMapper.selectList(queryWrapper);
users.forEach(System.out::println);
}

思路二

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Test
public void test08UseCondition() {
//定义查询条件,有可能为null(用户未输入或未选择)
String name = null;
Integer ageBegin = 10;
Integer ageEnd = 24;
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
//StringUtils.isNotBlank()判断某字符串是否不为空且长度不为0且不由空白符(whitespace)构成
queryWrapper
.like(StringUtils.isNotBlank(name), "name", "sss")
.ge(ageBegin != null, "age", ageBegin)
.le(ageEnd != null, "age", ageEnd);
// SELECT id,name,age,email FROM t_user WHERE (age >= ? AND age <= ?)
List<User> users = userMapper.selectList(queryWrapper);
users.forEach(System.out::println);
}

LambdaQueryWrapper

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Test
public void test09() {
//定义查询条件,有可能为null(用户未输入)
String name = null;
Integer ageBegin = 10;
Integer ageEnd = 24;
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
//避免使用字符串表示字段,防止运行时错误
queryWrapper
.like(StringUtils.isNotBlank(name), User::getName, "sss")
.ge(ageBegin != null, User::getAge, ageBegin)
.le(ageEnd != null, User::getAge, ageEnd);
// SELECT id,name,age,email FROM t_user WHERE (age >= ? AND age <= ?)
List<User> users = userMapper.selectList(queryWrapper);
users.forEach(System.out::println);
}

LambdaUpdateWrapper

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Test
public void test10() {
//组装set子句
LambdaUpdateWrapper<User> updateWrapper = new LambdaUpdateWrapper<>();
updateWrapper
.set(User::getAge, 21)
.set(User::getEmail, "liu@qq.com")
.like(User::getName, "六")
.and(i -> i.lt(User::getAge, 24).or().isNull(User::getEmail)); //lambda
User user = new User();
// UPDATE t_user SET age=?,email=? WHERE (name LIKE ? AND (age < ? OR email IS NULL))
int result = userMapper.update(user, updateWrapper);
System.out.println("受影响的行数:" + result);
}

插件

分页插件

MyBatis Plus自带分页插件,只要简单的配置即可实现分页功能

  1. 添加配置类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    @Configuration
    // @MapperScan("com.cuc.mybatisplus.mapper") //可以将主类中的注解移到此处
    public class MybatisPlusConfig {

    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
    MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
    interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
    return interceptor;
    }
    }
  2. 测试

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    @Test
    public void testPage(){
    //设置分页参数
    Page<User> page = new Page<>(1, 5);
    userMapper.selectPage(page, null);
    //获取分页数据
    List<User> list = page.getRecords();
    list.forEach(System.out::println);
    System.out.println("当前页:"+page.getCurrent());
    System.out.println("每页显示的条数:"+page.getSize());
    System.out.println("总记录数:"+page.getTotal());
    System.out.println("总页数:"+page.getPages());
    System.out.println("是否有上一页:"+page.hasPrevious());
    System.out.println("是否有下一页:"+page.hasNext());
    }

    image-20220710225352782

xml自定义分页

  1. UserMapper中定义接口方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    @Mapper
    public interface UserMapper extends BaseMapper<User> {
    /**
    * 根据年龄查询用户列表,分页显示
    * @param page 分页对象,xml中可以从里面进行取值,传递参数 Page 即自动分页,必须放在第一位
    * @param age 年龄
    */
    Page<User> selectPageVo(@Param("page") Page<User> page, @Param("age") Integer age);

    }
  2. UserMapper.xml中编写SQL

    1
    2
    3
    4
    5
    6
    7
    <!--SQL片段-->
    <sql id="BaseColumns">id,name,age,email</sql>

    <!-- Page<User> selectPageVo(@Param("page") Page<User> page, @Param("age") Integer age); -->
    <select id="selectPageVo" resultType="com.cuc.mybatisplus.pojo.User">
    SELECT <include refid="BaseColumns"></include> FROM t_user WHERE age > #{age}
    </select>
  3. 测试

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    @Test
    public void testSelectPageVo(){
    //设置分页参数
    Page<User> page = new Page<>(1, 5);
    userMapper.selectPageVo(page, 10);
    //获取分页数据
    List<User> list = page.getRecords();
    list.forEach(System.out::println);
    System.out.println("当前页:"+page.getCurrent());
    System.out.println("每页显示的条数:"+page.getSize());
    System.out.println("总记录数:"+page.getTotal());
    System.out.println("总页数:"+page.getPages());
    System.out.println("是否有上一页:"+page.hasPrevious());
    System.out.println("是否有下一页:"+page.hasNext());
    }

    image-20220710230544573

乐观锁

场景

一件商品,成本价是80元,售价是100元。老板先是通知小刘,说你去把商品价格增加50元。小刘正在玩游戏,耽搁了一个小时。正好一个小时后,老板觉得商品价格增加到150元,价格太高,可能会影响销量。又通知小王,你把商品价格降低30元。

此时,小刘和小王同时操作商品后台系统。小刘操作的时候,系统先取出商品价格100元;小王 也在操作,取出的商品价格也是100元。小刘将价格加了50元,并将100+50=150元存入了数据库;小王将商品减了30元,并将100-30=70元存入了数据库。是的,如果没有锁,小刘的操作就完全被小王的覆盖了。

现在商品价格是70元,比成本价低10元。几分钟后,这个商品很快出售了1千多件商品,老板亏1万多。

乐观锁与悲观锁

上面的故事,如果是乐观锁,小王保存价格前,会检查下价格是否被人修改过了。如果被修改过了,则重新取出的被修改后的价格,150元,这样他会将120元存入数据库。

如果是悲观锁,小刘取出数据后,小王只能等小刘操作完之后,才能对价格进行操作,也会保证最终的价格是120元。

模拟修改冲突

  1. 数据库表

    image-20220710235205109

  2. 实体类

    1
    2
    3
    4
    5
    6
    7
    8
    @Data
    @TableName("t_product")
    public class Product {
    private long id;
    private String name;
    private Integer price;
    private Integer version;
    }
  3. mapper接口

    1
    2
    3
    4
    @Mapper
    public interface ProductMapper extends BaseMapper<Product> {

    }
  4. 测试

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    @Test
    public void testConcurrentUpdate() {
    //1、小刘
    Product p1 = productMapper.selectById(1L);
    System.out.println("小刘取出的价格:" + p1.getPrice()); // 100
    //2、小王
    Product p2 = productMapper.selectById(1L);
    System.out.println("小王取出的价格:" + p2.getPrice()); // 100
    //3、小刘将价格加了50元,存入了数据库
    p1.setPrice(p1.getPrice() + 50);
    productMapper.updateById(p1);
    System.out.println("小刘修改结果:" + productMapper.selectById(1L)); // 150
    //4、小王将商品减了30元,存入了数据库
    p2.setPrice(p2.getPrice() - 30);
    productMapper.updateById(p2);
    System.out.println("小王修改结果:" + productMapper.selectById(1L)); // 70
    //最后的结果
    Product p3 = productMapper.selectById(1L);
    //价格覆盖,最后的结果:70
    System.out.println("最后的结果:" + p3.getPrice());
    }

乐观锁实现流程

  1. 取出记录时,获取当前version

    1
    SELECT id,`name`,price,`version` FROM product WHERE id=1
  2. 更新时,version + 1,如果where语句中的version版本不对,则更新失败

    1
    UPDATE product SET price=price+50, `version`=`version` + 1 WHERE id=1 AND `version`=1

Mybatis-Plus实现乐观锁

  1. 修改实体类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    @Data
    @TableName("t_product")
    public class Product {
    private long id;
    private String name;
    private Integer price;
    @Version
    private Integer version;
    }
  2. 添加乐观锁插件配置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
    MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
    //添加分页插件
    interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
    //添加乐观锁插件
    interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
    return interceptor;
    }
  3. 测试

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    @Test
    public void testConcurrentVersionUpdate() {
    //小刘取数据
    Product p1 = productMapper.selectById(1L);
    //小王取数据
    Product p2 = productMapper.selectById(1L);
    //小刘修改 + 50
    p1.setPrice(p1.getPrice() + 50);
    // UPDATE t_product SET name=?, price=?, version=1 WHERE id=? AND version=0
    productMapper.updateById(p1);
    //小王修改 - 30
    p2.setPrice(p2.getPrice() - 30);
    // UPDATE t_product SET name=?, price=?, version=1 WHERE id=? AND version=0
    int result = productMapper.updateById(p2);
    if(result == 0){
    //失败重试,重新获取version并更新
    p2 = productMapper.selectById(1L);
    p2.setPrice(p2.getPrice() - 30);
    // UPDATE t_product SET name=?, price=?, version=2 WHERE id=? AND version=1
    productMapper.updateById(p2);
    }
    //老板看价格
    Product p3 = productMapper.selectById(1L);
    System.out.println("老板看价格:" + p3.getPrice());
    }

通用枚举

表中的有些字段值是固定的,例如性别(男或女),此时我们可以使用MyBatis-Plus的通用枚举来实现

  1. 数据库表添加字段

    image-20220711003353022

  2. 创建通用枚举类型

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    @Getter
    public enum SexEnum {

    MALE(1, "男"),
    FEMALE(2, "女");

    @EnumValue
    private Integer sex;

    private String sexName;

    SexEnum(Integer sex, String sexName) {
    this.sex = sex;
    this.sexName = sexName;
    }
    }
  3. 配置扫描通用枚举

    1
    2
    3
    mybatis-plus:
    # 配置扫描通用枚举
    type-enums-package: com.cuc.mybatisplus.enums
  4. 修改实体类

    1
    2
    // 添加sex属性
    private SexEnum sex;
  5. 测试

    1
    2
    3
    4
    5
    6
    7
    8
    9
    @Test
    public void testSexEnum(){
    User user = new User();
    user.setName("Enum");
    user.setAge(20);
    //设置性别信息为枚举项,会将@EnumValue注解所标识的属性值存储到数据库
    user.setSex(SexEnum.MALE);
    userMapper.insert(user);
    }

代码生成器

相比mybatis的逆向工程,能逆向生成更多的东西。比如控制层、业务层、持久层等

  1. 引入依赖

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <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>
  2. 快速生成

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    public class FastAutoGeneratorTest {

    public static void main(String[] args) {
    FastAutoGenerator
    .create("jdbc:mysql://localhost:3306/db_mybatisplus?useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8", "root", "root")
    .globalConfig(builder -> {
    builder.author("swj") // 设置作者
    //.enableSwagger() // 开启 swagger 模式
    .fileOverride() // 覆盖已生成文件
    .outputDir("C:\\Users\\usesr\\Desktop\\test"); // 指定输出目录
    })
    .packageConfig(builder -> {
    builder.parent("com.cuc") // 设置父包名
    .moduleName("mybatisplus") // 设置父包模块名
    .pathInfo(Collections.singletonMap(OutputFile.mapperXml, "C:\\Users\\usesr\\Desktop\\test")); // 设置mapperXml生成路径
    })
    .strategyConfig(builder -> {
    builder.addInclude("t_user") // 设置需要生成的表名
    .addTablePrefix("t_", "c_"); // 设置过滤表前缀
    })
    .templateEngine(new FreemarkerTemplateEngine()) // 使用Freemarker 引擎模板,默认的是Velocity引擎模板
    .execute();
    }
    }

    image-20220711005237285

image-20220711005247180

多数据源

适用于多种场景:纯粹多库、 读写分离、 一主多从、 混合模式等 目前我们就来模拟一个纯粹多库的一个场景,其他场景类似

场景说明:

我们创建两个库,分别为db_mybatisplus(以前的库不动)与db_mybatisplus_1(新建),通过一个测试用例分别获取db_mybatisplus里的用户数据与db_mybatisplus_1里的商品数据,如果获取到说明多库模拟成功

  1. 数据库表

    image-20220711011822238

  2. 引入依赖

    1
    2
    3
    4
    5
    <dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
    <version>3.5.0</version>
    </dependency>
  3. 配置多数据源

    说明:注释掉之前的数据库连接,添加新配置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    spring:
    datasource:
    # url: jdbc:mysql://localhost:3306/db_mybatisplus?useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8
    # username: root
    # password: root
    # driver-class-name: com.mysql.cj.jdbc.Driver
    dynamic:
    # 设置默认的数据源或者数据源组,默认值即为master
    primary: master
    # 严格匹配数据源,默认false.true未匹配到指定数据源时抛异常,false使用默认数据源
    strict: false
    datasource:
    master:
    url: jdbc:mysql://localhost:3306/db_mybatisplus?useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8
    username: root
    password: root
    driver-class-name: com.mysql.cj.jdbc.Driver
    slave_1:
    url: jdbc:mysql://localhost:3306/db_mybatisplus_1?useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8
    username: root
    password: root
    driver-class-name: com.mysql.cj.jdbc.Driver
  4. 创建用户service

    1
    2
    public interface UserService extends IService<User> {
    }
    1
    2
    3
    4
    @Service
    @DS("master") //指定操作的数据源
    public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
    }
  5. 创建商品service

    1
    2
    public interface ProductService extends IService<Product> {
    }
    1
    2
    3
    4
    @Service
    @DS("slave_1")
    public class ProductServiceImpl extends ServiceImpl<ProductMapper, Product> implements ProductService {
    }
  6. 测试

    1
    2
    3
    4
    5
    @Test
    public void testDynamicDataSource(){
    System.out.println(userService.getById(4L));
    System.out.println(productService.getById(1L));
    }

MyBatisX插件

MyBatis-Plus为我们提供了强大的mapper和service模板,能够大大的提高开发效率

但是在真正开发过程中,MyBatis-Plus并不能为我们解决所有问题,例如一些复杂的SQL,多表联查,我们就需要自己去编写代码和SQL语句,我们该如何快速的解决这个问题呢,这个时候可以使用MyBatisX插件

MyBatisX一款基于 IDEA 的快速开发插件,为效率而生。

MyBatisX插件用法:https://baomidou.com/pages/ba5b24/

image-20220711012537145