MybatisPlus为mybatis的升级版,可以节省大量的工作时间,所有的CRUD代码都可以自动化完成,类似的还有JPA,tk-mapper
官网:https://mp.baomidou.com/
指南文档:https://mp.baomidou.com/guide/#%E7%89%B9%E6%80%A7
借用官网的话来说:MyBatis-Plus (opens new window)(简称 MP)是一个 MyBatis (opens new window)的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生
无侵入:只做增强不做改变,引入它不会对现有工程产生影响,如丝般顺滑
损耗小:启动即会自动注入基本 CURD,性能基本无损耗,直接面向对象操作
强大的 CRUD 操作:内置通用 Mapper、通用 Service,仅仅通过少量配置即可实现单表大部分 CRUD 操作,更有强大的条件构造器,满足各类使用需求
支持 Lambda 形式调用:通过 Lambda 表达式,方便的编写各类查询条件,无需再担心字段写错
支持主键自动生成:支持多达 4 种主键策略(内含分布式唯一 ID 生成器 - Sequence),可自由配置,完美解决主键问题
支持 ActiveRecord 模式:支持 ActiveRecord 形式调用,实体类只需继承 Model 类即可进行强大的 CRUD 操作
支持自定义全局通用操作:支持全局通用方法注入( Write once, use anywhere )
内置代码生成器:采用代码或者 Maven 插件可快速生成 Mapper 、 Model 、 Service 、 Controller 层代码,支持模板引擎,更有超多自定义配置等您来使用
内置分页插件:基于 MyBatis 物理分页,开发者无需关心具体操作,配置好插件之后,写分页等同于普通 List 查询
分页插件支持多种数据库:支持 MySQL、MariaDB、Oracle、DB2、H2、HSQL、SQLite、Postgre、SQLServer 等多种数据库
内置性能分析插件:可输出 Sql 语句以及其执行时间,建议开发测试时启用该功能,能快速揪出慢查询(现在没了 QAQ)
内置全局拦截插件:提供全表 delete 、 update 操作智能分析阻断,也可自定义拦截规则,预防误操作
首先利用官网上直接给的代码建一张表,这里就直接放出成品图了:
接下来创建一个springboot项目,导入相关依赖:
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
dependency>
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-boot-starterartifactId>
<version>3.4.3version>
dependency>
说明:不要同时导入mybatis和mybatisplus
配置连接数据库文件:
#Mysql 8版本以及部分5版本需要配置时区 同时8版本的驱动带cj字符
spring:
datasource:
username: root
password: 981216
url: jdbc:mysql://localhost:3306/mybatisplus?serverTimezone=GMT&useUnicode=true&characterEncoding=utf-8
driver-class-name: com.mysql.cj.jdbc.Driver
老方法pojo-dao(连接mybatis,配置mapper.xml文件)-service-controller一条龙
但是现在pojo不变,mapper层只需要配置接口,然后让接口继承BaseMapper:
//在对应的Mapper上面实现基本的接口 BaseMapper
@Repository //dao层注入spring
public interface UserMapper extends BaseMapper<User> {
//继承BaseMapper接口之后crud操作就不用写了
}
(使用@Repository后最好在主启动类上添加一个@MapperScan(“com.xige.Mapper”),好让springboot扫描)
继承BaseMapper之后,就会继承它的很多集成方法,然后调用的时候只需要直接点就可以点出来,当然,如果有部分复杂sql还是需要人手动布置逻辑然后调用
如下为springboot测试代码:
@SpringBootTest
class MybatisplusApplicationTests {
//继承了BaseMapper,所有的方法都来自父类,我们也可以编写自己的扩展方法
@Autowired
private UserMapper userMapper;
@Test
void contextLoads() {
//参数是一个wrapper,条件构造器,这里先不用,可以写为null
//查询全部用户
List<User> users = userMapper.selectList(null);
users.forEach(System.out::println);
}
}
我们使用baseMapper之后sql不可见,我们希望看到执行的sql语句,所以就必须要配置日志输出,老方法,直接在application中进行设置日志输出:
#配置日志 可以设置log4j或者其他的,但是需要导入依赖,此处直接使用默认输出
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
接下来做一个简单的插入测试,还是直接在springboot的test方法中进行:
注意,此处的故意没有设置user对象的id,并且id为主键且参数类型为long,而不是int
//插入测试
@Test
void testInsert(){
User user = new User();
user.setName("xi哥ge");
user.setAge(23);
user.setEmail("77777777777");
int insert = userMapper.insert(user);
System.out.println(insert);
System.out.println(user);
}
然后运行可以发现自动插入了一个id:
那么这个id从哪里来的呢?
这里就涉及了一个主键生成策略
默认生成策略:(默认不重复自增)
@TableId(type = IdType.ID_WORKER)
分布式系统唯一id生成策略: https://www.cnblogs.com/haoxinyue/p/5208136.html
此处用到的就是推特的雪花算法
snowflake是Twitter开源的分布式ID生成算法,结果是一个long型的ID。其核心思想是:使用41bit作为毫秒数,10bit作为机器的ID(5个bit是数据中心,5个bit的机器ID),12bit作为毫秒内的流水号(意味着每个节点在每毫秒可以产生 4096 个 ID),最后还有一个符号位,永远是0。
数据库中主键生成策略有四种:
1,默认自增 id上添加注解@TableId(type = IdType.AUTO)
2,uuid @TableId(type = IdType.UUID)
3,redis实现步进递增(1,4,7,10)
4,mybatisplus自带的雪花算法 @TableId(type = IdType.UUID)
使用主键自增要注意
1实体类字段标注@TableId(type = IdType.AUTO)
2.数据库字段一定要是自增,不然就会报错:
其余主键生成策略解释:
public enum IdType {
AUTO(0), //数据库id自增
NONE(1), //未设置主键时可以使用
INPUT(2), //手动输入
ID_WORKER(3), //默认全局id
UUID(4), //全局唯一id
ID_WORKER_STR(5); //ID_WORKER的字符串表示法
需要注意的是,BaseMapper中的更新方法需要传入的参数为user对象,而不只是一个id
//更新测试
@Test
public void testUpdate(){
User user = new User();
//通过条件自动拼接动态sql,有几个条件,sql语句就多几个条件,
user.setId(7L);
user.setName("yasuo");
user.setEmail("666666666");
int i = userMapper.updateById(user);
System.out.println(i);
}
数据库字段的创建时间,修改时间之类操作都需要自动化完成的
阿里巴巴的开发手册中有写到,所有的数据库表:gmt_create,gmt_modified字段几乎所有的表都要配置上,而且需要自动化
方式一:数据库级别(工作中肯定不允许)
直接在表中新增字段 create_time,update_time
注意这里使用的是sqlyogV12.09版本
然后把实体类同步再次测试插入方法
方式二,代码级别
代码级别则不用设置相关的默认值以及勾选更新操作了
只需要在实体类的属性上增加注解:
//TableField 表单属性
@TableField(fill = FieldFill.INSERT) //在插入的时候(FieldFill.INSERT)自动填充(fill)
private Date createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)//在插入和更新的时候(FieldFill.INSERT_UPDATE)自动填充
private Date updateTime;
然后编写处理器处理注解即可,基本的接口已经已经有了,我们只需要使用即可:
@Slf4j //日志输出
@Component //首先要注入IOC容器被springboot识别就需要添加组件注解@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
//实现接口就需要重写两个方法
//插入时的填充策略
@Override
public void insertFill(MetaObject metaObject) {
log.info("start insert fill.....");
//实现接口自带方法三个参数,第一个是修改的字段名,想要插入的字段值,以及给哪个数据处理(此方法参数)
this.setFieldValByName("createTime",new Date(),metaObject);
this.setFieldValByName("updateTime",new Date(),metaObject);
}
//更新时的填充策略
@Override
public void updateFill(MetaObject metaObject) {
log.info("start insert fill.....");
this.setFieldValByName("updateTime",new Date(),metaObject);
}
}
乐观锁
总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,只在更新的时候会判断一下在此期间别人有没有去更新这个数据。
悲观锁
总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞,直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。
两种锁各有优缺点,像乐观锁适用于写比较少的情况下(多读场景),即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。但如果是多写的情况,一般会经常产生冲突,这就会导致上层应用会不断的进行retry,这样反倒是降低了性能,所以一般多写的场景下用悲观锁就比较合适。
如果不考虑事务隔离性,会产生什么关于读的问题?
脏读 不可重复读 幻读
脏读指一个事务读取了另外一个事务未提交的数据。如果第一个事务这时候回滚了,那么第二个事务就读到脏数据了
幻读一个事务在前后两次查询同一个范围的时候、后一次查询看到了前一次查询未看到的行.两点说明:
1.在可重复读隔离级别下、普通查询是快照读、不会看到别的事务插入数据.所以、只会在当前读下才会出现
2.幻读仅指新插入的行被读到、修改后满足条件、而被读取到、不能称为幻读.
不可重复读是指一个事务范围内,多次查询某个数据,却得到不同的结果。在第一个事务中的两次读取数据之间,由于第二个事务的修改,第一个事务两次读到的数据可能就是不一样的。
二者的区别是,脏读是某一事务读取了另外一个事务因回滚而未提交的数据,不可重复读是读取了其他事务提交的数据。
写有时候也会有丢失更新的问题:
乐观锁可以通过添加版本号和ACS算法来实现,操作之前检查一下添加的版本号或者时间戳是否对应,对应之后再修改
有篇博客写的不错 https://www.jianshu.com/p/766093f59687
乐观锁的具体实现(版本号方法实现):
官方文档地址: https://baomidou.com/pages/0d93c0/#_1-%E9%85%8D%E7%BD%AE%E6%8F%92%E4%BB%B6
举例:
乐观锁:先查询获得版本号version=1
--a线程
update user set name ="aaa",version = version + 1
where id = 2 and version = 1
--b线程
update user set name ="aaa",version = version + 1
where id = 2 and version = 1
以上面两个线程为例,两个线程执行的时候必定有一个线程抢先执行,
执行之后version+1=2,这样就不满足另外一个线程的执行条件了,则另外的线程也就必定失败
实操:
首先在数据库添加一个version的int字段默认为1
然后实体类也添加上:
@Version //乐观锁version注解
private int version;
然后创建一个配置类:
其中乐观锁插件如下:
@MapperScan("com.xige.Mapper") //扫描mapper文件 一般都放在config类中
@EnableTransactionManagement //自动管理事务
@Configuration //配置类注入
public class MybatisPlusConfig {
//注册乐观锁插件
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
return interceptor;
}
}
然后简单测试一下就会发现:
程序首先执行了一个查询操作:
然后才执行的update操作,并在条件后新增一个version条件:
可以看到数据库中version最终也改变了
再模拟一下多线程环境:
//测试乐观锁使用失败
@Test
public void testOptimisticLocker2(){
//线程1
User user = userMapper.selectById(1L);
user.setName("剑神1");
user.setEmail("55555555");
//模拟线程2插队进来
User user2 = userMapper.selectById(1L);
user2.setName("剑神2");
user2.setEmail("666666");
int i = userMapper.updateById(user2);
int i1 = userMapper.updateById(user);
System.out.println("线程1执行影响行数"+i1);
System.out.println("线程2执行影响行数"+i);
System.out.println("执行完毕~~");
}
有先后顺序地拿着相同的执行条件,结果不言而喻:
这里如果没有乐观锁的话,自然就会覆盖插队线程的数据,进而影响结果
ps:这个乐观锁感觉类似于多线程中的灯塔法
//测试查询单个用户
@Test
public void testSelectById(){
User user = userMapper.selectById(1L);
System.out.println(user);
}
//测试批量id
@Test
public void testSelectByBatchId(){
List<User> list = userMapper.selectBatchIds(Arrays.asList(1, 2, 3));
list.forEach(System.out::println);
}
@Test
//and 条件查询方法之一就是使用map操作
public void testSelectByBatchIds(){
HashMap<String, Object> map = new HashMap<>();
//自定义需要查询
map.put("name","yasuo");
map.put("age","20");
List<User> users = userMapper.selectByMap(map);
users.forEach(System.out::println);
}
}
之前使用的分页查询一般都是limit或者pageHelper第三方插件
注意,springboot整合最新版MP下面的插件就不能用了,需要配置专门的分页拦截器:
// 最新mybatisplus分页需要实现分页拦截器
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor(){
MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor());
return mybatisPlusInterceptor;
}
MP也内置了分页插件:直接导入拦截器组件就可以进行使用了:
//分页插件
@Bean
public PaginationInnerInterceptor paginationInnerInterceptor(){
return new PaginationInnerInterceptor();
}
举例:
public void testPageSelect(){
// 参数一,当前页 参数二,页面大小
Page<User> page = new Page<>(1,5);
userMapper.selectPage(page,null);
page.getRecords().forEach(System.out::println);
//page.getRecords() 获取分页后的数据
System.out.println(page.getRecords());
//page.getTotal(); 一共多少条数据
System.out.println(page.getTotal());
//page.getCurrent() 当前第几页
System.out.println(page.getCurrent());
//page.getPages() 一共多少页
System.out.println(page.getPages());
}
基本删除操作:
@Test //ID单个删除
public void testDeleteById(){
userMapper.deleteById(1397580278536482818L);
}
@Test //id批量删除
public void testDeleteBatchId(){
userMapper.deleteBatchIds(Arrays.asList(12406206746455L,7L));
}
@Test //id批量删除
public void testDeleteMap(){
HashMap<String, Object> map = new HashMap<>();
map.put("age",28);
System.out.println(userMapper.deleteByMap(map));
}
我们在工作中会遇到特殊情况,比如逻辑删除
删了,但没完全删~
首先普及一下名词
物理删除:在数据库中直接移除
逻辑删除:在数据库汇总没有被移除,而是通过一个变量(deleted)来让他失效
比如:管理员可以查看被删除的记录,放置数据丢失,类似于回收站
说白了,其实属于更新操作,修改了deleted的默认值,然后普通用户查询的添加了一个deleted≠默认值的条件,管理员则没有附加查询条件
测试一下:在数据表中增加一个deleted字段,然后设定默认值为0
添加pojo对应的字段:
@TableLogic //逻辑删除
private int deleted;
配置:
mybatis-plus:
global-config:
db-config:
logic-delete-field: flag # 全局逻辑删除的实体字段名(since 3.3.0,配置后可以忽略不配置步骤2)
logic-delete-value: 1 # 逻辑已删除值(默认为 1)
logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
测试:
执行一下之前写的id删除
可以发现其实执行了一个update语句
数据库中记录还在,只是deleted变动了:
然后再执行查询语句可以发现:
查询的时候添加了一个逻辑判断,自动过滤被逻辑删除的字段,所以查不出来了,注意,除非使用mybatis.xml编辑原始sql语句,否则查不出来已被删除的数据
我们在平时的开发中会遇到一些慢sql,我们可以通过一些测试,比如druid找出来并进行优化
MP原先提供了性能分析插件现版本已经移除,如果sql执行超过一定时间就会停止运行,
该功能依赖 p6spy 组件,完美的输出打印 SQL 及执行时长 3.1.0 以上版本
首先导入插件:
<dependency>
<groupId>p6spygroupId>
<artifactId>p6spyartifactId>
<version>2.1.4version>
dependency>
这里往后补充方法
十分重要,我们写一些复杂的sql时可以使用它来进行替代
之前在集合查询的时候会提示让输入一个wrapper参数,而这个wrapper本身也有很多的关键字或者说是方法,来帮助我们进行逻辑处理,以下就是在MP官方文档中的部分方法:
eq:equals,等于
gt:greater than ,大于 >
ge:greater than or equals,大于等于≥
lt:less than,小于<
le:less than or equals,小于等于≤
between:相当于SQL中的BETWEEN 在一定范围内的值
like:模糊匹配。like(“name”,“黄”),相当于SQL的name like ‘%黄%’
likeRight:模糊匹配右半边。likeRight(“name”,“黄”),相当于SQL的name like ‘黄%’
likeLeft:模糊匹配左半边。likeLeft(“name”,“黄”),相当于SQL的name like ‘%黄’
notLike:notLike(“name”,“黄”),相当于SQL的name not like ‘%黄%’
isNull
isNotNull
and:SQL连接符AND
or:SQL连接符OR
in: in(“age",{1,2,3})相当于 age in(1,2,3)
groupBy: groupBy(“id”,“name”)相当于 group by id,name
orderByAsc :orderByAsc(“id”,“name”)相当于 order by id ASC,name ASC
orderByDesc :orderByDesc (“id”,“name”)相当于 order by id DESC,name DESC
以下为例,查询name不为null并且邮箱不为null年龄大于等于16的用户
@Test
void contextLoads(){
// 查询name不为null并且邮箱不为null年龄大于等于16的用户
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.isNotNull("name")
.isNotNull("email")
.ge("age",16);// ge 大于等于
userMapper.selectList(wrapper).forEach(System.out::println);
}
可以看到自动添加上了很多的判断句
再举例: 查询名字为Tom的用户:
@Test
void contextLoads2(){
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.eq("name","Tom");
System.out.println(userMapper.selectOne(wrapper));
}
注意了,这里如果查询的结果不唯一,那么sql执行不会有问题,但是结果会报错:
“期望查询值为一,但找到了四个”
再举例,查询年龄在16~23之间的用户:
@Test
void contextLoads3(){
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.between("age",16,23);// 区间
//查询名字为xi哥ge的所有用户
System.out.println(userMapper.selectCount(wrapper));
}
再比如模糊查询:
//模糊查询
@Test
void contextLoads4(){
QueryWrapper<User> wrapper = new QueryWrapper<>();
//查询name中不包含x的并且邮箱号以t开头
wrapper.notLike("name","x")
.likeRight("email","t");
List<Map<String, Object>> maps = userMapper.selectMaps(wrapper);
maps.forEach(System.out::println);
}
嵌套查询:
@Test
//嵌套查询
void contextLoads5(){
QueryWrapper<User> wrapper = new QueryWrapper<>();
//id 在子查询中查出来
wrapper.inSql("id","select id from user where id<3");
List<Object> objects = userMapper.selectObjs(wrapper);
objects.forEach(System.out::println);
}
排序查询:
@Test
//排序测试
void contextLoads6(){
QueryWrapper<User> wrapper = new QueryWrapper<>();
//通过id进行降序排序(Desc) 升序为Asc
wrapper.orderByDesc("id");
List<User> users = userMapper.selectList(wrapper);
users.forEach(System.out::println);
}
AutoGenerator 是 MyBatis-Plus 的代码生成器,通过 AutoGenerator 可以快速生成 Entity、
Mapper、Mapper XML、Service、Controller 等各个模块的代码,极大的提升了开发效率。
以下为例:直接在test中新建一个easycode方法:
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.generator.AutoGenerator;
import com.baomidou.mybatisplus.generator.config.DataSourceConfig;
import com.baomidou.mybatisplus.generator.config.GlobalConfig;
import com.baomidou.mybatisplus.generator.config.PackageConfig;
import com.baomidou.mybatisplus.generator.config.StrategyConfig;
import com.baomidou.mybatisplus.generator.config.po.TableFill;
import com.baomidou.mybatisplus.generator.config.rules.DateType;
import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;
import java.util.ArrayList;
// 代码自动生成器
public class KuangCode {
public static void main(String[] args) {
// 需要构建一个 代码自动生成器 对象
AutoGenerator mpg = new AutoGenerator();
// 配置策略 // 1、全局配置
GlobalConfig gc = new GlobalConfig();
String projectPath = System.getProperty("user.dir");
gc.setOutputDir(projectPath+"/src/main/java");
gc.setAuthor("狂神说");
gc.setOpen(false);
gc.setFileOverride(false);
// 是否覆盖
gc.setServiceName("%sService");
// 去Service的I前缀
gc.setIdType(IdType.ID_WORKER);
gc.setDateType(DateType.ONLY_DATE);
gc.setSwagger2(true);
mpg.setGlobalConfig(gc);
//2、设置数据源
DataSourceConfig dsc = new DataSourceConfig(); dsc.setUrl("jdbc:mysql://localhost:3306/kuang_community? useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT%2B8"); dsc.setDriverName("com.mysql.cj.jdbc.Driver");
dsc.setUsername("root");
dsc.setPassword("123456");
dsc.setDbType(DbType.MYSQL);
mpg.setDataSource(dsc);
//3、包的配置
PackageConfig pc = new PackageConfig();
pc.setModuleName("blog");
pc.setParent("com.kuang");
pc.setEntity("entity");
pc.setMapper("mapper");
pc.setService("service");
pc.setController("controller");
mpg.setPackageInfo(pc);
//4、策略配置
StrategyConfig strategy = new StrategyConfig();
strategy.setInclude("blog_tags","course","links","sys_settings","user_record"," user_say"); // 设置要映射的表名
strategy.setNaming(NamingStrategy.underline_to_camel);
strategy.setColumnNaming(NamingStrategy.underline_to_camel);
strategy.setEntityLombokModel(true);
// 自动lombok;
strategy.setLogicDeleteFieldName("deleted");
// 自动填充配置
TableFill gmtCreate = new TableFill("gmt_create", FieldFill.INSERT); TableFill gmtModified = new TableFill("gmt_modified", FieldFill.INSERT_UPDATE); ArrayList<TableFill> tableFills = new ArrayList<>(); tableFills.add(gmtCreate); tableFills.add(gmtModified); strategy.setTableFillList(tableFills);
// 乐观锁
strategy.setVersionFieldName("version");
strategy.setRestControllerStyle(true); strategy.setControllerMappingHyphenStyle(true); // localhost:8080/hello_id_2
mpg.setStrategy(strategy);
mpg.execute(); //执行
}
}
此处的代码自动生成器并没有进行测试,因为暂时没有适合的环境,老秦的环境是自己新建的一个项目,自带很多表,源码没有给.这里暂时只是把老秦的文档笔记复制一下,权当插眼,下次用到再说