MyBatis-Plus开发实战
大约 38 分钟
MyBatis-Plus 简介
MyBatis-Plus
- 无侵入性:它对现有的应用程序架构没有影响,可以在不修改原有代码的情况下进行增强。
- 高性能:由于它是增量式的增强,因此不会影响原有的性能。
- 强大的 CRUD 操作:内置了通用 Mapper 和通用 Service,使得大多数 CRUD 操作可以通过少量的配置实现。
- 支持 Lambda 表达式 :可以使用 Lambda 表达式方便地编写查询条件,避免了对字段拼写的担忧。
- 支持主键自动生成:提供了多种主键策略,包括分布式唯一 ID 生成器,以适应不同的应用场景。
- 支持 ActiveRecord 模式:使得实体类可以直接继承 Model 类来进行 CRUD 操作。
- 支持自定义通用操作:允许全局通用方法的注入,减少重复代码的开发量。
- 内置代码生成器:可以通过代码或 Maven 插件快速生成 Mapper、Model、Service、Controller 层的代码,并支持模板引擎。
- 内置分页插件:基于 MyBatis 物理分页,简化了分页功能的实现,无需关注具体的分页逻辑。
- 分页插件支持多种数据库:包括但不限于 MySQL、MariaDB、Oracle、DB2、H2、HSQL、SQLite、PostgreSQL 和 SQLServer 等。
- 内置性能分析插件:能够输出 SQL 语句的执行时间和相关信息,有助于诊断慢查询问题。
- 内置全局拦截插件:提供全表 delete、update 操作的智能分析阻断,也可以自定义拦截规则。
MyBatis-Plus 项目首页
- MyBatis-Plus 是一个不断维护的开源项目,开发者可以通过其官方站点获取该项目的信息,同时该站点也提供了该组件的使用指南以及讨论的相关信息
MyBatis-Plus 官方框架结构
- MyBatis-Plus 应用是在 MyBatis 框架的基础上构建的,其主要简化了数据层中的方法定,基于一些特定的代码生成逻辑,减少了重复类型的 SQL 命令定义,最终的数据执行依然由 MyBatis 框架发出,所以在配置 MyBatis-Plus 之前首先需要在项目中配置好随后再利用 MyBatis 特定的标记进行数据层定义,以实现最 Spring 与 MyBatis 的整合终的数据层代码开发
MyBatis-Plus 编程起步
1、
DROP DATABASE IF EXISTS yootk;
CREATE DATABASE yootk CHARACTER SET UTF8;
USE yootk;
CREATE TABLE project (
pid BIGINT AUTO_INCREMENT comment '项目ID',
name VARCHAR(50) comment '项目名称',
charge VARCHAR(50) comment '项目主管',
note TEXT comment '项目描述',
status INT comment '项目状态',
CONSTRAINT pk_pid PRIMARY KEY(pid)
) engine=innodb;
2、
// https://mvnrepository.com/artifact/com.baomidou/mybatis-plus
implementation group: 'com.baomidou', name: 'mybatis-plus', version: '3.5.2'
implementation group: 'org.springframework', name: 'spring-orm', version: '6.0.0-M3'
3、
package com.yootk;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
@ComponentScan("com.yootk")
@EnableAspectJAutoProxy // 启动内置的事务支持
public class StartMyBatisPlusApplication {
}
4、
yootk.database.driverClassName=com.mysql.cj.jdbc.Driver
yootk.database.jdbcUrl=jdbc:mysql://localhost:3306/yootk
yootk.database.username=root
yootk.database.password=mysqladmin
yootk.database.connectionTimeOut=3000
yootk.database.readOnly=false
yootk.database.pool.idleTimeOut=3000
yootk.database.pool.maxLifetime=60000
yootk.database.pool.maximumPoolSize=60
yootk.database.pool.minimumIdle=20
5、
package com.yootk.config;
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
import org.apache.ibatis.annotations.Mapper;
import org.mybatis.spring.mapper.MapperScannerConfigurer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.sql.DataSource;
@Configuration
public class MyBatisPlusConfig { // MyBaits-Plus项目配置类
@Bean
public MybatisSqlSessionFactoryBean sqlSessionFactoryBean(DataSource dataSource) {
MybatisSqlSessionFactoryBean factoryBean = new MybatisSqlSessionFactoryBean();
factoryBean.setDataSource(dataSource);
factoryBean.setTypeAliasesPackage("com.yootk.vo"); // 类型扫描包
return factoryBean;
}
@Bean
public MapperScannerConfigurer mapperScannerConfigurer() {
MapperScannerConfigurer configurer = new MapperScannerConfigurer();
configurer.setBasePackage("com.yootk.dao");
configurer.setAnnotationClass(Mapper.class);
return configurer;
}
}
6、
package com.yootk.vo;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
@TableName("project") // 此时表名称与类名称一致
public class Project {
@TableId(type = IdType.AUTO) // 自动增长ID
private Long pid;
@TableField("name")
private String name; // 列名称映射
private String charge;
private String note;
private Integer status;
public Long getPid() {
return pid;
}
public void setPid(Long pid) {
this.pid = pid;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getCharge() {
return charge;
}
public void setCharge(String charge) {
this.charge = charge;
}
public String getNote() {
return note;
}
public void setNote(String note) {
this.note = note;
}
public Integer getStatus() {
return status;
}
public void setStatus(Integer status) {
this.status = status;
}
}
7、
package com.yootk.dao;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.yootk.vo.Project;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface IProjectDAO extends BaseMapper<Project> {
}
8、
package com.yootk.test;
import com.yootk.StartMyBatisPlusApplication;
import com.yootk.dao.IProjectDAO;
import com.yootk.vo.Project;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
@ContextConfiguration(classes = StartMyBatisPlusApplication.class)
@ExtendWith(SpringExtension.class)
public class TestProjectDAO {
private static final Logger LOGGER = LoggerFactory.getLogger(TestProjectDAO.class);
@Autowired
private IProjectDAO projectDAO;
@Test
public void testAdd() { // 测试数据增加操作
Project project = new Project(); // 创建VO类对象
project.setName("李兴华Java就业编程训练营");
project.setCharge("李兴华");
project.setNote("系统的讲解Java程序员与Java架构师的完整技术图书,配套视频");
project.setStatus(0); // 0表示未删除
LOGGER.info("更新数据行数:{}、当前项目ID:{}",
this.projectDAO.insert(project), project.getPid());
}
}
BaseMapper 接口
BaseMapper 接口继承结构
- MyBatis-Plus 的核心功能是在于可以通过程序自动的生成最终要执行的 SQL 命令,而这一操作实现的核心,主要是依赖于 BaseMapper 接口
1、
@Test
public void testEdit() {
Project project = new Project(); // 创建VO类对象
project.setPid(1L); // 配置数据的ID
project.setName("GoLang就业编程训练营");
project.setCharge("李兴华");
project.setNote("高并发应用设计");
project.setStatus(0); // 0表示未删除
LOGGER.info("更新数据行数:{}", this.projectDAO.updateById(project));
}
2、
@Test
public void testSelectId() {
Project project = this.projectDAO.selectById(1L); // 根据ID查询数据
LOGGER.info("【项目信息】项目ID:{}、项目名称:{}、负责人:{}、说明:{}、状态:{}",
project.getPid(), project.getName(), project.getCharge(),
project.getNote(), project.getStatus());
}
3、
@Test
public void testSelectMap() {
Map<String, Object> params = new HashMap<>(); // 配置数据查询条件
params.put("pid", 2L); // 设置id的条件
params.put("charge", "李兴华"); // 条件配置
params.put("status", 0); // 条件配置
List<Project> projects = this.projectDAO.selectByMap(params);
for (Project project : projects) {
LOGGER.info("【项目信息】项目ID:{}、项目名称:{}、负责人:{}、说明:{}、状态:{}",
project.getPid(), project.getName(), project.getCharge(),
project.getNote(), project.getStatus());
}
}
4、
@Test
public void testDeleteById() {
LOGGER.info("删除项目数据:{}",
this.projectDAO.deleteBatchIds(List.of(1L, 2L, 3L)));
}
Wrapper 条件构造器
Wrapper 包装处理
- 在 SQL 语句的编写中,往往会基于一些特定的条件进行数据的更新或查询操作,例如:通过 where 子句并结合一系列的表达式进行限定查询、使用 group by 进行数据的分组统计,使用 order by 进行结果集排序等操作。为了满足此类的开发需要,在 MyBatis-Plus 中提供了 Wrapper 条件构造器
AbstractWrapper 类继承结构
- AbstractWrapper 抽象类中提供了大量的 SQL 命令配置方法,例如:isNull()、eq()、between()、groupby()、having()等,为了便于这些方法的管理,AbstractWrapper 同时实现了 Compare、Nested、Join、Func 四个父接口
1、
package com.yootk.test;
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.yootk.StartMyBatisPlusApplication;
import com.yootk.dao.IProjectDAO;
import com.yootk.vo.Project;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@ContextConfiguration(classes = StartMyBatisPlusApplication.class)
@ExtendWith(SpringExtension.class)
public class TestProjectWrapper { // 数据查询控制
private static final Logger LOGGER = LoggerFactory.getLogger(TestProjectWrapper.class);
@Autowired
private IProjectDAO projectDAO;
@Test
public void testSelectId() { // 根据ID进行数据查询
// QueryWrapper是Wrapper抽象类的子类,但是在做查询的时候需要定义返回值类型
QueryWrapper<Project> wrapper = new QueryWrapper<>(); // 定义查询包装期
wrapper.eq("pid", 3L); // 查询项目编号为3的信息
Project project = this.projectDAO.selectOne(wrapper); // 查询单个数据
LOGGER.info("【项目信息】项目ID:{}、项目名称:{}、负责人:{}、说明:{}、状态:{}",
project.getPid(), project.getName(), project.getCharge(),
project.getNote(), project.getStatus());
}
}
2、
@Test
public void testSelectCharge() {
QueryWrapper<Project> wrapper = new QueryWrapper<>(); // 查询包装器
// 此时需要编写两个条件,并且同时满足,肯定使用and运算符进行连接
wrapper.and(c -> c.eq("charge", "李兴华").like("name", "编程"));
List<Project> projects = this.projectDAO.selectList(wrapper); // 数据查询
for (Project project : projects) { // 数据迭代
LOGGER.info("【项目信息】项目ID:{}、项目名称:{}、负责人:{}、说明:{}、状态:{}",
project.getPid(), project.getName(), project.getCharge(),
project.getNote(), project.getStatus());
}
}
3、
@Test
public void testDelete() {
QueryWrapper<Project> wrapper = new QueryWrapper<>();
// 此时使用了OR进行多个条件的连接,采用的是第二种写法,这种写法就是使用代码链的方式定义
wrapper.eq("charge", "李兴华").or().between("pid", 10, 20);
LOGGER.info("根据条件删除项目:{}", this.projectDAO.delete(wrapper));
}
4、
@Test
public void testGroup() {
QueryWrapper<Project> wrapper = new QueryWrapper<>();
wrapper.select("charge", "COUNT(*) AS count"); // 定义SELECT子句
wrapper.groupBy("charge"); // 分组列
wrapper.having("count >= 1"); // 1个以上项目
wrapper.orderByDesc("count"); // 根据数量降序排列
List<Map<String, Object>> results = this.projectDAO.selectMaps(wrapper);
for (Map<String, Object> result : results) {
LOGGER.info("【分组统计查询】负责人姓名:{}、项目数量:{}",
result.get("charge"), result.get("count"));
}
}
5、
@Test
public void testLambda() {
LambdaQueryWrapper<Project> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Project::getPid, 6L).or().eq(Project::getStatus, 0);
List<Project> projects = this.projectDAO.selectList(wrapper); // 数据查询
for (Project project : projects) { // 数据迭代
LOGGER.info("【项目信息】项目ID:{}、项目名称:{}、负责人:{}、说明:{}、状态:{}",
project.getPid(), project.getName(), project.getCharge(),
project.getNote(), project.getStatus());
}
}
6、
@Test
public void testUpdate() { // 数据条件更新
QueryWrapper<Project> wrapper = new QueryWrapper<>();
wrapper.and(c->{
c.eq("pid", 6L).eq("status", 0);
});
Project project = new Project(); // 数据更新的内容
project.setNote("Python编程训练营");
project.setCharge("小李老师");
LOGGER.info("数据更新操作:{}", this.projectDAO.update(project, wrapper));
}
逻辑删除
逻辑删除
- 运行在生产环境中的项目,都会存在有大量的业务数据,而这些数据由于会与具体的业务产生关联,所以一般不会采用 DELETE 这种物理删除的方式,那么常用的做法就是进即:在数据表中追加一个逻辑删除的字段,行数据的逻辑删除当该字段为某个数值的时候表示是删除状态,这样在进行数据查询时就不再返回此数据项
1、
@Bean
public GlobalConfig.DbConfig dbConfig() { // 全局数据配置
GlobalConfig.DbConfig dbConfig = new GlobalConfig.DbConfig();
// 如果此时你所有的逻辑删除字段是统一的名称,那么也可以直接在此定义
// dbConfig.setLogicDeleteField("status"); // 如果不统一,通过注解配置
dbConfig.setLogicDeleteValue("1"); // 逻辑删除的内容
dbConfig.setLogicNotDeleteValue("0"); // 逻辑未删除内容
return dbConfig;
}
2、
@TableLogic // 逻辑删除标记字段
private Integer status;
3、
@Test
public void testDelete() {
LOGGER.info("根据条件删除项目:{}", this.projectDAO.deleteById(7L)); // 删除数据
}
4、
@Test
public void testSelectId() { // 根据ID进行数据查询
// QueryWrapper是Wrapper抽象类的子类,但是在做查询的时候需要定义返回值类型
QueryWrapper<Project> wrapper = new QueryWrapper<>(); // 定义查询包装期
wrapper.eq("pid", 7L); // 查询项目编号为3的信息
Project project = this.projectDAO.selectOne(wrapper); // 查询单个数据
LOGGER.info("【项目信息】项目ID:{}、项目名称:{}、负责人:{}、说明:{}、状态:{}",
project.getPid(), project.getName(), project.getCharge(),
project.getNote(), project.getStatus());
}
数据填充
数据填充
- 在一些业务逻辑繁琐的项目开发中,业务层除了需要接收数据对象之外,还需进行这些对象中部分数据的填充处理,例如:将当前的系统日期保存在数据对象之中,或者配置数据对象中的某个成员属性为固定内容,而这些操作的目的就是保证存储数据的有效性
在 MyBatis-Plus 之中为了简化业务层中关于此部分的操作逻辑,可以基于全局配置的方式在数据进行增加或修改时自动对指定的数据进行填充处理
MetaObjectHandler 接囗配置结构
- 子类实现 MetaObjectHandler 接口后,需要覆写该类的两个方法,一个是“insertFil()”方法,用于在数据增加前进行填充,另外一个是“updateFil()”方法用于在数据更新前进行填充,本次将基于这两个方法实现 status 字段的自动填充处理在增加和修改时将 status 的内容默认设置为 0(非删除状态)
1、
package com.yootk.handler;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import org.apache.ibatis.reflection.MetaObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
// MetaObjectHandler是由MP所提供的一个填充器的处理,这个操作可以在更新增加的时候自动设置属性内容
@Component
public class YootkMetaObjectHandler implements MetaObjectHandler {
private static final Logger LOGGER =
LoggerFactory.getLogger(YootkMetaObjectHandler.class); // 获取日志对象
@Override
public void insertFill(MetaObject metaObject) { // 增加时填充
LOGGER.info("【MetaObject】获取原始对象:{}", metaObject.getOriginalObject());
LOGGER.info("【MetaObject】获取name属性内容:{}", metaObject.getValue("name"));
LOGGER.info("【MetaObject】是否存在有getName()方法:{}",
metaObject.hasGetter("name"));
LOGGER.info("【MetaObject】是否存在有setName()方法:{}",
metaObject.hasSetter("name"));
// 在数据增加的时候status字段的内容默认为0
this.strictInsertFill(metaObject, "status", Integer.class, 0);
}
@Override
public void updateFill(MetaObject metaObject) { // 修改时填充
this.strictInsertFill(metaObject, "status", Integer.class, 0);
}
}
2、
package com.yootk.config;
import com.baomidou.mybatisplus.core.config.GlobalConfig;
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
import com.yootk.handler.YootkMetaObjectHandler;
import org.apache.ibatis.annotations.Mapper;
import org.mybatis.spring.mapper.MapperScannerConfigurer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import javax.sql.DataSource;
import java.io.IOException;
@Configuration
public class MyBatisPlusConfig { // MyBaits-Plus项目配置类
@Bean
public MybatisSqlSessionFactoryBean sqlSessionFactoryBean(
DataSource dataSource,
GlobalConfig globalConfig) { // 注入全局配置实例
MybatisSqlSessionFactoryBean factoryBean = new MybatisSqlSessionFactoryBean();
factoryBean.setGlobalConfig(globalConfig); // 配置生效
factoryBean.setDataSource(dataSource);
factoryBean.setTypeAliasesPackage("com.yootk.vo"); // 类型扫描包
PathMatchingResourcePatternResolver resolver =
new PathMatchingResourcePatternResolver(); // 路径匹配资源
String mapperPath = PathMatchingResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX +
"mybatis/mapper/*.xml"; // 所有Mapper文件的路径批磅亿毫
try {
factoryBean.setMapperLocations(resolver.getResources(mapperPath));
} catch (IOException e) {
throw new RuntimeException(e);
}
return factoryBean;
}
@Bean
public MapperScannerConfigurer mapperScannerConfigurer() {
MapperScannerConfigurer configurer = new MapperScannerConfigurer();
configurer.setBasePackage("com.yootk.dao");
configurer.setAnnotationClass(Mapper.class);
return configurer;
}
@Bean
public GlobalConfig.DbConfig dbConfig() { // 全局数据配置
GlobalConfig.DbConfig dbConfig = new GlobalConfig.DbConfig();
// 如果此时你所有的逻辑删除字段是统一的名称,那么也可以直接在此定义
// dbConfig.setLogicDeleteField("status"); // 如果不统一,通过注解配置
dbConfig.setLogicDeleteValue("1"); // 逻辑删除的内容
dbConfig.setLogicNotDeleteValue("0"); // 逻辑未删除内容
return dbConfig;
}
@Bean
public GlobalConfig globalConfig(YootkMetaObjectHandler metaObjectHandler) {
GlobalConfig config = new GlobalConfig(); // 全局配置类
config.setMetaObjectHandler(metaObjectHandler); // 填充器
return config;
}
}
3、
@TableLogic // 逻辑删除标记字段
@TableField(fill = FieldFill.INSERT_UPDATE) // 增加和修改的时候都要填充
private Integer status;
4、
@Test
public void testAdd() { // 测试数据增加操作
Project project = new Project(); // 创建VO类对象
project.setName("李兴华Java就业编程训练营");
project.setCharge("李兴华");
project.setNote("系统的讲解Java程序员与Java架构师的完整技术图书,配套视频");
project.setStatus(5); // 逻辑删除
LOGGER.info("更新数据行数:{}、当前项目ID:{}",
this.projectDAO.insert(project), project.getPid());
}
5、
@Test
public void testAdd() { // 测试数据增加操作
Project project = new Project(); // 创建VO类对象
project.setName("李兴华Java就业编程训练营");
project.setCharge("李兴华");
project.setNote("系统的讲解Java程序员与Java架构师的完整技术图书,配套视频");
// project.setStatus(5); // 逻辑删除
LOGGER.info("更新数据行数:{}、当前项目ID:{}",
this.projectDAO.insert(project), project.getPid());
}
主键策略
分布式主键生成策略
- 为了提高项目并发吞吐量,现代的 Java 应用中往往会采用集群方式进行项目的部署。考虑到数据库数据的可维护性,所以在项目设计时,主键字段一般都不会采用自动增长列的方式实现,而是会基于特定的程序算法来实现主键的生成处理操作。
雪花算法位结构规范
- 为了防止分布式集群环境中不同的主机节点产生相同的主键,所以一般的做法是基于 Twttier 开源的雪花算法(Snow Flake),实现分布式 ID 生成处理。该算法的优点在于整体上按照时间进行自增排序,不同的应用根据当前所处的机房以及主机编号进行标识这样就避免了产生主键碰撞的问题,并且效率较高(Snow Flake 每秒能够产生 26 万个 ID)。雪花算法生成的 ID 是一个 long 类型的数值,其中每一位都有着明确的组成规范
- 最高位标识(占 1 位):由于 long 长整型在 Java 中是带有符号位的,所以为了描述出正数,那么最高位必须是 0;
- 毫秒级时间戳(占 41 位):保存时间戳差值(当前时间戳-开始时间戳),一般开始时间戳由程序来设置;
- 数据机器位(10 位):用于避免重复 ID 的产生,由于不同的主机会部署在不同的机房,所以此处会拆分为数据中心位(5 位)以及工作主机位(5 位),所以最多支持 32 个数据中心,每个数据中心最多支持 32 台主机;
- 序列号(12 位):毫秒内的计数,支持每个节点产生的 ID 序号(每毫秒产生 4096 个 ID 序号)。
dentifierGenerator 接
- MyBatis-Plus 在设计时,考虑到了主键生成策略的设计需要,所以提供了 ldentifierGenerator 接口。在使用时需要在 GlobalConfig 全局配置类中定义需要通过“@Tableld(typeIdentifierGenerator 接囗实例,在进行主键字段配置时,= IdTyPe.ASSIGN_ID)”注解进行 ID 的生成策略的指派,这样在每次数据增加时,就会自动的将 ldentifierGenerator 接口实现类返回主键数据填充到相应的字段上
1、
package com.yootk.util;
public class SnowFlakeUtils { // 雪花算法工具类
private final static long START_STAMP = 1487260800000L; // 2017-02-17 21:35:27.915
private final static long SEQUENCE_BIT = 12; // 序列号占用的位数
private final static long MACHINE_BIT = 5; // 机器标识占用的位数,256个机器
private final static long DATACENTER_BIT = 5; // 数据中心占用位数,256个数据
private final static long MAX_DATACENTER_NUM =
-1L ^ (-1L << DATACENTER_BIT); // 数据中心最大值(31)
private final static long MAX_MACHINE_NUM =
-1L ^ (-1L << MACHINE_BIT); // 机器标识最大值(31)
private final static long MAX_SEQUENCE =
-1L ^ (-1L << SEQUENCE_BIT); // 序列号最大值(4095)
private final static long MACHINE_LEFT = SEQUENCE_BIT; // 左位移
private final static long DATACENTER_LEFT = SEQUENCE_BIT + MACHINE_BIT; // 左位移
private final static long TIMESTMP_LEFT = DATACENTER_LEFT + DATACENTER_BIT; // 左位移
private long datacenterId; // 数据中心
private long machineId; // 机器标识
private long sequence = 0L; // 序列号
private long lastStamp = -1L; // 上次时间戳
/**
* 构建雪花算法生成器实例
* @param datacenterId 数据中心ID
* @param machineId // 机器ID
*/
public SnowFlakeUtils(long datacenterId, long machineId) {
if (datacenterId > MAX_DATACENTER_NUM || datacenterId < 0) {
throw new IllegalArgumentException("datacenterId can't be greater than " +
MAX_DATACENTER_NUM + " or less than 0");
}
if (machineId > MAX_MACHINE_NUM || machineId < 0) {
throw new IllegalArgumentException("machineId can't be greater than " +
MAX_MACHINE_NUM + " or less than 0");
}
this.datacenterId = datacenterId; // 属性初始化
this.machineId = machineId; // 属性初始化
}
public synchronized long nextId() { // 获取下一个ID
long currentStamp = getCurrentStamp(); // 获取当前时间戳
if (currentStamp < this.lastStamp) { // 时间戳判断
throw new RuntimeException("Clock moved backwards. Refusing to generate id");
}
if (currentStamp == lastStamp) { // 相同毫秒内,序列号自增
this.sequence = (this.sequence + 1) & MAX_SEQUENCE;
if (this.sequence == 0L) { // 同一毫秒的序列数已经达到最大
for (int i = 0; i < 100; i++) { // 循环获取多几次,尽量避免重复
currentStamp = getNextMillis(); // 获取下一个时间戳
if (currentStamp != this.lastStamp) { // 结束判断
break; // 退出循环
}
}
}
} else { // 不同毫秒内,序列号置为0
this.sequence = 0L;
}
this.lastStamp = currentStamp; // 保存当前时间戳
return (currentStamp - START_STAMP) << TIMESTMP_LEFT //时间戳部分
| datacenterId << DATACENTER_LEFT //数据中心部分
| machineId << MACHINE_LEFT //机器标识部分
| sequence; //序列号部分
}
private long getNextMillis() {
long mills = getCurrentStamp(); // 获取当前时间戳
while (mills <= this.lastStamp) { // 如果小于最后一次获取的时间戳
mills = getCurrentStamp(); // 重新获取时间戳
}
return mills; // 返回时间戳
}
private long getCurrentStamp() { // 获取当前时间戳
return System.currentTimeMillis();
}
public static long getId() { // 获取ID
SnowFlakeUtils idGenerator = new SnowFlakeUtils(1, 1);
return idGenerator.nextId();
}
}
2、
package com.yootk.test;
import com.yootk.util.SnowFlakeUtils;
public class TestSnowFlake {
public static void main(String[] args) {
System.out.println(SnowFlakeUtils.getId()); // 每次通过该方法获取ID
}
}
3、
package com.yootk.generator;
import com.baomidou.mybatisplus.core.incrementer.IdentifierGenerator;
import com.yootk.util.SnowFlakeUtils;
import org.springframework.stereotype.Component;
@Component
public class SnowFlakeIdGenerator implements IdentifierGenerator { // 雪花ID生成器
@Override
public Number nextId(Object entity) { // 获取ID数据
return SnowFlakeUtils.getId();
}
}
4、
@Bean
public GlobalConfig globalConfig(
YootkMetaObjectHandler metaObjectHandler, // 数据填充器
IdentifierGenerator generator) { // ID生成器
GlobalConfig config = new GlobalConfig(); // 全局配置类
config.setMetaObjectHandler(metaObjectHandler); // 填充器
config.setIdentifierGenerator(generator); // 配置主键生成器
return config;
}
5、
@TableId(type = IdType.ASSIGN_ID) // 自动增长ID
private Long pid;
6、
@Test
public void testAdd() { // 测试数据增加操作
Project project = new Project(); // 创建VO类对象
project.setName("李兴华Java就业编程训练营");
project.setCharge("李兴华");
project.setNote("系统的讲解Java程序员与Java架构师的完整技术图书,配套视频");
LOGGER.info("更新数据行数:{}、当前项目ID:{}",
this.projectDAO.insert(project), project.getPid());
}
SQL 注入器
SQL 命令生成类
- MyBatis-Plus 中所有的数据层接口只要继承了 BaseMapper 接口后,就可以自动实现数据的 CRUD 操作支持,而在数据层接口中的每一个默认方法,本质上都会存在有一个 SQL 命令的生成类
扩充 SQL 生成器
- 为了便于数据层方法生成命令的统一管理,MyBatis-Plus 提供了 AbstractMethod 抽象英,不同操作的子类只需要覆写该类中提供的“injectMappedStatement()”方法即可进行 SQL 命令的配置。所有定义的 SQL 命令生成类,需要绑定数据层接口的方法同时还需要在 SQL 注入管理器中进行注册,这样也就意味着除了 MyBatis-Plus 内置的数据层操作方法之外,开发者也可以扩充自己所需要的数据操作方法。
1、
package com.yootk.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import java.util.List;
public interface YootkBaseMapper<T> extends BaseMapper<T> { // 自定义接口
public List<T> findAll(); // 扩充的数据方法
}
2、
package com.yootk.dao;
import com.yootk.mapper.YootkBaseMapper;
import com.yootk.vo.Project;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface IProjectDAO extends YootkBaseMapper<Project> {
}
3、
package com.yootk.mapper.impl;
import com.baomidou.mybatisplus.core.injector.AbstractMethod;
import com.baomidou.mybatisplus.core.metadata.TableInfo;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlSource;
public class YootkFindAll extends AbstractMethod { // 创建方法的SQL生成器
private static final String METHOD_NAME = "findAll"; // 定义映射方法名称
public YootkFindAll() {
super(METHOD_NAME); // 传递方法名称
}
@Override
public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass,
TableInfo tableInfo) {
String sql = "SELECT * FROM " + tableInfo.getTableName(); // 拼凑SQL语句
SqlSource source = super.languageDriver.createSqlSource(configuration, sql, modelClass);
return this.addSelectMappedStatementForTable(mapperClass,
source, tableInfo);
}
}
4、
package com.yootk.inject;
import com.baomidou.mybatisplus.core.injector.AbstractMethod;
import com.baomidou.mybatisplus.core.injector.DefaultSqlInjector;
import com.baomidou.mybatisplus.core.metadata.TableInfo;
import com.yootk.mapper.impl.YootkFindAll;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
@Component
public class YootkSqlInjector extends DefaultSqlInjector {
@Override
public List<AbstractMethod> getMethodList(Class<?> mapperClass, TableInfo tableInfo) {
List<AbstractMethod> methods = new ArrayList<>();
methods.addAll(super.getMethodList(mapperClass, tableInfo)); // 保存已有的配置方法
methods.add(new YootkFindAll()); // 追加新的配置方法映射
return methods;
}
}
5、
@Bean
public GlobalConfig globalConfig(
YootkMetaObjectHandler metaObjectHandler, // 数据填充器
IdentifierGenerator generator, // ID生成器
ISqlInjector sqlInjector) { // SQL注入器
GlobalConfig config = new GlobalConfig(); // 全局配置类
config.setMetaObjectHandler(metaObjectHandler); // 填充器
config.setIdentifierGenerator(generator); // 配置主键生成器
config.setSqlInjector(sqlInjector); // 配置SQL注入器
return config;
}
6、
@Test
public void testFindAll() {
List<Project> projects = this.projectDAO.findAll(); // 扩充的数据方法
for (Project project : projects) {
LOGGER.info("【项目信息】项目ID:{}、项目名称:{}、负责人:{}、说明:{}、状态:{}",
project.getPid(), project.getName(), project.getCharge(),
project.getNote(), project.getStatus());
}
}
7、
【项目信息】项目ID:6、项目名称:李兴华Java就业编程训练营、负责人:小李老师、说明:Python编程训练营、状态:0
【项目信息】项目ID:7、项目名称:GoLang就业编程训练营、负责人:李兴华、说明:高并发应用设计、状态:1
【项目信息】项目ID:8、项目名称:李兴华Java就业编程训练营、负责人:李兴华、说明:系统的讲解Java程序员与Java架构师的完整技术图书,配套视频、状态:5
【项目信息】项目ID:9、项目名称:李兴华Java就业编程训练营、负责人:李兴华、说明:系统的讲解Java程序员与Java架构师的完整技术图书,配套视频、状态:null
【项目信息】项目ID:10、项目名称:李兴华Java就业编程训练营、负责人:李兴华、说明:系统的讲解Java程序员与Java架构师的完整技术图书,配套视频、状态:5
【项目信息】项目ID:11、项目名称:李兴华Java就业编程训练营、负责人:李兴华、说明:系统的讲解Java程序员与Java架构师的完整技术图书,配套视频、状态:0
【项目信息】项目ID:769136186848579584、项目名称:李兴华Java就业编程训练营、负责人:李兴华、说明:系统的讲解Java程序员与Java架构师的完整技术图书,配套视频、状态:0
MyBatis-Plus 插件
MyBatis-Plus 插件开发
- 在 MyBaits 开发框架中提供了拦截器的处理机制,可以利用“@Intercepts”以及“@Signature”两个注解定义拦截的具体类型以及拦截方法,这样在每次进行数据操作前都可以由开发者进行一些自定义的功能实现。在 MyBatis-Plus 插件中提供了 MybatisPlusInterceptor 内置拦截器,该拦截器可以在数据更新与查询操作前进行处理,随后基于 InnerInterceptor 内置接口实现 MyBatis-Plus 插件配置
1、
package com.yootk.interceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.sql.SQLException;
public class YootkInnerInterceptor implements InnerInterceptor { // MP拦截器
private static final Logger LOGGER =
LoggerFactory.getLogger(YootkInnerInterceptor.class); // 获取日志对象
@Override
public boolean willDoQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
LOGGER.info("【willDoQuery()方法执行】绑定SQL:{}", boundSql.getSql());
return false; // 不再执行后续查询
}
@Override
public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
LOGGER.info("【beforeQuery()方法执行】绑定SQL:{}", boundSql.getSql());
}
}
2、
package com.yootk.config;
import com.baomidou.mybatisplus.core.config.GlobalConfig;
import com.baomidou.mybatisplus.core.incrementer.IdentifierGenerator;
import com.baomidou.mybatisplus.core.injector.ISqlInjector;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
import com.yootk.handler.YootkMetaObjectHandler;
import com.yootk.interceptor.YootkInnerInterceptor;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.plugin.Interceptor;
import org.mybatis.spring.mapper.MapperScannerConfigurer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import javax.sql.DataSource;
import java.io.IOException;
@Configuration
public class MyBatisPlusConfig { // MyBaits-Plus项目配置类
@Bean
public MybatisSqlSessionFactoryBean sqlSessionFactoryBean(
DataSource dataSource,
GlobalConfig globalConfig,
MybatisPlusInterceptor mybatisPlusInterceptor) { // 注入全局配置实例
MybatisSqlSessionFactoryBean factoryBean = new MybatisSqlSessionFactoryBean();
factoryBean.setGlobalConfig(globalConfig); // 配置生效
factoryBean.setDataSource(dataSource);
factoryBean.setPlugins(new Interceptor[]{mybatisPlusInterceptor}); // 注入拦截器
factoryBean.setTypeAliasesPackage("com.yootk.vo"); // 类型扫描包
PathMatchingResourcePatternResolver resolver =
new PathMatchingResourcePatternResolver(); // 路径匹配资源
String mapperPath = PathMatchingResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX +
"mybatis/mapper/*.xml"; // 所有Mapper文件的路径批磅亿毫
try {
factoryBean.setMapperLocations(resolver.getResources(mapperPath));
} catch (IOException e) {
throw new RuntimeException(e);
}
return factoryBean;
}
@Bean
public MapperScannerConfigurer mapperScannerConfigurer() {
MapperScannerConfigurer configurer = new MapperScannerConfigurer();
configurer.setBasePackage("com.yootk.dao");
configurer.setAnnotationClass(Mapper.class);
return configurer;
}
@Bean
public GlobalConfig.DbConfig dbConfig() { // 全局数据配置
GlobalConfig.DbConfig dbConfig = new GlobalConfig.DbConfig();
// 如果此时你所有的逻辑删除字段是统一的名称,那么也可以直接在此定义
// dbConfig.setLogicDeleteField("status"); // 如果不统一,通过注解配置
dbConfig.setLogicDeleteValue("1"); // 逻辑删除的内容
dbConfig.setLogicNotDeleteValue("0"); // 逻辑未删除内容
return dbConfig;
}
@Bean
public GlobalConfig globalConfig(
YootkMetaObjectHandler metaObjectHandler, // 数据填充器
IdentifierGenerator generator, // ID生成器
ISqlInjector sqlInjector) { // SQL注入器
GlobalConfig config = new GlobalConfig(); // 全局配置类
config.setMetaObjectHandler(metaObjectHandler); // 填充器
config.setIdentifierGenerator(generator); // 配置主键生成器
config.setSqlInjector(sqlInjector); // 配置SQL注入器
return config;
}
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() { // MP拦截器
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new YootkInnerInterceptor()); // 自定义MP拦截器
return interceptor;
}
}
3、
@Test
public void testSelectId() {
Project project = this.projectDAO.selectById(1L); // 根据ID查询数据
LOGGER.info("【项目信息】项目ID:{}、项目名称:{}、负责人:{}、说明:{}、状态:{}",
project.getPid(), project.getName(), project.getCharge(),
project.getNote(), project.getStatus());
}
分页插件
MyBatis-Plus 数据分页插件
- 为避免数据表数据加载过多,在项目开发中往往都要提供数据的分页加载支持,在每次分页时,除了要进行所需数据的加载之外,还需要进行数据行数的统计。传统的做法是在数据层中定义两个数据查询指令,以分别获取不同的数据,而 MyBatis-Plus 为了简化这一操作,提供了专属的分页拦截器
MyBatis-Plus 整合分页组件
- 由于 MyBatis-Plus 在设计时,充分考虑到了不同数据库的查询分页支持,提供了 PaginationInnerlnterceptor 拦截器处理类,可以在该类中通过 DbType 枚举类,定义当前所使用的数据库类型,这样就可以在数据查询时,自动生成与当前数据库匹配的分页 SQL 命令。PaginationInnerlnterceptor 类实现了 InnerInterceptor 父接口,该接为 MyBatis-Plus 提供的扩展接口,所以最终要想与 MyBatis 拦截器整合在一起,则需要通过 MyBatisPlusInterceptor 类进行包装,最后还需要将定义的拦截器与 MybatisSalSessionFactoryBean 实例整合在一起后才可以生效
1、
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() { // MP拦截器
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
PaginationInnerInterceptor page = new PaginationInnerInterceptor(DbType.MYSQL);
// 如果数据返回量太高,首先不是正常的操作,其次内存会被严重占用,会导致OOM问题的
page.setMaxLimit(500L); // 最多返回500数据
interceptor.addInnerInterceptor(page); // 追加拦截器
return interceptor;
}
2、
package com.yootk.test;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.yootk.StartMyBatisPlusApplication;
import com.yootk.dao.IProjectDAO;
import com.yootk.vo.Project;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@ContextConfiguration(classes = StartMyBatisPlusApplication.class)
@ExtendWith(SpringExtension.class)
public class TestProjectSplitPage {
private static final Logger LOGGER = LoggerFactory.getLogger(TestProjectSplitPage.class);
@Autowired
private IProjectDAO projectDAO;
@Test
public void testSplitPage() {
int currentPage = 1; // 当前所在页
int lineSize = 5; // 每页查询数量
String keyword = ""; // 模糊查询关键字
QueryWrapper<Project> wrapper = new QueryWrapper<>(); // 查询包装期
wrapper.like("name", keyword); // 关键字查询
// 在MP里面使用了 IPage进行分页数据的定义
IPage<Project> page = new Page<>(currentPage, lineSize, true);
IPage<Project> result = this.projectDAO.selectPage(page, wrapper); // 数据查询
LOGGER.info("【分页统计】总页数:{}", result.getPages());
LOGGER.info("【分页统计】总记录数:{}", result.getTotal());
for (Project project : result.getRecords()) { // 获取全部返回结果
LOGGER.info("【项目信息】项目ID:{}、项目名称:{}、负责人:{}、说明:{}、状态:{}",
project.getPid(), project.getName(), project.getCharge(),
project.getNote(), project.getStatus());
}
}
}
乐观锁插件
乐观锁插件
- 为了保证在并发下数据更新的有效性,常规的做法是采用数据锁的方式保护指定的数据,但是传统的悲观锁并不适合于高并发的应用场景。为了得到良好的锁处理性能,在开发中推荐的做法是乐观锁,采用版本号方式进行更新控制
1、
ALTER TABLE project ADD version INT DEFAULT 1;
2、
@Version // 乐观锁版本标记
private Integer version;
public void setVersion(Integer version) {
this.version = version;
}
public Integer getVersion() {
return version;
}
3、
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() { // MP拦截器
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
PaginationInnerInterceptor page = new PaginationInnerInterceptor(DbType.MYSQL);
// 如果数据返回量太高,首先不是正常的操作,其次内存会被严重占用,会导致OOM问题的
page.setMaxLimit(500L); // 最多返回500数据
interceptor.addInnerInterceptor(page); // 追加拦截器
OptimisticLockerInnerInterceptor lock = new OptimisticLockerInnerInterceptor(); // 乐观锁
interceptor.addInnerInterceptor(lock); // 配置乐观锁
return interceptor;
}
4、
@Test
public void testEditOptimistic() { // 乐观锁更新
Project project = this.projectDAO.selectById(6L); // 加载数据
project.setName("Node.JS就业编程训练营");
LOGGER.info("更新数据行数:{}", this.projectDAO.updateById(project));
}
5、
package com.yootk.test;
import com.yootk.StartMyBatisPlusApplication;
import com.yootk.dao.IProjectDAO;
import com.yootk.vo.Project;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
@ContextConfiguration(classes = StartMyBatisPlusApplication.class)
@ExtendWith(SpringExtension.class)
public class TestProjectOptimisticLock {
private static final Logger LOGGER = LoggerFactory.getLogger(TestProjectOptimisticLock.class);
@Autowired
private IProjectDAO projectDAO;
private Long pid = 6L; // 要更新的数据编号
@Test
public void testThreadEdit() throws Exception {
startFirstThread(); // 开启第一个线程
startSecondThread(); // 开启第二个线程
TimeUnit.SECONDS.sleep(10); // 等待一下
}
public void startFirstThread() { // 第一个线程操作
new Thread(()->{
Project project = projectDAO.selectById(pid); // 进行数据查询
project.setName("【" + Thread.currentThread().getName() + "】Java课程");
try {
TimeUnit.SECONDS.sleep(5); // 第二个线程的版本号一定会改变
} catch (InterruptedException e) {}
LOGGER.info("【{}】数据更新:", Thread.currentThread().getName(),
projectDAO.updateById(project));
}, "第一个更新线程").start();
}
public void startSecondThread() { // 第一个线程操作
new Thread(()->{
Project project = projectDAO.selectById(pid); // 进行数据查询
project.setName("【" + Thread.currentThread().getName() + "】SSM课程");
LOGGER.info("【{}】数据更新:", Thread.currentThread().getName(),
projectDAO.updateById(project));
}, "第二个更新线程").start();
}
}
防全表更新与删除插件
更新防护插件
- 在传统的项目开发中,此类操作应在开发人员编写业务时有意识的进行回避,但是由于一个项目团队由于会存在有技术和业务理解上的差异,所以为了防止此类问题的出现 MyBatis-Plus 提供了一个全表更新以及全表删除的防护插件。该插件的核心逻辑在于如果发现当前执行的是 UPDATE 或 DELETE 语句,并且没有设置 WHERE 子句时就会阻止当前的更新操作,并抛出异常,下面来观察一下该插件的具体使用。
1、
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() { // MP拦截器
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
PaginationInnerInterceptor page = new PaginationInnerInterceptor(DbType.MYSQL);
// 如果数据返回量太高,首先不是正常的操作,其次内存会被严重占用,会导致OOM问题的
page.setMaxLimit(500L); // 最多返回500数据
interceptor.addInnerInterceptor(page); // 追加拦截器
OptimisticLockerInnerInterceptor lock = new OptimisticLockerInnerInterceptor(); // 乐观锁
interceptor.addInnerInterceptor(lock); // 配置乐观锁
BlockAttackInnerInterceptor block = new BlockAttackInnerInterceptor(); // 防护插件
interceptor.addInnerInterceptor(block);
return interceptor;
}
2、
package com.yootk.dao;
import com.yootk.mapper.YootkBaseMapper;
import com.yootk.vo.Project;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface IProjectDAO extends YootkBaseMapper<Project> {
public long doDeleteAll(); // 【导火索】删除全部
public long doUpdateAll(); // 【导火索】全部更新
}
3、
<?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">
<!-- 设置命名空间,可以与不同表的同类型操作进行区分,使用时以“空间名称.id”的方式调用 -->
<mapper namespace="com.yootk.dao.IProjectDAO"> <!-- 与数据层接口名称一致 -->
<!-- 如果太复杂的查询,不要通过方法处理了,直接在此处按照传统的MyBatis方式开发 -->
<delete id="doDeleteAll">
DELETE FROM project;
</delete>
<update id="doUpdateAll">
UPDATE project SET charge='小李老师'
</update>
</mapper>
4、
package com.yootk.test;
import com.yootk.StartMyBatisPlusApplication;
import com.yootk.dao.IProjectDAO;
import com.yootk.vo.Project;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@ContextConfiguration(classes = StartMyBatisPlusApplication.class)
@ExtendWith(SpringExtension.class)
public class TestBlock {
private static final Logger LOGGER = LoggerFactory.getLogger(TestBlock.class);
@Autowired
private IProjectDAO projectDAO;
@Test
public void testDeleteAll() {
LOGGER.info("删除全部数据:{}", this.projectDAO.doDeleteAll());
}
@Test
public void testUpdateAll() {
LOGGER.info("更新全部数据:{}", this.projectDAO.doUpdateAll());
}
}
动态表名插件
表名称动态切换
- 在一些项目开发中,有些时候为了进行数据表的标记,往往会在表名称前进行一些前缀或后缀的标记,例如:当前描述项目的表名称为 project,但是在数据库定义时,往往会将其定义为“muyan_yootk_proiect”或者“project_dev”并且有可能还会存在有数据源切换的问题,当程序切换到 A 数据源的时候,数据表名称为"muyan_project',而当程序切换到 B 数据源的时候,数据表名称为"yootk_project"
1、
package com.yootk.table;
import com.baomidou.mybatisplus.extension.plugins.handler.TableNameHandler;
public class YootkTableNameHandler implements TableNameHandler { // 表名称处理
@Override
public String dynamicTableName(String sql, String tableName) { // 获取最终执行的表名称
// 这里面可以编写很多的逻辑,也可以直接读取各种配置文件
return "muyan_yootk_" + tableName; // 固定了一个前缀
}
}
2、
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() { // MP拦截器
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
PaginationInnerInterceptor page = new PaginationInnerInterceptor(DbType.MYSQL);
// 如果数据返回量太高,首先不是正常的操作,其次内存会被严重占用,会导致OOM问题的
page.setMaxLimit(500L); // 最多返回500数据
interceptor.addInnerInterceptor(page); // 追加拦截器
OptimisticLockerInnerInterceptor lock = new OptimisticLockerInnerInterceptor(); // 乐观锁
interceptor.addInnerInterceptor(lock); // 配置乐观锁
BlockAttackInnerInterceptor block = new BlockAttackInnerInterceptor(); // 防护插件
interceptor.addInnerInterceptor(block);
DynamicTableNameInnerInterceptor dynaTable =
new DynamicTableNameInnerInterceptor(); // 动态表名称拦截器
dynaTable.setTableNameHandler(new YootkTableNameHandler()); // 添加转换器
interceptor.addInnerInterceptor(dynaTable); // 追加拦截器
return interceptor;
}
3、
@Test
public void testSelectId() {
Project project = this.projectDAO.selectById(6L); // 根据ID查询数据
LOGGER.info("【项目信息】项目ID:{}、项目名称:{}、负责人:{}、说明:{}、状态:{}",
project.getPid(), project.getName(), project.getCharge(),
project.getNote(), project.getStatus());
}
多租户插件
租户模式
- 随着当前 SaaS(Software-as-a-Service,软件即服务)技术的不断发展,越来越多的企业采用了云办公平台的入驻模式,这样在平台进行数据库设计时,就需要对数据进行有效的维护,例如:现在的项目表要转为 SaaS 运行机制,那么就应该在 project 表中追加一个“tenant id”的字段,该字段描述的是租户 ID,在进行数据操作时,就可以利用该字段区分不同租户的数据
多租户模式实现
- 为了便于租户模式的实现,MyBatis-Plus 提供了 TenantLinelnnerlnterceptor 插件并日提供了 TenantLineHandler 租户处理接口,在该接口中开发者可以设置不同租户的信息以及与之匹配的数据字段
1、
ALTER TABLE project ADD tenant_id VARCHAR(50) DEFAULT 'muyan';
2、
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() { // MP拦截器
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
PaginationInnerInterceptor page = new PaginationInnerInterceptor(DbType.MYSQL);
// 如果数据返回量太高,首先不是正常的操作,其次内存会被严重占用,会导致OOM问题的
page.setMaxLimit(500L); // 最多返回500数据
interceptor.addInnerInterceptor(page); // 追加拦截器
OptimisticLockerInnerInterceptor lock = new OptimisticLockerInnerInterceptor(); // 乐观锁
interceptor.addInnerInterceptor(lock); // 配置乐观锁
BlockAttackInnerInterceptor block = new BlockAttackInnerInterceptor(); // 防护插件
interceptor.addInnerInterceptor(block);
TenantLineInnerInterceptor tenant = new TenantLineInnerInterceptor(); // 多租户插件
tenant.setTenantLineHandler(new TenantLineHandler() { // 进行插件逻辑的定义
@Override
public Expression getTenantId() {
// 此时可以追加一些标记的获取操作逻辑,但是本次采用固定结构了
return new StringValue("muyan"); // 配置租户的ID
}
});
interceptor.addInnerInterceptor(tenant);
return interceptor;
}
3、
@Test
public void testAdd() { // 测试数据增加操作
Project project = new Project(); // 创建VO类对象
project.setName("李兴华Java就业编程训练营");
project.setCharge("李兴华");
project.setNote("系统的讲解Java程序员与Java架构师的完整技术图书,配套视频");
project.setStatus(0); // 0表示未删除
LOGGER.info("更新数据行数:{}、当前项目ID:{}",
this.projectDAO.insert(project), project.getPid());
}
4、
@Test
public void testSelectId() {
Project project = this.projectDAO.selectById(769201945431904256L); // 根据ID查询数据
LOGGER.info("【项目信息】项目ID:{}、项目名称:{}、负责人:{}、说明:{}、状态:{}",
project.getPid(), project.getName(), project.getCharge(),
project.getNote(), project.getStatus());
}
SQL 性能规范插件
SQL 性能规范插件
- 使用 MyBatis 进行应用开发最大的好处在于,所有要执行的 SQL 都可以由开发者自行定但是不同的开发者由于技术水平存在有落差,所以很难保证所编写出来的 SQL 语仓一定是高性能的,例如,在进行数据查询时或更新操作时没有编写 WHERE、在根据某个字段查询时,该字段没有使用到索引等。为了解决这类问题,MyBatis-Plus 提供了一个 SQL 性能规范的处理插件,如果发现当前用户执行的 SQL 语句存在有问题,则会直接抛出异常,下面来看下该插件的具体使用。
1、
public List<Project> findAllByCharge(String charge); // charge字段上没有索引
2、
<select id="findAllByCharge" resultType="Project" parameterType="string">
SELECT pid, name, charge, note, status, version FROM project WHERE charge=#{charge}
</select>
3、
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() { // MP拦截器
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
PaginationInnerInterceptor page = new PaginationInnerInterceptor(DbType.MYSQL);
// 如果数据返回量太高,首先不是正常的操作,其次内存会被严重占用,会导致OOM问题的
page.setMaxLimit(500L); // 最多返回500数据
interceptor.addInnerInterceptor(page); // 追加拦截器
OptimisticLockerInnerInterceptor lock = new OptimisticLockerInnerInterceptor(); // 乐观锁
interceptor.addInnerInterceptor(lock); // 配置乐观锁
TenantLineInnerInterceptor tenant = new TenantLineInnerInterceptor(); // 多租户插件
tenant.setTenantLineHandler(new TenantLineHandler() { // 进行插件逻辑的定义
@Override
public Expression getTenantId() {
// 此时可以追加一些标记的获取操作逻辑,但是本次采用固定结构了
return new StringValue("muyan"); // 配置租户的ID
}
});
interceptor.addInnerInterceptor(tenant);
IllegalSQLInnerInterceptor illegal = new IllegalSQLInnerInterceptor(); // 非法SQL
interceptor.addInnerInterceptor(illegal);
return interceptor;
}
4、
@Test
public void testUpdateAll() {
LOGGER.info("更新全部数据:{}", this.projectDAO.doUpdateAll());
}
5、
CREATE INDEX project_bitmap ON project(tenant_id);
6、
@Test
public void testFindAllByCharge() {
List<Project> projects = this.projectDAO.findAllByCharge("李兴华"); // 扩充的数据方法
for (Project project : projects) {
LOGGER.info("【项目信息】项目ID:{}、项目名称:{}、负责人:{}、说明:{}、状态:{}",
project.getPid(), project.getName(), project.getCharge(),
project.getNote(), project.getStatus());
}
}
7、
CREATE INDEX project_charge ON project(charge);
数据安全保护
采用明文方式配置
- 商业应用项目的核心业务流程的展开,大多都是基于数据库实现的,在当前的应用开发中,为了便于项目的开发以及数据库配置项的维护,往往会将所有的 JDBC 信息都定义在 database.properties 配置文件之中,并且基于明文的方式部署在了应用服务器上那么就有可能会因为操作系统的漏洞,而造成该配置文件内容的泄露,从而导致数据库出现安全问题
AES 加密存储
- 考虑到项目应用的安全性,所以对于一些核心的配置文件中的核心配置项就需要进行加密处理,即便由于各种漏洞,导致配置文件丢失,但是由于加密算法的密钥为应用程序所有,这样也能够保证核心数据不会轻易丢失
1、
package com.yootk.util;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;
public class AESUtil { // AES加密工具
public static final String KEY = "www.JIXIANIT.com"; // 密钥
private static final String CHARSET = "UTF-8"; // 编码
private static int OFFSET = 16; // 偏移量
private static String TRANSFORMATION = "AES/CBC/PKCS5Padding"; // AES加密模式
private static String ALGORITHM = "AES"; // 加密算法
public static String encrypt(String content) { // 数据加密
return encrypt(content, KEY); // 默认KEY加密
}
public static String decrypt(String content) { // 数据解密
return decrypt(content, KEY); // 默认KEY解密
}
public static String encrypt(String content, String key) { // 数据加密
try {
SecretKeySpec skey = new SecretKeySpec(key.getBytes(), ALGORITHM); // 密钥KEY
IvParameterSpec iv = new IvParameterSpec(
key.getBytes(), 0, OFFSET); // 初始化向量
Cipher cipher = Cipher.getInstance(TRANSFORMATION); // 加密模式
byte[] byteContent = content.getBytes(CHARSET); // 获取字节数组
cipher.init(Cipher.ENCRYPT_MODE, skey, iv); // 初始化加密模式
byte[] result = cipher.doFinal(byteContent); // AES加密
return Base64.getEncoder().encodeToString(result); // Base64加密
} catch (Exception e) {
return null;
}
}
public static String decrypt(String content, String key) { // 数据解密
try {
SecretKeySpec skey = new SecretKeySpec(key.getBytes(), ALGORITHM); // 加密KEY
IvParameterSpec iv = new IvParameterSpec(
key.getBytes(), 0, OFFSET); // 初始化向量
Cipher cipher = Cipher.getInstance(TRANSFORMATION); // 加密模式
cipher.init(Cipher.DECRYPT_MODE, skey, iv); // 初始化解密模式
byte [] data = Base64.getDecoder().decode(content); // Base64解密
byte[] result = cipher.doFinal(data); // AES解密
return new String(result); // 解密
} catch (Exception e) {}
return null;
}
}
2、
package com.yootk.test;
import com.yootk.util.AESUtil;
public class TestAES { // 数据的加密操作
public static void main(String[] args) {
String jdbcUrl = "jdbc:mysql://localhost:3306/yootk";
String username = "root";
String password = "mysqladmin";
System.out.println("JDBC连接地址:" + AESUtil.encrypt(jdbcUrl));
System.out.println("数据库用户名:" + AESUtil.encrypt(username));
System.out.println("数据库密码:" + AESUtil.encrypt(password));
}
}
3、
yootk.database.driverClassName=com.mysql.cj.jdbc.Driver
yootk.database.jdbcUrl=rT7odPPV+bL9HMpaC1fJ3gr3mXkE7+BMIF+j+WPY8jvjnqvILSmSkDYxVdh4JEH9
yootk.database.username=qQuA179BGazDbdMZ/jB4zQ==
yootk.database.password=l/QJC3VvJIF8q/iFbqamkg==
yootk.database.connectionTimeOut=3000
yootk.database.readOnly=false
yootk.database.pool.idleTimeOut=3000
yootk.database.pool.maxLifetime=60000
yootk.database.pool.maximumPoolSize=60
yootk.database.pool.minimumIdle=20
4.
@Bean("dataSource") // Bean注册
public DataSource dataSource() { // 配置数据源
HikariDataSource dataSource = new HikariDataSource(); // DataSource子类实例化
dataSource.setDriverClassName(this.driverClassName); // 驱动程序
dataSource.setJdbcUrl(AESUtil.decrypt(this.jdbcUrl)); // JDBC连接地址
dataSource.setUsername(AESUtil.decrypt(this.username)); // 用户名
dataSource.setPassword(AESUtil.decrypt(this.password)); // 密码
dataSource.setConnectionTimeout(this.connectionTimeout); // 连接超时
dataSource.setReadOnly(this.readOnly); // 是否为只读数据库
dataSource.setIdleTimeout(this.idleTimeout); // 最小维持时间
dataSource.setMaxLifetime(this.maxLifetime); // 连接的最大时长
dataSource.setMaximumPoolSize(this.maximumPoolSize); // 连接池最大容量
dataSource.setMinimumIdle(this.minimumIdle); // 最小维持连接量
return dataSource; // 返回Bean实例
}
ActiveRecord
AR 模式实现
- 在常见的 ORM 开发框架中,为了便于数据操作,一般都会提供有一个专属的数据操作类(例如:MyBatis 中提供的 SqlSession),而后为了便于业务的开发,开发者会将这个操作类的方法调用包装在数据层实现类之中,所以最终往往是由数据层基于实体类的方式实现最终的数据处理操作。
- 在 MyBatis-Plus 之中,由于需要提供实体类与数据表之间的映射配置,所以要在实体类中添加大量的注解,所以为了进一步发挥实体类的作用,提供了一种 AR(Active Record,活动记录)模式,该模式是一种领域模型,直接基于实体类的方式即可实现数据操作
1、
public class Project extends Model<Project> {}
2、
@Test
public void testAdd() { // 测试数据增加操作
Project project = new Project(); // 创建VO类对象
project.setName("李兴华Java就业编程训练营");
project.setCharge("李兴华");
project.setNote("系统的讲解Java程序员与Java架构师的完整技术图书,配套视频");
project.setStatus(0); // 0表示未删除
LOGGER.info("更新数据行数:{}、当前项目ID:{}", project.insert(), project.getPid());
}
3、
@Test
public void testSelectId() {
Project project = new Project(); // 需要实例化Project操作
project.setPid(769201945431904256L); // 设置了查询条件
Project result = project.selectById(); // 数据查询
LOGGER.info("【项目信息】项目ID:{}、项目名称:{}、负责人:{}、说明:{}、状态:{}",
result.getPid(), result.getName(), result.getCharge(),
result.getNote(), result.getStatus());
}
通用枚举
枚举操作
- 常规的项目开发中,实体类中所使用的数据类型一般都要与数据列类型有所对应,但是考虑到项目管理方面的需要,对于一些有限的数据项,可以基于枚举的方式进行定义例如:现在假设在一个项目之中的负责人只有两个,那么这时就可以考虑定义一个 ChargeEnum 的枚举类,而 Project 类中的 charge 属性也需要将 char 属性类型定义为 ChargeEnum
1、
package com.yootk.type;
import com.baomidou.mybatisplus.annotation.EnumValue;
public enum ChargeEnum {
LEE("李兴华", "编程技术讲师"), YOOTK("沐言优拓", "编程训练营");
@EnumValue // 枚举值
private String name; // 这项是核心项
private String desc; // 描述信息
private ChargeEnum(String name, String desc) {
this.name = name;
this.desc = desc;
}
}
2、
private ChargeEnum charge; // 使用枚举描述负责人
public ChargeEnum getCharge() {
return charge;
}
public void setCharge(ChargeEnum charge) {
this.charge = charge;
}
3、
@Test
public void testAdd() { // 测试数据增加操作
Project project = new Project(); // 创建VO类对象
project.setName("李兴华Java就业编程训练营");
project.setCharge(ChargeEnum.LEE); // 使用枚举
project.setNote("系统的讲解Java程序员与Java架构师的完整技术图书,配套视频");
project.setStatus(0); // 0表示未删除
LOGGER.info("更新数据行数:{}、当前项目ID:{}", project.insert(), project.getPid());
}
IService
业务接口抽象
- 为了可以实现业务层的简化,在定义具体业务接口时需要多继承-个 IService 父接口同时在 lService 接口中本身也定义有大量的抽象方法,而这些抽象方法的实现主要依靠的是 Servicelmpl 子类实现的
1、
package com.yootk.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.yootk.vo.Project;
public interface IProjectService extends IService<Project> {
}
2、
package com.yootk.service;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.yootk.dao.IProjectDAO;
import com.yootk.vo.Project;
import org.springframework.stereotype.Service;
@Service
public class ProjectServiceImpl extends ServiceImpl<IProjectDAO, Project> implements IProjectService {
}
3、
@Autowired
private IProjectService projectService; // 业务接口实例
@Test
public void testAdd() { // 测试数据增加操作
List<Project> projects = new ArrayList<>();
for (int x = 0; x < 10; x ++) {
Project project = new Project(); // 创建VO类对象
project.setName("李兴华Java就业编程训练营");
project.setCharge(ChargeEnum.LEE); // 使用枚举
project.setNote("系统的讲解Java程序员与Java架构师的完整技术图书,配套视频");
project.setStatus(0); // 0表示未删除
projects.add(project); // 追加到集合之中
}
LOGGER.info("批量增加:{}", this.projectService.saveOrUpdateBatch(projects));
}
4、
@Test
public void testSelectId() {
Project result = this.projectService.getById(769201945431904256L);
LOGGER.info("【项目信息】项目ID:{}、项目名称:{}、负责人:{}、说明:{}、状态:{}",
result.getPid(), result.getName(), result.getCharge(),
result.getNote(), result.getStatus());
}
5、
@Test
public void testDelete() {
LOGGER.info("数据删除:{}", this.projectService.removeById(769201945431904256L));
}
MyBatis-Plus 逆向工程
MyBatis-Plus 逆向工程
- 合理的 MVC 设计模式之中,所有的业务处理操作都需要经由控制层执行,虽然 MyBatis-Plus 提供 BaseMapper 简化了数据层开发,又提供了 IService 接口简化了业务层开发但是这样的支持还远远不足。因为一个完善的 SSM 应用中需要考虑到每一张实体表的基础 CRUD 处理,因此在 MvBatis-Plus 中直接提供了逆向工程的支持,即:可以根据指定数据库中实体表的结构自动生成所需的程序源代码
// https://mvnrepository.com/artifact/com.baomidou/mybatis-plus-generator
implementation group: 'com.baomidou', name: 'mybatis-plus-generator', version: '3.5.5'
// https://mvnrepository.com/artifact/org.apache.velocity/velocity-engine-core
implementation group: 'org.apache.velocity', name: 'velocity-engine-core', version: '2.3'
project(":mybatis-plus") {
dependencies {
implementation(libraries.'mysql-connector-java')
implementation(libraries.'mybatis-plus')
implementation(libraries.'spring-orm')
implementation(libraries.'mybatis-spring')
implementation(libraries.'HikariCP')
implementation(libraries.'mybatis-plus-generator')
implementation(libraries.'velocity-engine-core')
}
}
package com.yix.test;
import com.baomidou.mybatisplus.generator.FastAutoGenerator;
import com.baomidou.mybatisplus.generator.config.OutputFile;
import java.util.Collections;
/**
* @author wangdx
*/
public class CreateEngine {
public static final String URL = "jdbc:mysql://192.168.16.8:3306/yootk";
public static final String USERNAME = "root";
public static final String PASSWORD = "root";
public static void main(String[] args) {
FastAutoGenerator.create(URL, USERNAME, PASSWORD)
.globalConfig(builder -> {
builder.author("王大祥")
.enableSwagger()
.outputDir("D:\\develop\\project\\images\\mybatisplus");
})
.packageConfig(builder -> {
builder.parent("com.yix")
.moduleName("ssm")
.pathInfo(Collections.singletonMap(OutputFile.xml, "D:\\develop\\project\\images\\mybatisplus\\mpr"));
})
.strategyConfig(builder -> {
builder.addInclude("project")
.addTablePrefix("");
})
.execute();
}
}
demo