文档 官方文档: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 操作智能分析阻断,也可自定义拦截规则,预防误操作
入门案例 创建数据库 1 2 3 4 5 6 7 8 9 10 CREATE DATABASE `mybatis_plus` ; 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;
添加数据 1 2 3 4 5 6 7 INSERT INTO USER ( id, NAME, age, email )VALUES ( 1 , 'Jone' , 18 , 'test1@baomidou.com' ), ( 2 , 'Jack' , 20 , 'test2@baomidou.com' ), ( 3 , 'Tom' , 28 , 'test3@baomidou.com' ), ( 4 , 'Sandy' , 21 , 'test4@baomidou.com' ), ( 5 , 'Billie' , 24 , 'test5@baomidou.com' );
新建一个工程
引入依赖 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <dependency > <groupId > org.projectlombok</groupId > <artifactId > lombok</artifactId > </dependency > <dependency > <groupId > com.baomidou</groupId > <artifactId > mybatis-plus-boot-starter</artifactId > <version > 3.5.1</version > </dependency > <dependency > <groupId > mysql</groupId > <artifactId > mysql-connector-java</artifactId > <scope > runtime</scope > </dependency >
安装lombok插件
第一个查询案例 首先新建一个实体类 pojo.User
1 2 3 4 5 6 7 @Data public class User { Integer id; String name; Integer age; String email; }
然后添加对应的mapper映射文件 mapper.UserMapper
1 2 3 4 5 6 7 8 public interface UserMapper extends BaseMapper <User > {}
在启动类上添加 @MapperScan 自动扫描 mapper 接口
1 2 3 4 5 6 7 8 9 @SpringBootApplication @MapperScan("com.szx.mybatisplusproduct.mapper") public class MybatisPlusProductApplication { public static void main (String[] args) throws UnknownHostException { SpringApplication.run(MybatisPlusProductApplication.class, args); } }
添加测试文件
1 2 3 4 5 6 7 8 9 10 11 @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
配置文件中添加如下配置即可
1 2 3 mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
添加完成后会在控制台输入查询的sql语句信息
BaseMapper BaseMapper提供了很多基础方法,我们可以使用提供的方法来实现一个表基本的增删改查
新增方法 1 2 3 4 5 6 7 8 9 10 11 @Test public void testInsert () { User user = new User(); user.setName("张三" ); user.setAge(18 ); user.setEmail("zhangsan@123.com" ); int insert = userMapper.insert(user); System.out.println(user.getId()); }
查看插入表中的结果
删除方法 根据id删除 1 2 3 4 5 6 7 @Test public void testDeleteById () { int resDelCount = userMapper.deleteById(1 ); System.out.println("resDelCount = " + resDelCount); }
根据条件删除 1 2 3 4 5 6 7 8 9 10 @Test public void testDeleteByMap () { HashMap<String, Object> map = new HashMap<>(); map.put("name" ,"张三" ); map.put("age" ,18 ); int i = userMapper.deleteByMap(map); System.out.println("i = " + i); }
根据id集合批量删除数据 1 2 3 4 5 6 7 @Test public void testBatchDelete () { List<Integer> ids = Arrays.asList(2 , 3 ); int i = userMapper.deleteBatchIds(ids); System.out.println("i = " + i); }
修改方法 1 2 3 4 5 6 7 8 9 @Test public void testUpdate () { User user = new User(); user.setId(2 ); user.setName("张三" ); int i = userMapper.updateById(user); System.out.println("i = " + i); }
查询方法 根据id查询单条数据 1 2 3 User user = userMapper.selectById(2 ); System.out.println("user = " + user);
根据id批量查询多条数据 1 2 3 4 List<Integer> ids = Arrays.asList(2 , 3 , 4 ); List<User> users = userMapper.selectBatchIds(ids); System.out.println("users = " + users);
根据map查询 1 2 3 4 5 6 HashMap<String, Object> searchMap = new HashMap<>(); searchMap.put("name" ,"Jack" ); searchMap.put("age" ,20 ); List<User> users1 = userMapper.selectByMap(searchMap); System.out.println("users1 = " + users1);
根据条件查询 这里要传入一个条件构造器,没有时可以传入一个null
1 2 3 List<User> userList = userMapper.selectList(null ); userList.forEach(System.out::println);
查询单个值 这里也是传入条件构造器,没有则传入null
例如:查询数据的总数
1 2 3 Long total = userMapper.selectCount(null ); System.out.println("total = " + total);
自定义SQL方法 首先添加自定义的mapper映射文件,在MyBatisPlus中已经帮我们定义了mapper映射文件的位置,就是在配置文件目录中添加 mapper
文件夹,然后自动会读取以任何目录下,任何名字的 xml 结尾的映射文件
所以首先在 mapper 目录中新建一个映射文件,映射文件的名字和接口名字一样
然后在接口中添加自定义查询方法
1 2 3 4 5 @Repository public interface UserMapper extends BaseMapper <User > { Map<String,Object> mySelectMapById (Integer id) ; }
添加映射sql
1 2 3 4 5 6 7 8 9 10 11 <?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" > <select id ="mySelectMapById" resultType ="map" > select * from user where id = #{id} </select > </mapper >
测试自定义的方法
1 2 3 4 5 @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
1 2 3 4 5 6 7 8 public interface UserService extends IService <User > { }
然后添加实现类,继承 ServiceImpl 类
1 2 3 4 5 6 7 8 9 @Service public class UserServiceImpl extends ServiceImpl <UserMapper , User > implements UserService {}
查询数据总条数 1 2 3 4 5 6 7 8 9 10 11 12 @Autowired UserServiceImpl userService; @Test public void testGetCount () { long count = userService.count(); System.out.println("count = " + count); }
批量插入 由于sql语句有长度限制,海量的数据插入无法放在单条的sql中实现,所以在Service接口中实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @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); }
批量删除 1 2 3 4 5 6 7 8 9 10 11 @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
然后执行查询方法
1 2 3 4 5 6 7 8 9 10 11 12 @Autowired UserServiceImpl userService; @Test public void testGetCount () { long count = userService.count(); System.out.println("count = " + count); }
会出现如下错误
这时候我们可以在实体类上用 @TableName
来指明要操作的表名
1 2 3 4 5 6 7 8 @TableName("t_user") @Data public class User { Integer id; String name; Integer age; String email; }
此时再来查询就可以正常查询了
全局配置表名前缀 1 2 3 4 5 6 7 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
1 2 3 4 5 6 7 8 @TableName("t_user") @Data public class User { Integer t_id; String name; Integer age; String email; }
此时来执行一个新增操作
1 2 3 4 5 6 7 8 9 @Test public void testSave () { User user = new User(); user.setName("Jack" ); user.setAge(18 ); user.setEmail("jack@123.com" ); boolean save = userService.save(user); System.out.println("save = " + save); }
出现如下错误
这时我们使用 @TableId
来指定一个属性为主键
1 2 3 4 5 6 7 8 9 10 @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")
来指定主键的名称
1 2 3 4 5 6 7 8 9 @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自增,
测试使用数据库的主键递增策略
1 2 3 4 5 6 7 8 9 @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,那么这种情况下新建会不会有问题呢?
现在吧数据库表中的列名改一下
实体类还是这样
1 2 3 4 5 6 7 8 9 @TableName("t_user") @Data public class User { @TableId(value = "t_id",type = IdType.AUTO) Integer id; String name; Integer age; String email; }
运行新增方法,出现如下错误,找不到 name 列
这时,可以用 @TableField
注解来声明这个属性对应哪一个列
1 2 3 4 5 6 7 8 9 10 @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
注解
1 2 3 4 5 6 7 8 9 10 11 12 13 @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; }
然后执行删除操作
1 2 3 4 5 @Test public void testRemoveById () { boolean b = userService.removeById(8 ); System.out.println("b = " + b); }
执行结果
通过执行结果可以看到,实际运行的SQL时修改操作,此时来看表中id为8的数据是否还在
通过结果可以看出id等于8的数据并没有被删除,只是吧状态改成了1
然后再来一下查询方法,是否会把已删除的数据查到
1 2 3 4 5 @Test public void testGetUserList () { List<User> userList = userService.list(); userList.forEach(System.out::println); }
运行结果
会自动添加一个 is_deleted = 0 的查询条件
条件构造器和常用接口 wapper介绍
QueryWrapper 组装查询条件 例如:
查询姓名包含 a,并且年龄在 20 到 30 岁之间,并且邮箱不为空的数据
1 2 3 4 5 6 7 8 9 10 @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语句
1 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升序排序
1 2 3 4 5 6 7 8 @Test public void test2 () { QueryWrapper<User> qw = new QueryWrapper<>(); qw.orderByDesc("age" ).orderByAsc("t_id" ); List<User> userList = userService.list(qw); userList.forEach(System.out::println); }
查询结果中的SQL语句
1 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
返回结果
组装删除条件 删除邮箱为空的数据
1 2 3 4 5 6 7 @Test public void test3 () { QueryWrapper<User> qw = new QueryWrapper<>(); qw.isNull("email" ); boolean remove = userService.remove(qw); System.out.println("remove = " + remove); }
查询结果中的SQL语句
1 UPDATE t_user SET is_deleted= 1 WHERE is_deleted= 0 AND (email IS NULL )
返回结果
条件的优先级 将年龄大于20并且姓名中包含有a的或者邮箱为空的数据修改
1 2 3 4 5 6 7 8 9 10 11 12 13 @Test public void test4 () { 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
1 UPDATE t_user SET age= ? WHERE is_deleted= 0 AND (age > ? AND user_name LIKE ? OR email IS NULL )
使用 lambda 表达式
1 2 3 4 5 6 7 8 9 10 @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 表达式内的逻辑优先运算
1 UPDATE t_user SET age= ? WHERE is_deleted= 0 AND (user_name LIKE ? AND (age > ? OR email IS NULL ))
组装select子句 比如我只查询 user_name 和 age
1 2 3 4 5 6 7 8 @Test public void test6 () { QueryWrapper<User> qw = new QueryWrapper<>(); qw.select("user_name" ,"age" ); Map<String, Object> map = userService.getMap(qw); System.out.println("map = " + map); }
SQL语句
1 SELECT user_name,age FROM t_user WHERE is_deleted= 0
实现子查询 查询年龄小于等于20的数据
1 2 3 4 5 6 7 8 @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
1 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 1 2 3 4 5 6 7 8 9 10 11 @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" ,"zhangsan@123.com" ); boolean update = userService.update(null , up); System.out.println("update = " + update); }
生成的SQL
1 UPDATE t_user SET user_name= ?,email= ? WHERE is_deleted= 0 AND (user_name LIKE ? AND (age > ? OR email IS NULL ))
运行结果
模拟开发场景中的条件查询 思路一:复杂版 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @Test public void test2 () { String user_name = "" ; Integer ageStart = 15 ; Integer ageEnd = 30 ; QueryWrapper<User> qw = new QueryWrapper<>(); if (StringUtils.isNotBlank(user_name)){ qw.like("user_name" ,user_name); } if (ageStart != null ){ qw.ge("age" ,ageStart); } if (ageEnd != null ){ qw.le("age" ,ageEnd); } List<User> list = userService.list(qw); list.forEach(System.out::println); }
生成的sql语句
1 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参数的重载方法构建查询条件,简化代码的编写
1 2 3 4 5 6 7 8 9 10 11 12 13 @Test public void test3 () { 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 表达式来表示操作的列名,避免写时字符串导致出错
1 2 3 4 5 6 7 8 9 10 11 12 @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
1 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 1 2 3 4 5 6 7 8 9 10 11 @Test public void test () { 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
1 UPDATE t_user SET age= ?,user_name= ? WHERE is_deleted= 0 AND (user_name LIKE ?)
插件 分页插件 mybatis-plus 自带分页插件,通过简单的配置即可
首先新建配置类
1 2 3 4 5 6 7 8 9 @Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor () { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }
测试分页加条件查询
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 @Test public void test () { Page<User> userPage = new Page<>(1 , 5 ); LambdaQueryWrapper<User> qw = new LambdaQueryWrapper<>(); qw.like(User::getName,"a" ); Page<User> page = userService.page(userPage,qw); 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
1 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 接口中新增接口
1 2 3 4 5 @Repository public interface UserMapper extends BaseMapper <User > { Page<User> selectMyPageVo (Page<User> page,Integer age) ; }
在 UserMapper.xml 中添加接口映射
1 2 3 4 5 6 <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 >
测试
1 2 3 4 5 6 7 8 9 10 @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元。
模拟上面的冲突 首先新建一个商品表并插入一条数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 SET NAMES utf8mb4;SET FOREIGN_KEY_CHECKS = 0 ;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 ; INSERT INTO `t_product` VALUES (1 , '外星人笔记本' , 100 , 0 );SET FOREIGN_KEY_CHECKS = 1 ;
新建对应实体类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package com.szx.mybatisplusproduct.pojo;import com.baomidou.mybatisplus.annotation.IdType;import com.baomidou.mybatisplus.annotation.TableId;import lombok.Data;@Data public class Product { @TableId(type = IdType.AUTO) Integer id; String name; Integer price; Integer version; }
添加 Mapper
1 2 public interface ProductMapper extends BaseMapper <Product > {}
模拟上面的场景
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 26 @SpringBootTest public class ProductTest { @Autowired ProductMapper productMapper; @Test public void test () { Product p1 = productMapper.selectById(1 ); Product p2 = productMapper.selectById(1 ); p1.setPrice(p1.getPrice() + 50 ); productMapper.updateById(p1); p2.setPrice(p2.getPrice() - 30 ); productMapper.updateById(p2); Product p3 = productMapper.selectById(1 ); System.out.println(p3); } }
最后获取的结果商品价格变成了 70 元
乐观锁的实现流程 数据库中添加version字段
取出记录时,获取当前version
1 SELECT id,`name`,price,`version` FROM product WHERE id= 1
更新时,version + 1,如果where语句中的version版本不对,则更新失败
1 UPDATE product SET price= price+ 50 , `version`= `version` + 1 WHERE id= 1 AND `version`= 1
MybatisPlus实现乐观锁 修改实体类,在版本号属性上添加 @Version
1 2 3 4 5 6 7 8 9 10 @Data public class Product { @TableId(type = IdType.AUTO) Integer id; String name; Integer price; @Version Integer version; }
在配置类中添加乐观锁插件
1 2 3 4 5 6 7 8 9 10 11 12 @Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor () { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor()); return interceptor; } }
再次测试价格修改流程
首先小李查询价格
1 SELECT id,name,price,version FROM t_product WHERE id= ?
查到价格为100
然后小王查询价格
1 SELECT id,name,price,version FROM t_product WHERE id= ?
查到价格也是100
然后小李修改价格
1 UPDATE t_product SET name= ?, price= ?, version= ? WHERE id= ? AND version= ?
并成功修改,同时版本号从1改成2
然后小王再去改版本号是1的价格
1 UPDATE t_product SET name= ?, price= ?, version= ? WHERE id= ? AND version= ?
由于上面小李已经把版本号改成2了,这时候小王再去修改版本号是1的数据,自然没有修改成功
最后老板再去查看价格
1 SELECT id,name,price,version FROM t_product WHERE id= ?
价格为150
优化价格修改流程 添加一个修改失败再次请求的机制
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 26 27 28 29 30 31 32 @SpringBootTest public class ProductTest { @Autowired ProductMapper productMapper; @Test public void test () { Product p1 = productMapper.selectById(1 ); Product p2 = productMapper.selectById(1 ); p1.setPrice(p1.getPrice() + 50 ); productMapper.updateById(p1); 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属性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @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; }
添加枚举类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Getter public enum SexEnum { MALE(1 ,"男" ), FEMALE(2 ,"女" ); @EnumValue Integer sex; String sexName; SexEnum(Integer sex,String sexName){ this .sex = sex; this .sexName = sexName; } }
配置扫描通用枚举
1 2 3 4 5 6 7 8 9 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
测试修改方法
1 2 3 4 5 6 7 8 9 10 11 12 @Autowired UserServiceImpl userService; @Test public void test () { 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
1 UPDATE t_user SET sex= ? WHERE is_deleted= 0 AND (user_name LIKE ?)
运行结果
多数据源 适用于多个表的情况。我们创建两个库,分别为:mybatis_plus(以前的库不动)与mybatis_plus_1(新建),将mybatis_plus库的product表移动到mybatis_plus_1库,这样每个库一张表,通过一个测试用例分别获取用户数据与商品数据,如果获取到说明多库模拟成功
首先新建一个数据
引入依赖
1 2 3 4 5 <dependency > <groupId > com.baomidou</groupId > <artifactId > dynamic-datasource-spring-boot-starter</artifactId > <version > 3.5.0</version > </dependency >
重新配置数据源,要把原来的配置注释掉
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 server: port: 8899 spring: application: name: MyBatisPlus学习项目 datasource: dynamic: primary: master 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
1 2 3 4 5 @DS("master") @Service public class UserServiceImpl extends ServiceImpl <UserMapper , User > implements UserService {}
创建 ProductService
1 2 3 4 5 @DS("slave_1") @Service public class ProductServiceImp extends ServiceImpl <ProductMapper , Product > implements ProductService { }
测试查询
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @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); } }
返回的结果
可以看出两个数据库中的数据都被成功的查询到
代码生成器 首先安装插件
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 >
然后直接运行下面的代码即可自动根据表来生成代码
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 26 27 28 29 30 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" ) .fileOverride() .outputDir("D://0000学习文件//MyBatisPlus" ); }) .packageConfig(builder -> { builder.parent("com.baomidou.mybatisplus.samples.generator" ) .moduleName("system" ) .pathInfo(Collections.singletonMap(OutputFile.mapperXml, "D://0000学习文件//MyBatisPlus" )); }) .strategyConfig(builder -> { builder.addInclude("t_user" ) .addTablePrefix("t_" , "c_" ); }) .templateEngine(new FreemarkerTemplateEngine()) .execute(); } }
运行结果
MyBatisX插件 MyBatis-Plus为我们提供了强大的mapper和service模板,能够大大的提高开发效率但是在真正开发过程中,MyBatis-Plus并不能为我们解决所有问题,例如一些复杂的SQL,多表联查,我们就需要自己去编写代码和SQL语句,我们该如何快速的解决这个问题呢,这个时候可以使用MyBatisX插件MyBatisX一款基于 IDEA 的快速开发插件,为效率而生。
官方用法:https://baomidou.com/pages/ba5b24/
插件安装 搜索 mybatisx 并安装
配置数据源
快速生成实体类,Mapper
自动生成的文件
快速生成查询