mybatis-plus John Doe 2023-08-11 2025-07-26 MyBatisPlus 1. MyBatis-Plus介绍 1. 简介 MyBatis-Plus (简称 MP)是一个 MyBatis的增强工具,在 MyBatis 的基础上只做增强不做改变,为 简化开发、提高效率而生。
我们的愿景是成为 MyBatis 最好的搭档,就像魂斗罗中的 1P、2P,基友搭配,效率翻倍。
2. 特性
无侵入 :只做增强不做改变,引入它不会对现有工程产生影响,如丝般顺滑
损耗小 :启动即会自动注入基本 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 操作智能分析阻断,也可自定义拦截规则,预防 误操作
3. 支持数据库
任何能使用MyBatis进行 CRUD, 并且支持标准 SQL 的数据库,具体支持情况如下
MySQL,Oracle,DB2,H2,HSQL,SQLite,PostgreSQL,SQLServer,Phoenix,Gauss , ClickHouse,Sybase,OceanBase,Firebird,Cubrid,Goldilocks,csiidb
达梦数据库,虚谷数据库,人大金仓数据库,南大通用(华库)数据库,南大通用数据库,神通数据 库,瀚高数据库
4. 框架结构
5. 文档 官方地址: http://mp.baomidou.com
Github: https://github.com/baomidou/mybatis-plus
Gitee: https://gitee.com/baomidou/mybatis-plus
文档发布地址: https://baomidou.com/pages/24112f
2. 入门案例 1. 开发环境 IDE:idea-2021.3
JDK:JDK11
构建工具:maven 3.8.4
MySQL版本:MySQL 8.0.29
Spring Boot:2.7.0
MyBatis-Plus:3.5.1
2. 创建数据库及表 1 2 3 4 5 CREATE DATABASE `mybatis_plus` ;use `mybatis_plus`; CREATE TABLE `user ` ( `id` bigint (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 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' );
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 26 27 28 29 30 31 32 <dependencies > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter</artifactId > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-test</artifactId > <scope > test</scope > </dependency > <dependency > <groupId > com.baomidou</groupId > <artifactId > mybatis-plus-boot-starter</artifactId > <version > 3.5.1</version > </dependency > <dependency > <groupId > org.projectlombok</groupId > <artifactId > lombok</artifactId > <optional > true</optional > </dependency > <dependency > <groupId > mysql</groupId > <artifactId > mysql-connector-java</artifactId > </dependency > </dependencies >
4. 编写代码 1. 配置application.yml 1 2 3 4 5 6 7 8 9 10 spring: datasource: type: com.zaxxer.hikari.HikariDataSource driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/mybatis_plus username: root password: '123456'
注意:
驱动类driver-class-name
spring boot 2.0(内置jdbc5驱动),驱动类使用:
driver-class-name: com.mysql.jdbc.Driver
spring boot 2.1及以上(内置jdbc8驱动),驱动类使用:
driver-class-name: com.mysql.cj.jdbc.Driver
连接地址url
否则运行测试用例报告如下错误:
java.sql.SQLException: The server time zone value ‘ÖÐ1ú±ê×1⁄4ʱ1⁄4ä’ is unrecognized or represents more
2. 启动类 1 2 3 4 5 6 7 8 9 10 11 @SpringBootApplication @MapperScan("com.atguigu.mybatisplus.mapper") public class MybatisplusApplication { public static void main (String[] args) { SpringApplication.run(MybatisplusApplication.class, args); } }
3. 添加实体 1 2 3 4 5 6 7 8 9 10 @Data @AllArgsConstructor @NoArgsConstructor public class User { private Long id; private String name; private Integer age; private String email; }
4. 添加mapper
BaseMapper 是MyBatis-Plus提供的模板mapper,其中包含了基本的CRUD方法,泛型为操作的实体类型
1 2 public interface UserMapper extends BaseMapper <User> {}
5. 测试 1 2 3 4 5 6 7 8 9 10 11 12 13 @SpringBootTest class MybatisplusApplicationTests { @Autowired private UserMapper userMapper; @Test public void testSelectList () { List<User> list = userMapper.selectList(null );` list.forEach(System.out::println); } }
注意:
IDEA在 userMapper 处报错,因为找不到注入的对象,因为类是动态创建的,但是程序可以正确的执行。
为了避免报错,可以在mapper接口上添加 @Repository 注解
6. 添加日志 在application.yml中配置日志输出
1 2 3 4 mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
3. 基本操作 1. BaseMapper MyBatis-Plus中的基本CRUD在内置的BaseMappe r中已得到了实现,我们可以直接使用,接口如下:
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 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 public interface BaseMapper <T> extends Mapper <T> { int insert (T entity) ; int deleteById (Serializable id) ; int deleteById (T entity) ; int deleteByMap (@Param(Constants.COLUMN_MAP) Map<String, Object> columnMap) ; int delete (@Param(Constants.WRAPPER) Wrapper<T> queryWrapper) ; int deleteBatchIds (@Param(Constants.COLLECTION) Collection<?> idList) ; int updateById (@Param(Constants.ENTITY) T entity) ; int update (@Param(Constants.ENTITY) T entity, @Param(Constants.WRAPPER) Wrapper<T> updateWrapper) ; T selectById (Serializable id) ; List<T> selectBatchIds (@Param(Constants.COLLECTION) Collection<? extends Serializable> idList) ; List<T> selectByMap (@Param(Constants.COLUMN_MAP) Map<String, Object> columnMap) ; default T selectOne (@Param(Constants.WRAPPER) Wrapper<T> queryWrapper) { List<T> ts = this .selectList(queryWrapper); if (CollectionUtils.isNotEmpty(ts)) { if (ts.size() != 1 ) { throw ExceptionUtils.mpe("One record is expected, but the query result is multiple records" ); } return ts.get(0 ); } return null ; } default boolean exists (Wrapper<T> queryWrapper) { Long count = this .selectCount(queryWrapper); return null != count && count > 0 ; } Long selectCount (@Param(Constants.WRAPPER) Wrapper<T> queryWrapper) ; List<T> selectList (@Param(Constants.WRAPPER) Wrapper<T> queryWrapper) ; List<Map<String, Object>> selectMaps (@Param(Constants.WRAPPER) Wrapper<T> queryWrapper) ; List<Object> selectObjs (@Param(Constants.WRAPPER) Wrapper<T> queryWrapper) ; <P extends IPage <T>> P selectPage (P page, @Param(Constants.WRAPPER) Wrapper<T> queryWrapper) ; <P extends IPage <Map<String, Object>>> P selectMapsPage (P page, @Param(Constants.WRAPPER) Wrapper<T> queryWrapper) ; }
2. CRUD 1. 插入 1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Test public void testInsert () { User user = new User (); user.setName("张三" ;); user.setAge("12" ); user.setEmail("zhangsan@atguigu.com" ); int result = userMapper.insert(user); System.out.println("result = " + result); System.out.println("id = " + user.getId()); }
最终执行的结果,所获取的id为1475754982694199298
这是因为MyBatis-Plus在实现插入数据时,会默认基于雪花算法的策略生成id
2. 删除 1 2 3 4 5 6 7 8 @Test public void testDeleteById () { int result = userMapper.deleteById(1475754982694199298L ); System.out.println("受影响行数:" +result); }
1 2 3 4 5 6 7 8 9 @Test public void testDeleteBatchIds () { List<Long> idList = Arrays.asList(1L , 2L , 3L ); int result = userMapper.deleteBatchIds(idList); System.out.println("受影响行数:" +result); }
1 2 3 4 5 6 7 8 9 10 @Test public void testDeleteByMap () { Map<String, Object> map = new HashMap <>(); map.put("age" , 23 ); map.put("name" , "张三" ); int result = userMapper.deleteByMap(map); System.out.println("受影响行数:" +result); }
3. 修改 1 2 3 4 5 6 7 8 9 10 11 12 @Test public void testUpdate () { User user = new User (); user.setIdƔL); user.setName("李四" ); user.setEmail("lisi@atguigu.com" ); userMapper.updateById(user); System.out.println("user = " + user); }
4. 查询 1 2 3 4 5 6 7 8 9 @Test public void testSelectById () { User user = userMapper.selectByIdƑL); System.out.println("user = " + user); }
1 2 3 4 5 6 7 8 9 @Test public void testSelectBatchIds () { List<Long> list = Arrays.asListƑL, 2L , 3L ); List<User> users = userMapper.selectBatchIds(list); users.forEach(System.out::println); }
1 2 3 4 5 6 7 8 9 10 11 @Test public void testSelectByMap () { Map<String, Object> map = new HashMap <>(); map.put("name" , "Jack" ); map.put("age" , 18 ); List<User> users = userMapper.selectByMap(map); users.forEach(System.out::println); }
1 2 3 4 5 6 7 8 @Test public void testSelectList () { List<User> users = userMapper.selectList(null ); users.forEach(System.out::println); }
通过观察BaseMapper中的方法,大多方法中都有Wrapper类型的形参,此为条件构造器,可针对于SQL语句设置不同的条件,若没有条件,则可以为该形参赋值null,即查询(删除/修改)所有数据
自定义功能
1 2 3 Map<String,Object> selectMapById (Long id) ;
1 2 3 4 5 <select id ="selectMapById" resultType ="map" > select id,name,age,email from t_user where id = #{id} </select >
1 2 3 4 5 6 @Test public void testConsumer () { Map<String, Object> map = userMapper.selectMapById(1L ); System.out.println("map = " + map); }
3. IService MyBatis-Plus中有一个接口 IService和其实现类 ServiceImpl,封装了常见的业务层逻辑。
通用 Service CRUD 封装IService接口,进一步封装 CRUD 采用 get 查询单行 remove 删
除 list 查询集合 page 分页 前缀命名方式区分 Mapper 层避免混淆,
泛型 T 为任意实体对象
建议如果存在自定义通用 Service 方法的可能,请创建自己的 IBaseService 继承
Mybatis-Plus 提供的基类
官网地址
1. IService实现类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 public interface UserService extends IService <User> {} @Service public class UserServiceImpl extends ServiceImpl <UserMapper, User> implements UserService {}
2. 查询记录数 1 2 3 4 5 6 7 8 9 10 11 12 @Autowired private UserService userService;@Test public void testGetCount () { long count = userService.count(); System.out.println("总记录数 = " + count); }
3. 批量插入 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Test public void testInsertMore () { List<User> users = new ArrayList <>(); for (int i = 1 ; i <= 10 ; i++) { User user = new User (); user.setName("lxg" + i); user.setAge(20 + i); users.add(user); } boolean b = userService.saveBatch(users); System.out.println(b); }
4. 基本注解 1. @TableName 经过以上的测试,在使用MyBatis-Plus实现基本的CRUD时,我们并没有指定要操作的表,只是在 Mapper接口继承BaseMapper时,设置了泛型User,而操作的表为user表
由此得出结论,MyBatis-Plus在确定操作的表时,由BaseMapper的泛型决定,即实体类型决定,且默认操作的表名和实体类型的类名一致
1. 问题 若实体类类型的类名和要操作的表的表名不一致,会出现什么问题?
我们将表user更名为t_user,测试查询功能
程序抛出异常,Table 'mybatis_plus.user' doesn't exist,因为现在的表名为t_user,而默认操作 的表名和实体类型的类名一致,即user表
2. 使用@TableName 解决 在实体类类型上添加@TableName("t_user"),标识实体类对应的表,即可成功执行SQL语句
3. 通过全局配置解决 在开发的过程中,我们经常遇到以上的问题,即实体类所对应的表都有固定的前缀,例如t_或tbl_
此时,可以使用MyBatis-Plus提供的全局配置,为实体类所对应的表名设置默认的前缀,那么就
不需要在每个实体类上通过@TableName标识实体类对应的表
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_
2. @Tableld MyBatis-Plus在实现CRUD时,会默认将id作为主键列,并在插入数据时,默认 基于雪花算法的策略生成id
1. 问题 若实体类和表中表示主键的不是id,而是其他字段,例如uid,MyBatis-Plus会自动识别uid为主键列吗?
我们实体类中的属性id改为uid,将表中的字段id也改为uid,测试添加功能
程序抛出异常,Field ‘uid’ doesn’t have a default value,说明MyBatis-Plus没有将uid作为主键 赋值
2. 使用@TableId解决 在实体类中uid属性上通过@TableId将其标识为主键,即可成功执行SQL语句
3. @TableId的value属性 若实体类中主键对应的属性为id,而表中表示主键的字段为uid,此时若只在属性id上添加注解 @TableId,则抛出异常Unknown column 'id' in 'field list',即MyBatis-Plus仍然会将id作为表的
主键操作,而表中表示主键的是字段uid
此时需要通过@TableId注解的value属性,指定表中的主键字段,@TableId("uid") 或 @TableId(value="uid")
4. @Tableld的type属性
type属性用来定义主键策略
值
描述
AUTO
数据库ID自增
NONE
该类型为未设置主键类型(注解里等于跟随全局,全局里约等于 INPUT)
INPUT
用户输入ID
ASSIGN_ID
基于雪花算法的策略生成数据id,与数据库id是否设置自增无关
ASSIGN_UUID
分配UUID (主键类型为 string) 默认实现类
配置全局主键策略
1 2 3 4 5 6 7 8 9 10 11 mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: table-prefix: t_ id-type: auto
5. 雪花算法
背景
需要选择合适的方案去应对数据规模的增长,以应对逐渐增长的访问压力和数据量。
数据库的扩展方式主要包括:业务分库、主从复制,数据库分表。
数据库分表
将不同业务数据分散存储到不同的数据库服务器,能够支撑百万甚至千万用户规模的业务,但如果业务
继续发展,同一业务的单表数据也会达到单台数据库服务器的处理瓶颈。例如,淘宝的几亿用户数据,
如果全部存放在一台数据库服务器的一张表中,肯定是无法满足性能要求的,此时就需要对单表数据进
行拆分。
单表数据拆分有两种方式:垂直分表和水平分表 。示意图如下:
垂直分表
垂直分表适合将表中某些不常用且占了大量空间的列拆分出去。
例如,前面示意图中的 nickname 和 description 字段,假设我们是一个婚恋网站,用户在筛选其他用户的时候,主要是用 age 和 sex 两个字段进行查询,而 nickname 和 description 两个字段主要用于展示,一般不会在业务查询中用到。description 本身又比较长,因此我们可以将这两个字段独立到另外一张表中,这样在查询 age 和 sex 时,就能带来一定的性能提升。
水平分表
水平分表适合表行数特别大的表,有的公司要求单表行数超过 5000 万就必须进行分表,这个数字可以作为参考,但并不是绝对标准,关键还是要看表的访问性能。对于一些比较复杂的表,可能超过 1000 万就要分表了;而对于一些简单的表,即使存储数据超过 1 亿行,也可以不分表。
但不管怎样,当看到表的数据量达到千万级别时,作为架构师就要警觉起来,因为这很可能是架构的性能瓶颈或者隐患。
水平分表相比垂直分表,会引入更多的复杂性,例如要求全局唯一的数据id该如何处
1. 主键自增
以最常见的用户 ID 为例,可以按照 1000000 的范围大小进行分段,1 ~ 999999 放到表 1中,1000000 ~ 1999999 放到表2中,以此类推。
复杂点:分段大小的选取。分段太小会导致切分后子表数量过多,增加维护复杂度;分段太大可能会导致单表依然存在性能问题,一般建议分段大小在 100 万至 2000 万之间,具体需要根据业务选取合适的分段大小。
优点:可以随着数据的增加平滑地扩充新的表。例如,现在的用户是 100 万,如果增加到 1000 万,只需要增加新的表就可以了,原有的数据不需要动。
缺点:分布不均匀。假如按照 1000 万来进行分表,有可能某个分段实际存储的数据量只有 1 条,而另外一个分段实际存储的数据量有 1000 万条。
2. 取模
同样以用户 ID 为例,假如我们一开始就规划了 10 个数据库表,可以简单地用 user_id % 10 的值来表示数据所属的数据库表编号,ID 为 985 的用户放到编号为 5 的子表中,ID 为 10086 的用户放到编号 为 6 的子表中。
复杂点:初始表数量的确定。表数量太多维护比较麻烦,表数量太少又可能导致单表性能存在问题。
优点:表分布比较均匀。
缺点:扩充新的表很麻烦,所有数据都要重分布。
3. 雪花算法
雪花算法是由Twitter公布的分布式主键生成算法,它能够保证不同表的主键的不重复性,以及相同表的主键的有序性。
核心思想:
长度共64bit(一个long型)。
首先是一个符号位,1bit标识,由于long基本类型在Java中是带符号的,最高位是符号位,正数是0,负数是1,所以id一般是正数,最高位是0。
41bit时间截(毫秒级),存储的是时间截的差值(当前时间截 - 开始时间截),结果约等于69.73年。
10bit作为机器的ID(5个bit是数据中心,5个bit的机器ID,可以部署在1024个节点)。
12bit作为毫秒内的流水号(意味着每个节点在每毫秒可以产生 4096 个 ID)。
优点:整体上按照时间自增排序,并且整个分布式系统内不会产生ID碰撞,并且效率较高。
3. @TableField MyBatis-Plus在执行SQL语句时,要保证实体类中的属性名和表中的字段名一致。
如果实体类中的属性名和字段名不一致的情况,会出现什么问题呢?
1. 情况1 若实体类中的属性使用的是驼峰命名风格,而表中的字段使用的是下划线命名风格。
例如实体类属性userName,表中字段user_name。
此时MyBatis-Plus会自动将下划线命名风格转化为驼峰命名风格。
相当于在MyBatis中配置。
2. 情况2 若实体类中的属性和表中的字段不满足情况1。
例如实体类属性name,表中字段username。
此时需要在实体类属性上使用@TableField("username")设置属性所对应的字段名
4. @TableLogic 1. 逻辑删除
2. 实现 step1:数据库中创建逻辑删除状态列,设置默认值为0
step2:实体类中添加@TableLogic逻辑删除属性
step3:测试
测试删除功能,真正执行的是修改
UPDATE t_user SET is_deleted=1 WHERE id=? AND is_deleted=0
测试查询功能,被逻辑删除的数据默认不会被查询
SELECT id,username AS name,age,email,is_deleted FROM t_user WHERE is_deleted=0
5. 条件构造接口 1. wrapper
Wrapper : 条件构造抽象类 ,最顶端父类
2. QueryWrapper 1. 组装查询条件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Test public void test01 () { QueryWrapper<User> queryWrapper = new QueryWrapper <>(); queryWrapper.like("user_name" , "a" ) .between("age" , 20 , 30 ) .isNotNull("email" ); List<User> users = userMapper.selectList(queryWrapper); users.forEach(System.out::println); }
2. 组装排序条件 1 2 3 4 5 6 7 8 9 10 11 12 13 @Test public void test02 () { QueryWrapper<User> queryWrapper = new QueryWrapper <>(); queryWrapper.orderByDesc("age" ) .orderByAsc("uid" ); List<User> users = userMapper.selectList(queryWrapper); users.forEach(System.out::println); }
3. 组装删除条件 1 2 3 4 5 6 7 8 9 10 11 @Test public void test03 () { QueryWrapper<User> queryWrapper = new QueryWrapper <>(); queryWrapper.isNull("email" ); int result = userMapper.delete(queryWrapper); System.out.println("result = " + result); }
4. 条件的优先级 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 33 34 35 36 @Test public void test04 () { QueryWrapper<User> queryWrapper = new QueryWrapper <>(); queryWrapper.gt("age" , 20 ) .like("user_name" , "a" ) .or() .isNotNull("email" ); User user = new User (); user.setName("小明" ); user.setEmail("test@atguigu.com" ); int result = userMapper.update(user, queryWrapper); System.out.println("result = " + result); } @Test public void test05 () { QueryWrapper<User> queryWrapper = new QueryWrapper <>(); queryWrapper.like("user_name" , "a" ) .and(i -> i.gt("age" , 20 ).or().isNull("email" )); User user = new User (); user.setName("小红" ); user.setEmail("test@atguigu.com" ); int result = userMapper.update(user, queryWrapper); System.out.println("result = " + result); }
5. 组装select子句 1 2 3 4 5 6 7 8 9 10 11 @Test public void test06 () { QueryWrapper<User> queryWrapper = new QueryWrapper <>(); queryWrapper.select("user_name" , "age" , "email" ); List<Map<String, Object>> users = userMapper.selectMaps(queryWrapper); users.forEach(System.out::println); }
6. 实现子查询 1 2 3 4 5 6 7 8 9 10 11 12 @Test public void test07 () { QueryWrapper<User> queryWrapper = new QueryWrapper <>(); queryWrapper.inSql("uid" , "select uid from t_user where uid <= 100" ); List<User> list = userMapper.selectList(queryWrapper); list.forEach(System.out::println); }
3. UpdateWrapper 1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Test public void test08 () { UpdateWrapper<User> updateWrapper = new UpdateWrapper <>(); updateWrapper.like("user_name" , "a" ) .and(i -> i.gt("age" , 20 ).or().isNull("email" )); updateWrapper.set("user_name" , "小黑" ).set("email" , "abc@atguigu.com" ); int result = userMapper.update(null , updateWrapper); System.out.println("result = " + result); }
4. Condition 在真正开发的过程中,组装条件是常见的功能,而这些条件数据来源于用户输入,是可选的,因此我们在组装这些条件时,必须先判断用户是否选择了这些条件,若选择则需要组装该条件,若没有选择则一定不能组装,以免影响SQL执行的结果
1. 思路一
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 @Test public void test09 () { String username = "a" ; Integer ageBegin = null ; Integer ageEnd = 30 ; QueryWrapper<User> queryWrapper = new QueryWrapper <>(); if (StringUtils.isNotBlank(username)) { queryWrapper.like("user_name" , username); } if (ageBegin != null ) { queryWrapper.gt("age" , ageBegin); } if (ageEnd != null ) { queryWrapper.le("age" , ageEnd); } List<User> list = userMapper.selectList(queryWrapper); list.forEach(System.out::println); }
2. 思路二
上面的实现方案没有问题,但是代码比较复杂,我们可以使用带condition参数的重载方法构建查询条件,简化代码的编写
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Test public void test10 () { String username = "a" ; Integer ageBegin = null ; Integer ageEnd = 30 ; QueryWrapper<User> queryWrapper = new QueryWrapper <>(); queryWrapper.like(StringUtils.isNotBlank(username), "user_name" , username) .gt(ageBegin != null , "age" , ageBegin) .le(ageEnd != null , "age" , ageEnd); List<User> list = userMapper.selectList(queryWrapper); list.forEach(System.out::println); }
5. LambdaQueryWrapper 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @Test public void test11 () { String username = "a" ; Integer ageBegin = null ; Integer ageEnd = 30 ; LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper <>(); queryWrapper.like(StringUtils.isNotBlank(username), User::getName, username) .gt(ageBegin != null , User::getAge, ageBegin) .le(ageEnd != null , User::getAge, ageEnd); List<User> list = userMapper.selectList(queryWrapper); list.forEach(System.out::println); }
6. LambdaUpdateWrapper 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Test public void test12 () { LambdaUpdateWrapper<User> updateWrapper = new LambdaUpdateWrapper <>(); updateWrapper.like(User::getName, "a" ) .and(i -> i.gt(User::getAge, 20 ).or().isNull(User::getEmail)); updateWrapper.set(User::getName, "小黑" ).set(User::getEmail, "abc@atguigu.com" ); int result = userMapper.update(null , updateWrapper); System.out.println("result = " + result); }
6. 插件 1. 分页插件 MyBatis Plus自带分页插件,只要简单的配置即可实现分页功能
1. 配置类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Configuration @MapperScan("scan.your.mapper.package") 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 16 17 18 @Autowired private ProductMapper productMapper;@Test public void testPage () { Page<User> page = new Page <>(1 , 3 ); userMapper.selectPage(page, null ); System.out.println( page.getRecords()); System.out.println("总页数:" + page.getPages()); System.out.println("总记录数" + page.getTotal()); System.out.println("是否有上一页" + page.hasNext()); System.out.println("是否有下一页:" + page.hasPrevious()); }
2. xml自定义分页 1. 定义接口方法 1 2 3 4 5 6 7 Page<User> selectPageVo (@Param("page") Page<User> page, @Param("age") Integer age) ;
2. xml中编写SQL 1 2 3 4 5 <select id ="selectPageVo" resultType ="User" > select uid as `id`,user_name as `name`,age,email from t_user where age > #{age} </select >
3. 测试 1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Test public void testSelectPageVo () { Page<User> page = new Page <>(1 , 5 ); userMapper.selectPageVo(page, 20 ); 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()); }
3. 乐观锁 1. 场景 一件商品,成本价是80元,售价是100元。老板先是通知小李,说你去把商品价格增加50元。小李正在玩游戏,耽搁了一个小时。正好一个小时后,老板觉得商品价格增加到150元,价格太高,可能会影响销量。又通知小王,你把商品价格降低30元。
此时,小李和小王同时操作商品后台系统。小李操作的时候,系统先取出商品价格100元;小王也在操作,取出的商品价格也是100元。小李将价格加了50元,并将100+50=150元存入了数据库;小王将商品减了30元,并将100-30=70元存入了数据库。是的,如果没有锁,小李的操作就完全被小王的覆盖了。
现在商品价格是70元,比成本价低10元。几分钟后,这个商品很快出售了1千多件商品,老板亏1 万多。
2. 乐观锁和悲观锁 如果是乐观锁,小王保存价格前,会检查下价格是否被人修改过了。如果被修改过了,则重新取出的被修改后的价格,150元,这样他会将120元存入数据库。
如果是悲观锁,小李取出数据后,小王只能等小李操作完之后,才能对价格进行操作,也会保证最终的价格是120元。
3. 模拟修改冲突 1. 准备库和表
1 2 3 4 5 6 7 CREATE TABLE t_product( id BIGINT (20 ) NOT NULL COMMENT '主键ID' , NAME VARCHAR (30 ) NULL DEFAULT NULL COMMENT '商品名称' , price INT (11 ) DEFAULT 0 COMMENT '价格' , VERSION INT (11 ) DEFAULT 0 COMMENT '乐观锁版本号' , PRIMARY KEY (id) ); INSERT INTO t_product (id, NAME, price) VALUES (1 , '外星人笔记本' , 100 );
2. 实体类
1 2 3 4 5 6 7 @Data public class Product { private Long id; private String name; private Integer price; private Integer version; }
3. mapper
1 2 3 4 @Repository 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 22 @Test public void testProduct01 () { Product productLi = productMapper.selectById(1 ); System.out.println("小李查询的商品价格:" + productLi.getPrice()); Product productWang = productMapper.selectById(1 ); System.out.println("小王查询的商品价格:" + productWang.getPrice()); productLi.setPrice(productLi.getPrice() + 50 ); productMapper.updateById(productLi); productWang.setPrice(productWang.getPrice() - 30 ); int result = productMapper.updateById(productWang); Product productBoss = productMapper.selectById(1 ); System.out.println("老板查询的商品价格:" + productBoss.getPrice()); }
4. 乐观锁实现流程 数据库中添加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
5. MyBatis-Plus实现乐观锁 1. 实体类
1 2 3 4 5 6 7 8 9 10 Data 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 10 11 12 13 14 15 16 @Configuration @MapperScan("com.atguigu.mybatisplus.mapper") public class myBatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor () { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor (); interceptor.addInnerInterceptor(new PaginationInnerInterceptor (DbType.MYSQL)); interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor ()); return interceptor; } }
3. 测试修改冲突
小李查询商品信息:SELECT id,name,price,version FROM t_product WHERE id=?
小王查询商品信息:SELECT id,name,price,version FROM t_product WHERE id=?
小李修改商品价格,自动将version+1
UPDATE t_product SET name=?, price=?, version=? WHERE id=? AND version=? Parameters: 外星人笔记本(String), 150(Integer), 1(Integer), 1(Long), 0(Integer)
小王修改商品价格,此时version已更新,条件不成立,修改失败
UPDATE t_product SET name=?, price=?, version=? WHERE id=? AND version=? Parameters:外星人笔记本(String), 70(Integer), 1(Integer), 1(Long), 0(Integer)
最终,小王修改失败,查询价格:150
SELECT id,name,price,version FROM t_product WHERE id=?
4. 优化流程
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 @Test public void testProduct01 () { Product productLi = productMapper.selectById(1 ); System.out.println("小李查询的商品价格:" + productLi.getPrice()); Product productWang = productMapper.selectById(1 ); System.out.println("小王查询的商品价格:" + productWang.getPrice()); productLi.setPrice(productLi.getPrice() + 50 ); productMapper.updateById(productLi); productWang.setPrice(productWang.getPrice() - 30 ); int result = productMapper.updateById(productWang); if (result == 0 ) { Product productNew = productMapper.selectById(1 ); productNew.setPrice(productNew.getPrice() - 30 ); productMapper.updateById(productNew); } Product productBoss = productMapper.selectById(1 ); System.out.println("老板查询的商品价格:" + productBoss.getPrice()); }
7. 枚举映射 表中的有些字段值是固定的,例如性别(男或女),此时我们可以使用MyBatis-Plus的通用枚举来实现
1. 创建表
1 2 3 4 5 6 7 8 9 create table tb_user( id bigint not null comment '主键ID' , username varchar (30 ) comment "姓名", age int comment '年龄' , email varchar (50 ) comment '邮箱' , sex int comment '性别' , primary key (id) )
2. 枚举类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @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 4 5 6 7 8 9 10 11 12 13 14 15 mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: table-prefix: tb_ id-type: auto type-aliases-package: com.atguigu.mybatisplus.pojo type-enums-package: com.atguigu.mybatisplus.enums
4. 测试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @SpringBootTest public class MyBatisPlusEnumTest { @Autowired private UserMapper userMapper; @Test public void test () { User user = new User (); user.setName("admin" ); user.setAge(33 ); user.setSex(SexEnum.MALE); int result = userMapper.insert(user); System.out.println("result = " + result); } }
8. 代码生成器 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) { String path = "/Users/luxiaogen/Documents/RoadTo2w/Java/尚硅谷/MyBatisPlus-2022/demo" ; FastAutoGenerator.create("jdbc:mysql://localhost:3306/mybatis_plus?serverTimezone=GMT%2B8&characterEncoding=utf-8&useSSL=false" , "root" , "root" ) .globalConfig(builder -> { builder.author("atguigu" ) .fileOverride() .outputDir(path); }) .packageConfig(builder -> { builder.parent("com.atguigu" ) .moduleName("mybatisplus" ) .pathInfo(Collections.singletonMap(OutputFile.mapperXml, path)); }) .strategyConfig(builder -> { builder.addInclude("t_user" ) .addTablePrefix("t_" , "c_" ); }).templateEngine(new FreemarkerTemplateEngine ()) .execute(); } }
9. 多数据源 适用于多种场景:纯粹多库、 读写分离、 一主多从、 混合模式等 目前我们就来模拟一个纯粹多库的一个场景,其他场景类似。
场景说明:我们创建两个库,分别为:mybatis_plus(以前的库不动)与mybatis_plus_1(新建),将 mybatis_plus库的product表移动到mybatis_plus_1库,这样每个库一张表,通过一个测试用例 分别获取用户数据与商品数据,如果获取到说明多库模拟成功。
1. 准备数据表 1 2 3 4 5 6 7 8 9 10 11 CREATE DATABASE `mybatis_plus_1` ;use `mybatis_plus_1`; CREATE TABLE product( id BIGINT (20 ) NOT NULL COMMENT '主键ID' , name VARCHAR (30 ) NULL DEFAULT NULL COMMENT '商品名称' , price INT (11 ) DEFAULT 0 COMMENT '价格' , version INT (11 ) DEFAULT 0 COMMENT '乐观锁版本号' , PRIMARY KEY (id) ); INSERT INTO product (id, NAME, price) VALUES (1 , '外星人笔记本' , 100 );
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 25 26 <dependency > <groupId > com.baomidou</groupId > <artifactId > mybatis-plus-boot-starter</artifactId > <version > 3.5.1</version > </dependency > <dependency > <groupId > org.projectlombok</groupId > <artifactId > lombok</artifactId > <optional > true</optional > </dependency > <dependency > <groupId > mysql</groupId > <artifactId > mysql-connector-java</artifactId > </dependency > <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 spring: datasource: dynamic: primary: master strict: false datasource: master: url: jdbc:mysql://localhost:3306/mybatis_plus?serverTimezone=GMT%2B8&characterEncoding=utf-8&useSSL=false driver-class-name: com.mysql.cj.jdbc.Driver username: root password: 'root' slave_1: url: jdbc:mysql://localhost:3306/mybatis_plus_1?serverTimezone=GMT%2B8&characterEncoding=utf-8&useSSL=false driver-class-name: com.mysql.cj.jdbc.Driver username: root password: 'root'
4. 创建用户service 1 2 public interface UserService extends IService <User> {}
1 2 3 4 5 @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 6 7 8 9 10 11 12 13 14 15 16 17 @SpringBootTest class MybatisPlusDatasourceApplicationTests { @Autowired private UserService userService; @Autowired private ProductService productService; @Test public void test () { System.out.println(userService.getById(1L )); System.out.println(productService.getById(1L )); } }
结果:
1、都能顺利获取对象,则测试成功
2、如果我们实现读写分离,将写操作方法加上主库数据源,读操作方法加上从库数据源,自动切换,是不是就能实现读写分离?