Mybatis简介
MyBatis-Plus(简称 MP)是一个 MyBatis 的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。
(来自官网)
体验Mybatisplus
1.创建SpringBoot工程,导入mysql,lombok,spring web
2.改setting里面的encoding为utf-8
3.pom文件中引入依赖
<!-- druid数据源-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.16</version>
</dependency>
<!-- mybatisplus-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.1</version>
</dependency>
4.yml文件中设置数据源相关参数
4.编写实体类
package com.example.entity;
import lombok.Data;
@Data
public class User {
private Long id;
private String name;
private String password;
private Integer age;
private String tel;
}
5.编写mapper接口
继承BaseMapper,指定泛型为User
加上@Mapper注解
tips:如何让springboot的启动类扫到mapper文件夹里面的mapper接口
1.加上@Mapper注解,同时需要保证mapper文件夹在启动类所在包或其子包中(也就是启动类所在的层次要比被扫描的文件夹更高或同级)
2.启动类加上@MapperScan,指定要扫描的文件夹
6.写一个测试类,进行测试
@SpringBootTest
public class TestUserMapper {
@Autowired
private UserMapper userMapper;
@Test
void testSelectAll(){
List<User> users = userMapper.selectList(null);
System.out.println("users:"+users);
}
}
ps:如果是第二种方式扫描mapper接口,userMapper会爆红,这是因为UserMapper是一个接口,不能实例化对象 。只有在服务器启动IOC容器初始化后,由框架创建DAO接口的代理对象来注入。 现在服务器并未启动,所以代理对象也未创建,IDEA查找不到对应的对象注入,所以提示报红 一旦服务启动,就能注入其代理对象,所以该错误提示不影响正常运行。
Mybatisplus标准CURD以及分页
增
@Test
void testInsert(){
//增
User user = new User();
user.setName("李四");
user.setAge(18);
user.setPassword("1599");
user.setTel("18959657411");
int i = userMapper.insert(user);
System.out.println("新增了"+i+"条数据");
}
删
@Test
void testDelete(){
//删
int i = userMapper.deleteById(1827551884282552322L);
System.out.println("删除了"+i+"条数据");
}
改
@Test
void testUpdate(){
//改
User user = new User();
user.setId(1L);
user.setName("Tom·A·Cat");
int i = userMapper.updateById(user);
System.out.println("修改了"+i+"条数据");
}
ps:修改的时候,只修改实体对象中有值的字段 。如果是我们以前用的mybatis做开发,没有指定password,age和tel,这些字段会被覆盖为null,mybatisplus的优势就体现出来了。
查
@Test
void testSelect(){
//查
User user = userMapper.selectById(1L);
System.out.println(user);
}
分页
官网:分页插件 | MyBatis-Plus
首先要添加一个分页拦截器
package com.example.config;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MybatisPlusConfig {
/**
* 添加分页插件
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); // 如果配置多个插件, 切记分页最后添加
// 如果有多数据源可以不配具体类型, 否则都建议配上具体的 DbType
return interceptor;
}
}
使用分页功能
@Test
void testPage(){
//1.创建IPage对象(Page实现了IPage接口),设置分页参数
Page<User> page = new Page<>(1,3);//当前页码为1 每页5条数据
//2.执行分页查询
userMapper.selectPage(page,null);//没有指定条件
//3.获取分页结果
System.out.println("当前页码:"+page.getCurrent());
System.out.println("每页显示数:"+page.getSize());
System.out.println("一共多少页:"+page.getPages());
System.out.println("一共多少条数据:"+page.getTotal());
System.out.println("数据:"+page.getRecords());
}
*****************************************************
DQL编程控制
条件查询
我们需要依靠wrapper来构建要查询的条件
构建条件查询的三种方式
年龄小于10岁的
1.QueryWrapper
//1.QueryWrapper
@Test
void testQuery01(){
QueryWrapper<User> qw = new QueryWrapper<>();
qw.lt("age",10);
System.out.println(userMapper.selectList(qw));
}
2.lambda方式的QueryWrapper
//2.lambda方式的QueryWrapper
@Test
void testQuery02(){
QueryWrapper<User> qw = new QueryWrapper<>();
qw.lambda().lt(User::getAge,10);
System.out.println(userMapper.selectList(qw));
}
解决了字段可能写错的问题 。
3.LambdaQueryWrapper
//3.LambdaQueryWrapper
@Test
void testQuery03(){
LambdaQueryWrapper<User> lqw = new LambdaQueryWrapper<>();
lqw.lt(User::getAge,10);
System.out.println(userMapper.selectList(lqw));
}
不用 .lambda()了,代码更加简洁。
多条件查询
年龄大于10且小于20的 (and)
//年龄大于10且小于20
@Test
void testQuery04(){
LambdaQueryWrapper<User> lqw = new LambdaQueryWrapper<>();
lqw.gt(User::getAge,10).lt(User::getAge,20);//支持链式编程
System.out.println(userMapper.selectList(lqw));
}
年龄小于10或大于20的 (or)
//年龄小于10或大于20
@Test
void testQuery05(){
LambdaQueryWrapper<User> lqw = new LambdaQueryWrapper<>();
lqw.lt(User::getAge,10).or().gt(User::getAge,20);// or()
System.out.println(userMapper.selectList(lqw));
}
null条件判定
需求
用户输入两个年龄(minAge和maxAge),查询用户指定年龄范围里的User,但是如果用户只想查看18岁以下的User,只填写了maxAge为18,那么minAge为null,这时候要怎样让minAge不加入条件呢?MybatisPlus早就想到了这一点。
**************************************************************
写一个AgeField实体类来模拟前端传来的minAge和maxAge
package com.example.entity;
import lombok.Data;
@Data
//模拟前端传过来的最小年龄和最大年龄
public class AgeField {
private Integer minAge;
private Integer maxAge;
}
测试代码
/**
* null判定
*/
@Test
void testQuery06(){
AgeField af = new AgeField();
af.setMinAge(10);
af.setMaxAge(20);
LambdaQueryWrapper<User> lqw = new LambdaQueryWrapper<>();
lqw.gt(af.getMinAge()!=null,User::getAge,af.getMinAge())
.lt(af.getMaxAge()!=null,User::getAge,af.getMaxAge());
List<User> users = userMapper.selectList(lqw);
System.out.println(users);
System.out.println(users.size());
}
参数condition:为真时该条件才加入查询语句
参数column:表中字段
参数val:传入的字段值
数据库
共15条记录
测试结果
传入10和20 ,查到两条数据
现在我们把这两条注掉,两条的条件都为false,不加入查询条件,相当于select * from user
// af.setMinAge(10);
// af.setMaxAge(20);
查到15条数据,也就是user表中的所有数据,符合预期。
查询投影
user表中新增一个sex字段,同时修改实体类
查询结果包含模型类中部分属性
LambdaQueryMapper写法
@Test
//查询结果包含模型类中部分属性
void testQuery07(){
LambdaQueryWrapper<User> lqw = new LambdaQueryWrapper<>();
//查询年龄在10~20之间的所有用户的id,name,age信息
lqw.select(User::getId,User::getName,User::getAge);
lqw.gt(User::getAge,10);
lqw.lt(User::getAge,20);
System.out.println(userMapper.selectList(lqw));
}
QueryMapper写法
@Test
void testQuery08(){
QueryWrapper<User> qw = new QueryWrapper<>();
qw.select("id","name","age");
qw.gt("age",10);
qw.lt("age",20);
System.out.println(userMapper.selectList(qw));
}
测试结果
查询结果包含模型类中未定义属性
聚合和分组,无法使用lambda表达式完成,正如MP官网所说的,MybatisPlus只是对Mybatis进行增强,MP无法做到的事情我们还是要老老实实地回到mapper接口里面写sql。
聚合查询
@Test
//聚合
void testQuery09(){
QueryWrapper<User> qw = new QueryWrapper<>();
//这里也可以连着写,为了方便看,用逗号分开
qw.select("count(*) as count",
"max(age) as maxAge",
"min(age) as minAge",
"sum(age) as sumAge",
"avg(age) as avgAge");
//用selectMaps方法,返回一个map,这个map的key为聚合字段,value为聚合结果
List<Map<String, Object>> maps = userMapper.selectMaps(qw);
System.out.println(maps);
}
分组查询
相当于 select sex,count(*) from user group by sex;
@Test
//分组
void testQuery10(){
//按性别分组
QueryWrapper<User> qw = new QueryWrapper<>();
qw.select("sex,count(*) as count");
qw.groupBy("sex");
List<Map<String, Object>> maps = userMapper.selectMaps(qw);
System.out.println(maps);
}
深入体验查询条件的设置
条件构造器 | MyBatis-Plus
等值查询
@Test
void testQuery11(){
//等值查询 模拟登录校验
LambdaQueryWrapper<User> lqw = new LambdaQueryWrapper<>();
lqw.eq(User::getName,"张三").eq(User::getPassword,"zs666");
User loginUser = userMapper.selectOne(lqw);//只查一个用selectOne就可以
System.out.println(loginUser);
}
范围查询
@Test
void testQuery12(){
//范围查询 lt le gt ge between
LambdaQueryWrapper<User> lqw = new LambdaQueryWrapper<>();
lqw.between(User::getAge,10,20);
userMapper.selectList(lqw);
}
模糊查询
@Test
void testQuery13(){
/**
* 模糊查询 like
* %:匹配任意字符
* _:匹配任意单个字符
* **********************
* 官网中并没有看到匹配任意单个字符要怎么写,
* 如果要匹配 刘亦菲、刘二菲、刘三菲...这样的名字可能还是要自己写sql (like 刘_菲)
*/
LambdaQueryWrapper<User> lqw = new LambdaQueryWrapper<>();
//lqw.like(User::getName,"o"); //名字里带o的 like %o%
//lqw.likeRight(User::getName,"杨"); // like 杨%
//lqw.likeLeft(User::getName,"at"); // like %at
lqw.notLike(User::getName,"月"); //not like %月%
userMapper.selectList(lqw);
}
排序查询
1.
@Test
void testQuery14(){
//排序查询
LambdaQueryWrapper<User> lqw = new LambdaQueryWrapper<>();
/**
* 参数:condition isAsc columns
* conidtion:条件,为true时 orderby 加入查询语句
* isAsc:是否为升序
* columns:按哪个列排序
*/
//按年龄降序,年龄一样按id降序
lqw.orderBy(true,false,User::getAge,User::getId);
userMapper.selectList(lqw);
}
2.
@Test
void testQuery15(){
LambdaQueryWrapper<User> lqw = new LambdaQueryWrapper<>();
//按年龄降序,按id升序
//注意这里哪个字段写在前面,它的优先级就更高
lqw.orderByDesc(User::getAge);
lqw.orderByAsc(User::getId);
userMapper.selectList(lqw);
}
字段映射和表名映射
问题1:表字段与编码属性设计不同步
实体类里面是pwd,数据库里面是password
解决:
问题2:编码中添加了数据库中未定义的属性
问题3:采用默认查询开放了更多的字段查看权限
pwd是敏感数据,如果进行查询,返回的json数据中携带了pwd,这是很危险的,因此我们需要在查询中将pwd默认隐藏
@Test
void testQuery16(){
LambdaQueryWrapper<User> lqw = new LambdaQueryWrapper<>();
userMapper.selectList(lqw);
}
默认的查询是查不出来pwd的
@Test
void testQuery17(){
LambdaQueryWrapper<User> lqw = new LambdaQueryWrapper<>();
lqw.select(User::getPwd);
userMapper.selectList(lqw);
}
指定要查pwd,这样是可以查出来的
问题4:表名与编码开发设计不同步
DML编程控制
id生成策略
MP通过@TableId控制id的生成策略
ctrl+鼠标左键,进入IdType源码
/**
* 数据库ID自增
* <p>该类型请确保数据库设置了 ID自增 否则无效</p>
*/
AUTO(0),
/**
* 该类型为未设置主键类型(注解里等于跟随全局,全局里约等于 INPUT)
*/
NONE(1),
/**
* 用户输入ID
* <p>该类型可以通过自己注册自动填充插件进行填充</p>
*/
INPUT(2),
/* 以下3种类型、只有当插入对象ID 为空,才自动填充。 */
/**
* 分配ID (主键类型为number或string),
* 默认实现类 {@link com.baomidou.mybatisplus.core.incrementer.DefaultIdentifierGenerator}(雪花算法)
*
* @since 3.3.0
*/
ASSIGN_ID(3),
/**
* 分配UUID (主键类型为 string)
* 默认实现类 {@link com.baomidou.mybatisplus.core.incrementer.DefaultIdentifierGenerator}(UUID.replace("-",""))
*/
ASSIGN_UUID(4),
/**
* @deprecated 3.3.0 please use {@link #ASSIGN_ID}
*/
@Deprecated
ID_WORKER(3),
/**
* @deprecated 3.3.0 please use {@link #ASSIGN_ID}
*/
@Deprecated
ID_WORKER_STR(3),
/**
* @deprecated 3.3.0 please use {@link #ASSIGN_UUID}
*/
@Deprecated
UUID(4);
AUTO(0)
id自增,同时数据库中要设置id自增
@TableId(type = IdType.AUTO)
private Long id;
ALTER TABLE tb_user AUTO_INCREMENT=16;
@Test
void testInsert01(){
User user = new User();
user.setName("李薇");
user.setPwd("12637489");
user.setAge(50);
user.setSex(1);
user.setTel("959559999");
user.setId(20L);
userMapper.insert(user);
}
指定它的id为20,insert到数据库的id仍是16
删除这条数据,重新insert一次
id为17,由此知道AUTO_INCREMENT不会因为数据的删除而回退
NONE(1)
该类型为未设置主键类型(注解里等于跟随全局,全局里约等于 INPUT)
@TableId(type = IdType.NONE)
private Long id;
执行两次 testInsert01 ,指定id为20,25,结果如下
INPUT(2)
需要自己输入id
@TableId(type = IdType.INPUT)
private Long id;
执行两次 testInsert01 ,指定id为30,35,结果如下
ASSIGN_ID(3)<——过时的ID_WORKER和ID_WORKER_STR合并
雪花算法生成id(可兼容数值型和字符串型)
@TableId(type = IdType.ASSIGN_ID)
private Long id;
指定id为40,这样生成的id为40
@Test
void testInsert01(){
User user = new User();
user.setName("李薇");
user.setPwd("12637489");
user.setAge(50);
user.setSex(1);
user.setTel("959559999");
user.setId(40L);
userMapper.insert(user);
}
不指定id,按照雪花算法生成id
// user.setId(45L);
雪花算法生成的id的格式
ASSIGN_UUID(4)<——过时的UUID
以UUID生成算法作为id生成策略
需要表中主键的id为varchar(32),
因为生成的uuid为32位,形如b098c2c603a317326af11fe64371e916
@TableId(type = IdType.ASSIGN_UUID)
private String id;
执行1次添加
多数据删除
@Test
void testDelete01(){
//根据id删除多条数据
ArrayList<Long> list = new ArrayList<>();
list.add(1827910579805876225L);
list.add(40L);
userMapper.deleteBatchIds(list);
}
成功删除两条数据
逻辑删除
问题分析
实战演练
1.数据库表中添加一个deleted字段,默认0
2.实体类添加属性
//0表示正常值 1表示该数据已被逻辑删除
@TableLogic(value = "0",delval = "1")
private Integer deleted;
3.测试
3.1先进行一次查询所有,注意现在数据库里面的记录的deleted字段都是0
@Test
void testLogic(){
userMapper.selectList(null);
}
查出来所有的19条记录(注意这个sql,我们的代码中并没有显示地设定任何查询条件)
3.2 删除第一条数据再进行一次查询所有
@Test
void testLogic(){
userMapper.deleteById(1L);
userMapper.selectList(null);
}
id为1的记录已被逻辑删除,这里只有18条记录
deleted字段为1,表示被逻辑删除了
3.3 指定查询条件查一下
@Test
void testLogic02(){
userMapper.selectById(1L);
}
他会自动把逻辑删除字段加到查询条件里
4.设置逻辑删除字段的另一种方式:在yml文件中配置
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 日志
global-config:
banner: off # 关闭MybatisPlus启动图标
db-config:
table-prefix: tb_ #表名前缀
logic-delete-field: deleted #逻辑删除字段名
logic-not-delete-value: 0 #正常值
logic-delete-value: 1 #逻辑删除后的值
乐观锁
实现思路
0.添加拦截器
@Configuration
public class MybatisPlusConfig {
/**
* 添加分页插件
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());//添加乐观锁的拦截器
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); // 如果配置多个插件, 切记分页最后添加
// 如果有多数据源可以不配具体类型, 否则都建议配上具体的 DbType
return interceptor;
}
}
1.表中添加字段version,用作版本控制
给默认值为1,长度为int的最大值11位
2.实体类中添加属性
@Version
private Integer version;
3.测试
测试1
起始数据
执行更新语句
@Test
void testLock01(){
User user = new User();
user.setId(1L);
user.setName("tomcat");
userMapper.updateById(user);
}
发现version并没有改变,这是因为我们并没有拿到过version
重新执行如下语句
@Test
void testLock02(){
User user = userMapper.selectById(1L);
user.setName("tomkitty");
userMapper.updateById(user);
}
我们先查询id为1的用户拿到了该用户的实体(当然包括它的version)这样一来更新就会自动校验version
测试2
模拟两个请求对同一个记录进行修改
void testLock03(){
User user1 = userMapper.selectById(2L);
User user2 = userMapper.selectById(2L);
user1.setName("JerryA");
user2.setName("JerryB");
userMapper.updateById(user2);//version被修改成2
userMapper.updateById(user1);//它还以为是version=1 结果和数据库的version对不上 修改失败
}
最终结果
代码生成器
1.新创建一个springboot项目
引入mysql lombok,web(会生成controller) 即可
2.在pom文件中加入代码生成器需要的依赖
<!--代码生成器-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.4.1</version>
</dependency>
<!--velocity模板引擎-->
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity-engine-core</artifactId>
<version>2.3</version>
</dependency>
3.创建代码生成类
package com.example;
import com.baomidou.mybatisplus.annotation.IdType;
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;
public class CodeGenerator {
public static void main(String[] args) {
//1.获取代码生成器的对象
AutoGenerator autoGenerator = new AutoGenerator();
//2.设置数据库相关配置
DataSourceConfig ds = new DataSourceConfig();
ds.setDriverName("com.mysql.cj.jdbc.Driver");
ds.setUrl("jdbc:mysql://localhost:3306/mybatisplus_db?serverTimezone=UTC");
ds.setUsername("root");
ds.setPassword("root");
autoGenerator.setDataSource(ds);
//3.设置全局配置
GlobalConfig gc = new GlobalConfig();
//代码生成位置(默认D盘的根目录) user.dir=D:/java_workspace
gc.setOutputDir("D:/java_workspace/MybatisPlusProject02/src/main/java/com/example");
//设置生成完毕后是否打开生成代码所在的目录(默认打开)
gc.setOpen(false);
gc.setAuthor("chen");//设置作者
gc.setFileOverride(true);//是否覆盖原来生成的文件
gc.setMapperName("%sMapper"); //设置名字格式为为 UserMapper,AccountMapper.....
gc.setIdType(IdType.ASSIGN_ID); //设置id生成策略
autoGenerator.setGlobalConfig(gc);
//4.设置包名相关配置
PackageConfig pc = new PackageConfig();
//pc.setParent("com.baomidou");
pc.setParent("mpGenerator");//设置生成的包名
pc.setEntity("entity");//设置实体类包名
pc.setMapper("mapper");//设置mapper接口文件夹的名字
autoGenerator.setPackageInfo(pc);
//5.策略设置
StrategyConfig sc = new StrategyConfig();
sc.setInclude("tb_user");//设置参与代码生成的表
sc.setTablePrefix("tb_");//设置数据库表前缀,这样它生成的类前面就不会带Tb_了
sc.setRestControllerStyle(true);//设置控制器为rest风格的控制器
sc.setVersionFieldName("version");//设置乐观锁字段名
sc.setLogicDeleteFieldName("deleted");//设置逻辑删除字段名,仍然需要自己指定逻辑删除的值
sc.setEntityLombokModel(true);//启用lombok
autoGenerator.setStrategy(sc);
//6.执行代码生成
autoGenerator.execute();
}
}
4.运行代码生成类中的main方法,查看结果
开启Mybatisplus日志
yml文件里面配置
#查看MP执行的sql语句
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl