官网:http://mp.baomidou.com 参考教程:http://mp.baomidou.com/guide/
MyBatis-Plus(简称 MP)是一个 MyBatis 的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。
润物无声 只做增强不做改变,引入它不会对现有工程产生影响,如丝般顺滑。 效率至上 只需简单配置,即可快速进行 CRUD 操作,从而节省大量时间。 丰富功能 热加载、代码生成、分页、性能分析等功能一应俱全。mybatis_plus
其对应的数据库 Schema 脚本如下: COMMENT表示注释 BIGINT对应Java代码中的长整型Long
CREATE TABLE user ( id BIGINT(20) NOT NULL COMMENT '主键ID', name VARCHAR(30) NULL DEFAULT NULL COMMENT '姓名', age INT(11) NULL DEFAULT NULL COMMENT '年龄', email VARCHAR(50) NULL DEFAULT NULL COMMENT '邮箱', PRIMARY KEY (id) );其对应的数据库 Data 脚本如下:
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');
使用 Spring Initializr 快速初始化一个 Spring Boot 工程
Group:com.atguigu Artifact:mybatis-plus 版本:2.2.1.RELEASE
注意:引入 MyBatis-Plus 之后请不要再次引入 MyBatis,以避免因版本差异导致的问题。
<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> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependency> <!--mybatis-plus--> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.3.1</version> </dependency> <!--mysql运行时依赖--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <!--lombok用来简化实体类,可以生成get,set等方法--> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> </dependencies>(1)idea2019版本 (2)idea2018版本
在 application.properties 配置文件中添加 MySQL 数据库的相关配置: spring boot 2.1及以上(内置jdbc8驱动)
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver spring.datasource.url=jdbc:mysql://localhost:3306/mybatis_plus?serverTimezone=GMT%2B8 spring.datasource.username=root spring.datasource.password=123456注意:
1、这里的 url 使用了 ?serverTimezone=GMT%2B8 后缀,因为8.0版本的jdbc驱动需要添加这个后缀,否则运行测试用例报告如下错误:
java.sql.SQLException: The server time zone value ‘Öйú±ê׼ʱ¼ä’ is unrecognized or represents more
2、这里的 driver-class-name 使用了 com.mysql.cj.jdbc.Driver ,在 jdbc 8 中 使用这个驱动
spring boot 2.0(内置jdbc5驱动) 注意:driver和url的变化
#mysql数据库连接 spring.datasource.driver-class-name=com.mysql.jdbc.Driver spring.datasource.url=jdbc:mysql://localhost:3306/mybatis_plus?characterEncoding=utf-8&useSSL=false spring.datasource.username=root spring.datasource.password=123456在 Spring Boot 启动类中添加 @MapperScan 注解,扫描 Mapper 文件夹
@SpringBootApplication @MapperScan("com.atguigu.mybatisplus.mapper") public class MybatisPlusApplication { ...... }创建包 entity 编写实体类 User.java(此处使用了 Lombok 简化代码)
@Data public class User { private Long id; private String name; private Integer age; private String email; }查看编译结果
创建包 mapper 编写Mapper 接口: UserMapper.java
public interface UserMapper extends BaseMapper<User> { }添加测试类,进行功能测试:
package com.atguigu.mybatisplus; @SpringBootTest class MybatisPlusApplicationTests { @Autowired private UserMapper userMapper; @Test void testSelectList() { //UserMapper 中的 selectList() 方法的参数为 MP 内置的条件封装器 Wrapper //所以不填写就是无任何条件 List<User> users = userMapper.selectList(null); users.forEach(System.out::println); } }注意:
IDEA在 userMapper 处报错,因为找不到注入的对象,因为类是动态创建的,但是程序可以正确的执行。
为了避免报错,可以在 dao 层 的接口上添加 @Repository 注
通过以上几个简单的步骤,我们就实现了 User 表的 CRUD 功能,甚至连 XML 文件都不用编写!
查看控制台输出:
注意:数据库插入id值默认为:全局唯一id
随着业务规模的不断扩大,需要选择合适的方案去应对数据规模的增长,以应对逐渐增长的访问压力和数据量。 数据库的扩展方式主要包括:业务分库、主从复制,数据库分表
业务分库指的是按照业务模块将数据分散到不同的数据库服务器。例如,一个简单的电商网站,包括用户、商品、订单三个业务模块,我们可以将用户数据、商品数据、订单数据分开放到三台不同的数据库服务器上,而不是将所有数据都放在一台数据库服务器上。这样就变成了3个数据库同时承担压力,系统的吞吐量自然就提高了。 虽然业务分库能够分散存储和访问压力,但同时也带来了新的问题,接下来我们进行详细分析。
join 操作问题 业务分库后,原本在同一个数据库中的表分散到不同数据库中,导致无法使用 SQL 的 join 查询。 事务问题 原本在同一个数据库中不同的表可以在同一个事务中修改,业务分库后,表分散到不同的数据库中,无法通过事务统一修改。 成本问题 业务分库同时也带来了成本的代价,本来 1 台服务器搞定的事情,现在要 3 台,如果考虑备份,那就是 3 台变成了 6 台。读写分离的基本原理是将数据库读写操作分散到不同的节点上。读写分离的基本实现是:
数据库服务器搭建主从集群,一主一从、一主多从都可以。数据库主机负责读写操作,从机只负责读操作。数据库主机通过复制将数据同步到从机,每台数据库服务器都存储了所有的业务数据。业务服务器将写操作发给数据库主机,将读操作发给数据库从机。需要注意的是,这里用的是“主从集群”,而不是“主备集群”。“从机”的“从”可以理解为“仆从”,仆从是要帮主人干活的,“从机”是需要提供读数据的功能的;而“备机”一般被认为仅仅提供备份功能,不提供访问功能。所以使用“主从”还是“主备”,是要看场景的,这两个词并不是完全等同。
将不同业务数据分散存储到不同的数据库服务器,能够支撑百万甚至千万用户规模的业务,但如果业务继续发展,同一业务的单表数据也会达到单台数据库服务器的处理瓶颈。例如,淘宝的几亿用户数据,如果全部存放在一台数据库服务器的一张表中,肯定是无法满足性能要求的,此时就需要对单表数据进行拆分。 单表数据拆分有两种方式:垂直分表和水平分表。示意图如下:
单表进行切分后,是否要将切分后的多个表分散在不同的数据库服务器中,可以根据实际的切分效果来确定。如果性能能够满足业务要求,是可以不拆分到多台数据库服务器的,毕竟我们在上面业务分库的内容看到业务分库也会引入很多复杂性的问题。分表能够有效地分散存储压力和带来性能提升,但和分库一样,也会引入各种复杂性:
垂直分表:
垂直分表适合将表中某些不常用且占了大量空间的列拆分出去。例如,前面示意图中的 nickname 和 description 字段,假设我们是一个婚恋网站,用户在筛选其他用户的时候,主要是用 age 和 sex 两个字段进行查询,而 nickname 和 description 两个字段主要用于展示,一般不会在业务查询中用到。description 本身又比较长,因此我们可以将这两个字段独立到另外一张表中,这样在查询 age 和 sex 时,就能带来一定的性能提升。水平分表:
水平分表适合表行数特别大的表,有的公司要求单表行数超过 5000 万就必须进行分表,这个数字可以作为参考,但并不是绝对标准,关键还是要看表的访问性能。对于一些比较复杂的表,可能超过 1000 万就要分表了;而对于一些简单的表,即使存储数据超过 1 亿行,也可以不分表。但不管怎样,当看到表的数据量达到千万级别时,作为架构师就要警觉起来,因为这很可能是架构的性能瓶颈或者隐患。水平分表相比垂直分表,会引入更多的复杂性,例如数据id:
主键自增:
以最常见的用户 ID 为例,可以按照 1000000 的范围大小进行分段,1 ~ 999999 放到表 1中,1000000 ~ 1999999 放到表2中,以此类推。复杂点:分段大小的选取。分段太小会导致切分后子表数量过多,增加维护复杂度;分段太大可能会导致单表依然存在性能问题,一般建议分段大小在 100 万至 2000 万之间,具体需要根据业务选取合适的分段大小。优点:可以随着数据的增加平滑地扩充新的表。例如,现在的用户是 100 万,如果增加到 1000 万,只需要增加新的表就可以了,原有的数据不需要动。缺点:分布不均匀,假如按照 1000 万来进行分表,有可能某个分段实际存储的数据量只有 1000 条,而另外一个分段实际存储的数据量有 900 万条。Hash :
同样以用户 ID 为例,假如我们一开始就规划了 10 个数据库表,路由算法可以简单地用 user_id % 10 的值来表示数据所属的数据库表编号,ID 为 985 的用户放到编号为 5 的子表中,ID 为 10086 的用户放到编号为 6 的字表中。复杂点:初始表数量的选取。表数量太多维护比较麻烦,表数量太少又可能导致单表性能存在问题。优点:表分布比较均匀。缺点:扩充新的表很麻烦,所有数据都要重分布。分布式ID生成器之雪花算法: 雪花算法是由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碰撞,并且效率较高。
MyBatis-Plus默认的主键策略是:ASSIGN_ID (使用了雪花算法)
@TableId(type = IdType.ASSIGN_ID) private String id;(1)需要在创建数据表的时候设置主键自增 (2)实体字段中配置 @TableId(type = IdType.AUTO)
@TableId(type = IdType.AUTO) private Long id;要想影响所有实体的配置,可以设置全局主键配置
#全局设置主键生成策略 mybatis-plus.global-config.db-config.id-type=auto需求描述: 项目中经常会遇到一些数据,每次都使用相同的方式填充,例如记录的创建时间,更新时间等。我们可以使用MyBatis Plus的自动填充功能,完成这些字段的赋值工作。
在User表中添加datetime类型的新的字段 创建时间 gmt_create、修改时间 gmt_modified
实体上增加字段并添加自动填充注解
@Data public class User { ...... @TableField(fill = FieldFill.INSERT) private Date gmtCreate; //@TableField(fill = FieldFill.UPDATE) @TableField(fill = FieldFill.INSERT_UPDATE) private Date gmtModified; }注意:不要忘记添加 @Component 注解
package com.atguigu.mybatisplus.handler; @Slf4j @Component public class MyMetaObjectHandler implements MetaObjectHandler { @Override public void insertFill(MetaObject metaObject) { log.info("start insert fill ...."); this.setFieldValByName("gmtCreate", new Date(), metaObject); this.setFieldValByName("gmtModified", new Date(), metaObject); } @Override public void updateFill(MetaObject metaObject) { log.info("start update fill ...."); this.setFieldValByName("gmtModified", new Date(), metaObject); } }一件商品,成本价是80元,售价是100元。老板先是通知小李,说你去把商品价格增加50元。小李正在玩游戏,耽搁了一个小时。正好一个小时后,老板觉得商品价格增加到150元,价格太高,可能会影响销量。又通知小王,你把商品价格降低30元。
此时,小李和小王同时操作商品后台系统。小李操作的时候,系统先取出商品价格100元;小王也在操作,取出的商品价格也是100元。小李将价格加了50元,并将100+50=150元存入了数据库;小王将商品减了30元,并将100-30=70元存入了数据库。是的,如果没有锁,小李的操作就完全被小王的覆盖了。
现在商品价格是70元,比成本价低10元。几分钟后,这个商品很快出售了1千多件商品,老板亏1万多。
接下来将我们演示这一过程:
(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) );(2)添加数据
INSERT INTO product (id, NAME, price) VALUES (1, '外星人笔记本', 100);(3)实体类
package com.atguigu.mybatisplus.entity; @Data public class Product { private Long id; private String name; private Integer price; private Integer version; }(4)Mapper
package com.atguigu.mybatisplus.mapper; @Repository public interface ProductMapper extends BaseMapper<Product> { }(5)测试
@Autowired private ProductMapper productMapper; @Test public void testConcurrentUpdate() { //1、小李 Product p1 = productMapper.selectById(1L); System.out.println("小李取出的价格:" + p1.getPrice()); //2、小王 Product p2 = productMapper.selectById(1L); System.out.println("小王取出的价格:" + p2.getPrice()); //3、小李将价格加了50元,存入了数据库 p1.setPrice(p1.getPrice() + 50); productMapper.updateById(p1); //4、小王将商品减了30元,存入了数据库 p2.setPrice(p2.getPrice() - 30); int result = productMapper.updateById(p2); if(result == 0){//更新失败,重试 //重新获取数据 p2 = productMapper.selectById(1L); //更新 p2.setPrice(p2.getPrice() - 30); productMapper.updateById(p2); } //最后的结果 Product p3 = productMapper.selectById(1L); System.out.println("最后的结果:" + p3.getPrice()); }数据库中添加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接下来介绍如何在Mybatis-Plus项目中,使用乐观锁:
(1)修改实体类 添加 @Version 注解
@Version private Integer version;(2)创建配置文件 创建包config,创建文件MybatisPlusConfig.java 此时可以删除主类中的 @MapperScan 扫描注解
package com.atguigu.mybatisplus.config; @EnableTransactionManagement @Configuration @MapperScan("com.atguigu.mybatisplus.mapper") public class MybatisPlusConfig { }(3)注册乐观锁插件 在 MybatisPlusConfig 中注册 Bean
/** * 乐观锁插件 */ @Bean public OptimisticLockerInterceptor optimisticLockerInterceptor() { return new OptimisticLockerInterceptor(); }(4)测试
完成了动态sql的foreach的功能
@Test public void testSelectBatchIds(){ List<User> users = userMapper.selectBatchIds(Arrays.asList(1, 2, 3)); users.forEach(System.out::println); }通过map封装查询条件 注意:map中的key对应数据库中的列名。如:数据库user_id,实体类是userId,这时map的key需要填写user_id
@Test public void testSelectByMap(){ HashMap<String, Object> map = new HashMap<>(); map.put("name", "Helen"); map.put("age", 18); List<User> users = userMapper.selectByMap(map); users.forEach(System.out::println); }MyBatis Plus自带分页插件,只要简单的配置即可实现分页功能 (1)添加分页插件 配置类中添加@Bean配置
/** * 分页插件 */ @Bean public PaginationInterceptor paginationInterceptor() { return new PaginationInterceptor(); }(2)测试selectPage分页 测试:最终通过page对象获取相关数据
@Test public void testSelectPage() { Page<User> page = new Page<>(1,5); Page<User> pageParam = userMapper.selectPage(page, null); pageParam.getRecords().forEach(System.out::println); //当前页 System.out.println(pageParam.getCurrent()); //总页数 System.out.println(pageParam.getPages()); //每页规定多少条 System.out.println(pageParam.getSize()); //总条数 System.out.println(pageParam.getTotal()); //是否有下一页 System.out.println(pageParam.hasNext()); //是否有上一页 System.out.println(pageParam.hasPrevious()); }控制台sql语句打印:SELECT id,name,age,email,create_time,update_time FROM user LIMIT 0,5
当指定了特定的查询列时,希望分页结果列表只返回被查询的列,而不是很多null值 测试selectMapsPage分页:结果集是Map
@Test public void testSelectMapsPage() { //这种方式返回很多null列 //Page<User> page = new Page<>(1, 5); //QueryWrapper<User> queryWrapper = new QueryWrapper<>(); //指定查询的列 //queryWrapper.select("name", "age"); //Page<User> pageParam = userMapper.selectPage(page, queryWrapper); // //pageParam.getRecords().forEach(System.out::println); Page<Map<String, Object>> page = new Page<>(1, 5); QueryWrapper<User> queryWrapper = new QueryWrapper<>(); queryWrapper.select("name", "age"); Page<Map<String, Object>> pageParam = userMapper.selectMapsPage(page, queryWrapper); List<Map<String, Object>> records = pageParam.getRecords(); records.forEach(System.out::println); System.out.println(pageParam.getCurrent()); System.out.println(pageParam.getPages()); System.out.println(pageParam.getSize()); System.out.println(pageParam.getTotal()); System.out.println(pageParam.hasNext()); System.out.println(pageParam.hasPrevious()); }逻辑删除的使用场景:
可以进行数据恢复有关联数据,不便删除(1)数据库修改 添加 deleted字段 tinyint类型就相当于是boolean类型
ALTER TABLE `user` ADD COLUMN `deleted` boolean DEFAULT false(2)实体类修改 添加deleted 字段,并加上 @TableLogic 注解
@TableLogic //表示为逻辑字段 private Integer deleted;(3)配置(可选) application.properties 加入以下配置,此为默认值,如果你的默认值和mp默认的一样,该配置可无 注意:如果改了以下配置,数据库中逻辑删除字段的默认值也得改。
mybatis-plus.global-config.db-config.logic-delete-value=1 mybatis-plus.global-config.db-config.logic-not-delete-value=0(4)测试
测试后发现,数据并没有被删除,deleted字段的值由0变成了1测试后分析打印的sql语句,是一条update注意:被删除前,数据的deleted 字段的值必须是 0,才能被选取出来执行逻辑删除的操作 @Test public void testLogicDelete() { int result = userMapper.deleteById(1L); System.out.println(result); }(5)测试逻辑删除后的查询 MyBatis Plus中查询操作也会自动添加逻辑删除字段的判断
@Test public void testLogicDeleteSelect() { List<User> users = userMapper.selectList(null); users.forEach(System.out::println); }Wrapper : 条件构造抽象类,最顶端父类 AbstractWrapper : 用于查询条件封装,生成 sql 的 where 条件 QueryWrapper : 查询条件封装 UpdateWrapper : Update 条件封装 AbstractLambdaWrapper : 使用Lambda 语法 LambdaQueryWrapper :用于Lambda语法使用的查询Wrapper LambdaUpdateWrapper : Lambda 更新封装Wrapper
@SpringBootTest public class QueryWrapperTests { @Autowired private UserMapper userMapper; }注意:seletOne()返回的是一条实体记录,当出现多条时会报错
@Test public void testSelectOne() { QueryWrapper<User> queryWrapper = new QueryWrapper<>(); queryWrapper.eq("name", "Tom"); User user = userMapper.selectOne(queryWrapper);//只能返回一条记录,多于一条则抛出异常 System.out.println(user); }包含大小边界
@Test public void testSelectCount() { QueryWrapper<User> queryWrapper = new QueryWrapper<>(); queryWrapper.between("age", 20, 30); Integer count = userMapper.selectCount(queryWrapper); //返回数据数量 System.out.println(count); }selectMaps()返回Map集合列表,通常配合select()使用
@Test public void testSelectMaps() { QueryWrapper<User> queryWrapper = new QueryWrapper<>(); queryWrapper .select("name", "age") .like("name", "e") .likeRight("email", "5"); List<Map<String, Object>> maps = userMapper.selectMaps(queryWrapper);//返回值是Map列表 maps.forEach(System.out::println); }in、notIn:
notIn(“age”,{1,2,3})—>age not in (1,2,3)notIn(“age”, 1, 2, 3)—>age not in (1,2,3)inSql、notinSql:可以实现子查询
例: inSql(“age”, “1,2,3,4,5,6”)—>age in (1,2,3,4,5,6)例: inSql(“id”, “select id from table where id < 3”)—>id in (select id from table where id < 3) @Test public void testSelectObjs() { QueryWrapper<User> queryWrapper = new QueryWrapper<>(); //queryWrapper.in("id", 1, 2, 3); queryWrapper.inSql("id", "select id from user where id <= 3"); List<Object> objects = userMapper.selectObjs(queryWrapper);//返回值是Object列表 objects.forEach(System.out::println); }不调用or则默认为使用 and 连
@Test public void testUpdateOr() { //修改值 User user = new User(); user.setAge(99); user.setName("Andy"); //修改条件 QueryWrapper<User> queryWrapper = new QueryWrapper<>(); queryWrapper .like("name", "h") .or() .between("age", 20, 30); int result = userMapper.update(user, queryWrapper); System.out.println(result); }lambda表达式内的逻辑优先运算
@Test public void testUpdateLambda() { //修改值 User user = new User(); user.setAge(99); user.setName("Andy"); //修改条件 QueryWrapper<User> queryWrapper = new QueryWrapper<>(); queryWrapper .like("name", "n") .or(i -> i.like("name", "a").eq("age", 20)); int result = userMapper.update(user, queryWrapper); System.out.println(result); }注意:这里使用的是 UpdateWrapper 最终的sql会合并 user.setAge(),以及 userUpdateWrapper.set() 和 setSql() 中 的字段
@Test public void testUpdateSet() { //修改值 User user = new User(); //修改条件 UpdateWrapper<User> userUpdateWrapper = new UpdateWrapper<>(); userUpdateWrapper .set("name", "Peter")//可以使用set设置修改的字段 .set("age", 60) .setSql(" email = '123@qq.com'")//可以有子查询 .like("name", "h"); int result = userMapper.update(user, userUpdateWrapper); System.out.println(result); }