简介
MyBatis-Plus(简称 MP)是一个 MyBatis 的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。
只做增强不做改变,引入它不会对现有工程产生影响,如丝般顺滑。
只需简单配置,即可快速进行 CRUD 操作,从而节省大量时间。
热加载、代码生成、分页、性能分析等功能一应俱全。
如何使用
导入依赖
注意**:**引入 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用来简化实体类-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
application.yml
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
#mybatis日志
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
这里在url后面加上?serverTimezone=GMT%2B8是因为8.0版本的jdbc驱动需要添加这个后缀,否则运行测试用例报告如下错误:
java.sql.SQLException: The server time zone value 'Öйú±ê׼ʱ¼ä' is unrecognized or represents more 。
而且也可以发现这里的驱动不再是com.mysql.jdbc.Driver了,也是由于jdbc 8的版本,需要改用com.mysql.cj.jdbc.Driver.
然后,按步骤加上主启动类(需要加上@MapperScan注解去加载mapper)、实体类。
mapper接口我们可以直接继承BasedMapper<表对应实体类>,就可以使用了 。
这里注意,
IDEA在 userMapper 处报错,因为找不到注入的对象,因为类是动态创建的,但是程序可以正确的执行。
为了避免报错,可以在dao层的接口上添加 @Repository 注解,或者使用@Resource 替换@Repository。
比如如下格式。
@Repository
public interface UserMapper extends BaseMapper<User> {
}
如果想要测试SpringBoot工程,可以在测试类上加上@SpringBootTest注解,就可以正常注入bean了。然后测试方法上加上@Test。
数据库分库分表策略
背景
随着业务规模的不断扩大,需要选择合适的方案去对应数据规模的增长,以应对主键增长的访问量。
数据库的扩展方式主要包括:业务分库、主从复制、数据库分表。
1.业务分库
业务分库指的是按照业务模块将数据分散到不同的数据库服务器。例如:电商网站,包括用户,商品、订单三个业务模块,我们可以将用户数据,商品数据和订单数据分开放在三台不同的数据库服务器上,而不是将所有数据都放在同一台数据库服务器上,这样就变成了3个数据库同时承担压力,系统的吞吐量自然就高了。
产生的新问题:
- join操作问题。业务分库之后,原本在一个数据库里的表分散到不同的库里,就会导致无法使用sql 的join查询
- 事务问题。原本在同一个数据库中的表可以在同一个事务中修改,业务分库后,表分散到不同的数据库中,无法通过事务统一修改了。
- 成本问题。需要增加服务器。
2.主从复制,读写分离
读写分离的基本原理:
将数据库读写操作分散到不同的节点上。
如何实现:
- 数据库服务器搭建主从集群,一主一从,一主多从都可以。
- 数据库的主机负责读写,从机只负责读操作。
- 数据库主机通过复制将数据同步到从机,每台数据库服务器都存储了所有的业务数据。
- 业务服务器将写操作发给数据库主机,将读操作发给数据库从机。
注意:“主从”和“主备”中的从机和备用机的功能有所不同,从机需要提供读数据的功能,而备机一般被认为只提供备份功能,不提供访问功能,所以使用“主从”还是“主备”是需要看场景的。
3.数据库分表
单表数据拆分有两种方式:垂直分表和水平分表。
单表进行切分后,是否要将切分后的多个表分散在不同的数据库服务器中,可以提供实际的切分效果来确定。如果性能能够哦满足业务要求,是可以不拆分到堕胎数据库服务器的,因为业务分库会带来很多复杂的问题。分表能够有效的分散存储压力,提升性能,但是和分库一样,会引入各种复杂性。
垂直分表:
- 垂直分表适合将表中某些不常用并且占用了大量空间的列拆分出去
水平分表:
- 水平分表适合表行数特别大的表。当表的数据量达到千万级别时(当然也和表的字段数量有关),就要考虑是否进行分表了,因为这很可能是架构的性能瓶颈或者隐患。
水平分表相比垂直分表,会引入更多复杂的问题,比如主键id。
解决方案1——主键自增
以最常见的用户 ID 为例,可以按照 1000000 的范围大小进行分段,1 ~ 999999 放到表 1中,1000000 ~ 1999999 放到表2中,以此类推。
这个解决方案的问题在于分段大小的选取,分段太小会导致后子表数据量过多,增加维护复杂度;分段太大可能会导致单表依然存在性能问题,一般建议分段大小在100万到2000万之间,具体需要根究业务选取合适的分段大小。
优点是:可以随着数据的增加平滑的扩充新的表。例如,现在的用户是100万,如果增加到1000万,只需要增加新的表就可以了,原有的数据不需要动。
解决方案2——Hash
同样以用户 ID 为例,假如我们一开始就规划了 10 个数据库表,路由算法可以简单地用 user_id % 10 的值来表示数据所属的数据库表编号,ID 为 985 的用户放到编号为 5 的子表中,ID 为 10086 的用户放到编号为 6 的字表中。
复杂点:初始表数量的选取,表数量太多维护困难,数量太少可能会导致单表性能存在问题。
优点:表分布的比较均匀
缺点:扩充新的表比较麻烦,所有数据都要重新分布。
ID生成器:雪花算法snowflake
雪花算法是Twitter公布的分布式主键生成算法,它能够保证不同表的主键不重复性,以及相同表的主键的有序性。
核心思想:
- 长度一共64bit(long)
- 最前一个符号位,1bit,由于long在java中是带符号的,最高位是符号位,id一般是正数,最高位是0.
- 41bit的时间戳(毫秒级),存储的是时间戳的差值(当前时间戳-开始时间戳)。
- 10bit,是机器的id(其中5bit是数据中心,5bit是机器ID,可以部署在1024个节点)
- 12bit,毫秒内的流水号,也就意味着每个节点在每毫秒内可以产生4096个ID
优点:整体上按照时间自增排序,并且整个分布式系统内不会产生ID碰撞,并且效率比较高
MyBatis-Plus(MP)的主键策略
@TableId注解:指定主键策略,声明于类变量上。
1.ASSIGN_ID
MP默认的主键策略是ASSIGN_ID,也就是使用了雪花算法。
@TableId(type = IdType.ASSIGN_ID)
private String id;
2.AUTO自增策略
需要在创建数据表的时候设置主键自增。
@TableId(type = IdType.AUTO)
private Long id;
如果想要影响所有实体的配置,可以设置全局主键配置
#全局设置主键生成策略
mybatis-plus.global-config.db-config.id-type=auto
MP的自动填充功能
在项目中会遇到一些数据,每次都使用相同的方式填充,例如记录的创建时间,更新时间等。就可以使用MP的自动填充功能。
@TableField指定普通字段策略。
在注解中传入fill参数(用FieldFill中的字段填写)
@TableField(fill = FieldFill.INSERT)
private Date gmtCreate;
//@TableField(fill = FieldFill.UPDATE)
@TableField(fill = FieldFill.INSERT_UPDATE)
private Date gmtModified;
如何实现乐观锁
-
在表中添加version字段,取出记录时也要获取当前的version,每次更新version就+1,如果where中version版本不对,就让这次的更新失败。
-
修改实体类,在version属性上加@Version注解。
-
创建配置文件MybatisPlusConfig,这时可以删除主类中的@MapperScan扫描注解
@EnableTransactionManagement @Configuration @MapperScan("com.dyy.mybatisplus.mapper") public class MybatisPlusConfig { } -
注册乐观锁插件(其实就是注册一个bean)
/** * 乐观锁插件 */ @Bean public OptimisticLockerInterceptor optimisticLockerInterceptor() { return new OptimisticLockerInterceptor(); }
查询
1.通过多个id批量查询
mapper继承自BaseMapper接口中有已经实现的方法selectBatchIds(集合)。
@Test
public void testSelectBatchIds(){
List<User> users = userMapper.selectBatchIds(Arrays.asList(1, 2, 3));
users.forEach(System.out::println);
}
2.简单的条件查询
通过map封装查询条件。
方法:selectByMap(Map map);
注意map中的key必须完全对应数据库中的列名。
@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);
}
3.查询指定的列
当指定了特定的查询列时,希望分页结果只返回被查询的列,而不是很多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());
}
分页
MP自带分页插件。
-
添加分页插件,配置类中添加@Bean配置
/** * 分页插件 */ @Bean public PaginationInterceptor paginationInterceptor() { return new PaginationInterceptor(); } -
测试selectPage分页
@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
例子:
service接口中有如下业务
IPage<Teacher> selectPage(Long page, Long limit, TeacherQuery teacherQuery);
实现类(加上注解@Service,否则controller层调用无法自动注入)。
@Service
public class TeacherServiceImpl extends ServiceImpl<TeacherMapper, Teacher> implements TeacherService {
@Override
public IPage<Teacher> selectPage(Long page, Long limit, TeacherQuery teacherQuery) {
Page<Teacher> pageParam = new Page<>(page, limit);
QueryWrapper<Teacher> queryWrapper = new QueryWrapper<>();
queryWrapper.orderByAsc("sort");
if (teacherQuery == null){
return baseMapper.selectPage(pageParam, queryWrapper);
}
String name = teacherQuery.getName();
Integer level = teacherQuery.getLevel();
String begin = teacherQuery.getJoinDateBegin();
String end = teacherQuery.getJoinDateEnd();
if (!StringUtils.isEmpty(name)) {
//左%会使索引失效
queryWrapper.likeRight("name", name);
}
if (level != null) {
queryWrapper.eq("level", level);
}
if (!StringUtils.isEmpty(begin)) {
queryWrapper.ge("join_date", begin);
}
if (!StringUtils.isEmpty(end)) {
queryWrapper.le("join_date", end);
}
return baseMapper.selectPage(pageParam, queryWrapper);
}
}
controller,最后传给前端的是一个IPage对象
@ApiOperation("分页讲师列表")
@GetMapping("list/{page}/{limit}")
public R listPage(@ApiParam(value = "当前页码", required = true) @PathVariable Long page,
@ApiParam(value = "每页记录数", required = true) @PathVariable Long limit,
@ApiParam("讲师列表查询对象") TeacherQuery teacherQuery){
IPage<Teacher> pageModel = teacherService.selectPage(page, limit, teacherQuery);
return R.ok().data("pageModel", pageModel);
}
删除
1.根据id删除
方法:deleteById(long id)
@Test
public void testDeleteById(){
int result = userMapper.deleteById(5L);
System.out.println(result);
}
2.批量删除
方法:deleteBatchIds(List list)
@Test
public void testDeleteBatchIds() {
int result = userMapper.deleteBatchIds(Arrays.asList(8, 9, 10));
System.out.println(result);
}
3.简单条件删除
方法:deleteByMap(map)
@Test
public void testDeleteByMap() {
HashMap<String, Object> map = new HashMap<>();
map.put("name", "Helen");
map.put("age", 18);
int result = userMapper.deleteByMap(map);
System.out.println(result);
}
4.逻辑删除
一般来说,我们将删除分类为物理删除和逻辑删除
- 物理删除:真实删除,将对应数据从数据库中删除,之后查询不到这条被删除的数据。
- 逻辑删除:假删除,将对应数据中代表是否被删除字段的状态改为“被删除状态”,之后再数据库中仍旧能够看到这条数据记录。
逻辑删除的使用场景:
- 可以进行数据恢复
- 有关联数据,也不方便删除
实现流程
-
表添加字段deleted,布尔类型,默认值为false(数据库中boolean值会默认转为0或1,false->0,true->1)
-
实体类修改,添加上deleted字段,并且再字段上加上@TabelLogic注解(标记为逻辑删除)
-
可以改动下面的配置,这个配置是MP中的默认配置,如果和它一样,就不用改了。主要是给MP识别和区分数据的删除状态的。
mybatis-plus.global-config.db-config.logic-delete-value=1 mybatis-plus.global-config.db-config.logic-not-delete-value=0
配置完成后再进行测试删除方法,查看数据库数据是没有被删除的,deleted字段的值会由0变1.所以这个时候实际上进行的语句是update。
删除后再进行select操作,被标记为已删除的字段也不会被查出来。
注意,被删除前,数据的delete字段的值必须是0,才能被选取出来执行逻辑删除的。
Wapper类
是一个条件构造抽象类。用于封装查询和更新条件。
它的子类包括:
- AbstractWrapper : 用于查询条件封装,生成 sql 的 where 条件
- QueryWrapper : 查询条件封装(常用)
- UpdateWrapper : Update 条件封装
- AbstractLambdaWrapper : 使用Lambda 语法
- LambdaQueryWrapper :用于Lambda语法使用的查询Wrapper
- LambdaUpdateWrapper : Lambda 更新封装Wrapper
方法
| 查询方式 | 说明 |
|---|---|
| setSqlSelect | 设置 SELECT 查询字段 |
| where | WHERE 语句,拼接 + WHERE 条件 |
| and | AND 语句,拼接 + AND 字段=值 |
| andNew | AND 语句,拼接 + AND (字段=值) |
| or | OR 语句,拼接 + OR 字段=值 |
| orNew | OR 语句,拼接 + OR (字段=值) |
| eq | 等于= |
| allEq | 基于 map 内容等于= |
| ne | 不等于<> |
| gt | 大于> |
| ge | 大于等于>= |
| lt | 小于< |
| le | 小于等于<= |
| like | 模糊查询 LIKE |
| notLike | 模糊查询 NOT LIKE |
| in | IN 查询 |
| notIn | NOT IN 查询 |
| isNull | NULL 值查询 |
| isNotNull | IS NOT NULL |
| groupBy | 分组 GROUP BY |
| having | HAVING 关键词 |
| orderBy | 排序 ORDER BY |
| orderAsc | ASC 排序 ORDER BY |
| orderDesc | DESC 排序 ORDER BY |
| exists | EXISTS 条件语句 |
| notExists | NOT EXISTS 条件语句 |
| between | BETWEEN 条件语句 |
| notBetween | NOT BETWEEN 条件语句 |
| addFilter | 自由拼接 SQL |
| last | 拼接在最后,例如:last(“LIMIT 1”) |
都可以在QueryWrapper的对象中调用到这些方法,然后将条件参数传进去就可以了。然后将包装好的QueryWrapper对象传给selectMaps就可以查出数据了。
MP代码生成器
转自官方文档:baomidou.com/pages/779a6…
1.依赖
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.5.1</version>
</dependency>
注意:这个包没有传递依赖MP包,需要自己引入。
2.生成
快速生成:
FastAutoGenerator.create("url", "username", "password")
.globalConfig(builder -> {
builder.author("baomidou") // 设置作者
.enableSwagger() // 开启 swagger 模式
.fileOverride() // 覆盖已生成文件
.outputDir("D://"); // 指定输出目录
})
.packageConfig(builder -> {
builder.parent("com.baomidou.mybatisplus.samples.generator") // 设置父包名
.moduleName("system") // 设置父包模块名
.pathInfo(Collections.singletonMap(OutputFile.mapperXml, "D://")); // 设置mapperXml生成路径
})
.strategyConfig(builder -> {
builder.addInclude("t_simple") // 设置需要生成的表名
.addTablePrefix("t_", "c_"); // 设置过滤表前缀
})
.templateEngine(new FreemarkerTemplateEngine()) // 使用Freemarker引擎模板,默认的是Velocity引擎模板
.execute();
也可以使用交互式生成
FastAutoGenerator.create(DATA_SOURCE_CONFIG)
// 全局配置
.globalConfig((scanner, builder) -> builder.author(scanner.apply("请输入作者名称?")).fileOverride())
// 包配置
.packageConfig((scanner, builder) -> builder.parent(scanner.apply("请输入包名?")))
// 策略配置
.strategyConfig((scanner, builder) -> builder.addInclude(getTables(scanner.apply("请输入表名,多个英文逗号分隔?所有输入 all")))
.controllerBuilder().enableRestStyle().enableHyphenStyle()
.entityBuilder().enableLombok().addTableFills(
new Column("create_time", FieldFill.INSERT)
).build())
/*
模板引擎配置,默认 Velocity 可选模板引擎 Beetl 或 Freemarker
.templateEngine(new BeetlTemplateEngine())
.templateEngine(new FreemarkerTemplateEngine())
*/
.execute();
// 处理 all 情况
protected static List<String> getTables(String tables) {
return "all".equals(tables) ? Collections.emptyList() : Arrays.asList(tables.split(","));
}