目录
MybatisPlus
入门案例
步骤
- 创建数据库及表,导入数据
- 创建springboot项目,勾选实用技术,引入依赖
- 添加相关配置信息
- 编写实体类,创建mapper层接口
- 引导类添加mapper层接口扫描注解
- 编写测试类
代码实现
数据库及表
create database if not exists mybatisplus_db character set utf8;
use mybatisplus_db;
CREATE TABLE user (
id bigint(20) primary key auto_increment,
name varchar(32) not null,
password varchar(32) not null,
age int(3) not null ,
tel varchar(32) not null
);
insert into user values(1,'Tom','tom',3,'18866668888');
insert into user values(2,'Jerry','jerry',4,'16688886666');
insert into user values(3,'Jock','123456',41,'18812345678');
insert into user values(4,'传智播客','itcast',15,'4006184000');
MybatisPlus的Maven坐标
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.1</version>
</dependency>
配置数据库
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
#serverTimezone是用来设置时区,UTC是标准时区,和咱们的时间差8小时,所以可以将其修改为`Asia/Shanghai
url: jdbc:mysql://localhost:3306/mybatisplus_db?serverTimezone=Asia/Shanghai
username: root
password: root
创建实体类
User
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class User {
private Long id;
private String name;
private String password;
private Integer age;
private String tel;
private Integer online;
}
Mapper接口
UserMapper
public interface UserMapper extends BaseMapper<User> {
}
引导类
@SpringBootApplication
@MapperScan("com.shifan.mapper")//自动扫描该包下的所有接口
public class MybatisPlusApplication {
public static void main(String[] args) {
SpringApplication.run(MybatisPlusApplication.class, args);
}
}
测试类
@SpringBootTest
class MpApplicationTests {
@Autowired
private UserDao userDao;
@Test
public void testGetAll() {
List<User> userList = userDao.selectList(null);
System.out.println(userList);
}
}
简介
MyBatisPlus(简称MP)是基于MyBatis框架基础上开发的增强型工具,旨在简化开发、提高效率
官网:MybatisPlus
使用MP做标准数据层开发
标准CRUD
新增
int insert(T t)
/*
tips:爆红是因为UserMapper为接口,无法实例化实现注入
服务器启动IOC容器初始化后,框架会生成代理对象完成注入
*/
@Autowired
private UserMapper userMapper;
/**
* int insert(T t)
*/
@Test
void testSave() {
User user = User.builder()
.name("时帆")
.age(18)
.password("111")
.tel("13511111111")
.build();
userMapper.insert(user);
}
删除
int deleteById(Serializable id)
/**
* int deleteById(Serializable id)
*/
@Test
void testDeleteById(){
userMapper.deleteById(2L);
}
修改
int updateById(T t)
/**
* int updateById(T t)
*/
@Test
void testUpdateById(){
User user = User.builder()
.name("voracity")
.age(20)
.id(3L)
.password("333")
.build();
userMapper.updateById(user);
}
根据id查询
T selectById(Serializable id)
/**
* T selectById(Serializable id)
*/
@Test
void testSelectById(){
User user = userMapper.selectById(5L);
System.out.println("user = " + user);
}
查询所有
List selectList(Wrapper queryWrapper)
/**
* List<T> selectList(Wrapper<T> queryWrapper)
*/
@Test
void testSelectList(){
List<User> users = userMapper.selectList(null);
System.out.println("users = " + users);
}
分页查询
IPage selectPage(IPage page,Wrapper queryWrapper)
步骤1:调用方法传入参数获取返回值
/**
* IPage<T> selectPage(IPage<T> page , Wrapper<T> queryWrapper)
*/
@Test
void testSelectPage(){
//创建分页对象,设置分页参数,1为第一页,3为显示记录数
IPage<User> page = new Page<>(1,3);
userMapper.selectPage(page, null);
//获取分页结果
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());
}
步骤2:设置分页拦截器
这个拦截器MP已经为我们提供好了,我们只需要将其配置成Spring管理的bean对象即可。
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor(){
//1 创建MybatisPlusInterceptor拦截器对象
MybatisPlusInterceptor mpInterceptor=new MybatisPlusInterceptor();
//2 添加分页拦截器
mpInterceptor.addInnerInterceptor(new PaginationInnerInterceptor());
return mpInterceptor;
}
}
DQL编程控制
准备环境
application.yml
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
#serverTimezone是用来设置时区,UTC是标准时区,和咱们的时间差8小时,所以可以将其修改为`Asia/Shanghai
url: jdbc:mysql://localhost:3306/mybatisplus_db?serverTimezone=Asia/Shanghai
username: root
password: root
main:
#关闭springboot启动log
banner-mode: off
mybatis-plus:
configuration:
# 开启mp日志输出到控制台,影响性能,调试用
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
#关闭mp启动log打印
banner: off
引入依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.16</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.1</version>
</dependency>
</dependencies>
logback.xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!--取消初始化spring日志打印,resources目录下添加logback.xml,名称固定-->
</configuration>
条件查询
- Wrapper
- QueryWrapper
- LambdaQueryWrapper
@Autowired
private UserMapper userMapper;
/**
* 测试构建条件查询
*/
@Test
void testGetAll() {
/*
方式一:
lt表示小于,即条件为查询年龄小于30的数据
缺点:字段名出错难以察觉
*/
/*
QueryWrapper qw = new QueryWrapper();
qw.lt("age",30);
List<User> users = userMapper.selectList(qw);
System.out.println("users = " + users);
*/
/*
方式二:
必须指定泛型,调用lambda()方法开启lambda表达式的使用
缺点:多了一层.lambda()调用
*/
/*
QueryWrapper<User> qw = new QueryWrapper();
qw.lambda().lt(User::getAge,30);
List<User> users = userMapper.selectList(qw);
System.out.println("users = " + users);
*/
/*
方式三:
使用LambdaQueryWrapper类,不在需要调用lambda()
*/
LambdaQueryWrapper<User> lqw = new LambdaQueryWrapper<>();
lqw.lt(User::getAge,30);
List<User> users = userMapper.selectList(lqw);
System.out.println("users = " + users);
}
-
多条件构建:or()
-
null值判定
/**
* 测试构建多条件查询
*/
@Test
void testGetAll1() {
LambdaQueryWrapper<User> lqw = new LambdaQueryWrapper<>();
//需求:查询年龄在10岁到30岁之间的用户信息
//lqw.lt(User::getAge,30);
//lqw.gt(User::getAge,10);
//支持链式编程
//lqw.lt(User::getAge,30).gt(User::getAge,10);
//需求:查询年龄小于10或年龄大于30的数据
//lqw.lt(User::getAge,10).or().gt(User::getAge,30);
/*
null值判定,多条件查询时,可能存在条件传递为null的情况
需求:根据输入年龄范围来查询符合条件的记录(年龄上限,下限,可能不传递数据,即为null)
传统解决方案:使用if语句判断
*/
/*
QueryUser qu = new QueryUser();
qu.setAge(10);
if (qu.getAge()!=null){
lqw.gt(User::getAge,qu.getAge());
}
if (qu.getAge1()!=null){
lqw.lt(User::getAge,qu.getAge1());
}
*/
/*
MybatisPlus解决方案:
*/
QueryUser qu = new QueryUser();
qu.setAge(10);
lqw.gt(null!=qu.getAge(),User::getAge,qu.getAge());
lqw.lt(null!=qu.getAge1(),User::getAge,qu.getAge1());
List<User> users = userMapper.selectList(lqw);
System.out.println("users = " + users);
}
查询投影
- 查询指定字段:select()
/**
* 查询投影
*/
@Test
void testGetAll2() {
LambdaQueryWrapper<User> lqw = new LambdaQueryWrapper<>();
//需求:查询指定字段:id,name,age
/*
lqw.select(User::getId,User::getName,User::getAge);
List<User> users = userMapper.selectList(lqw);
*/
//若不使用lambda表达式,则需要手动设置字段
QueryWrapper<User> qw = new QueryWrapper<>();
qw.select("id","name","age");
List<User> users = userMapper.selectList(qw);
System.out.println("users = " + users);
}
-
聚合查询:eg:select(“count(id) count”)
-
分组查询:eg:groupBy(User::getTel)
/**
* 聚合函数查询
* 聚合函数不支持lambda表达式,lambda表达式只支持实体类属性字段
*/
@Test
void testGetAll3() {
QueryWrapper<User> qw = new QueryWrapper<>();
//qw.select("count(id) count");
//加上分组条件
qw.select("count(id) count ,tel")
.groupBy("tel");
List<Map<String, Object>> maps = userMapper.selectMaps(qw);
System.out.println("maps = " + maps);
}
@Test
void test(){
LambdaQueryWrapper<User> lqw = new LambdaQueryWrapper<>();
lqw.select(User::getTel);
lqw.groupBy(User::getTel);
List<Map<String, Object>> users = userMapper.selectMaps(lqw);
System.out.println("users = " + users);
}
查询条件
- 等值查询:eq()
/**
* 等值查询
* 需求:根据用户名和密码查询用户信息
* eq:相当于=
*/
@Test
void testGetOne() {
LambdaQueryWrapper<User> lqw = new LambdaQueryWrapper<>();
lqw.eq(User::getName,"John");
lqw.eq(User::getPassword,"shifan");
User user = userMapper.selectOne(lqw);
System.out.println("user = " + user);
}
- 范围查询:lt(),le(),gt(),ge(),between()
/**
* 范围查询
* 需求:对年龄进行范围查询,使用lt()、le()、gt()、ge()、between()进行范围查询
* le:相当于<=
* ge:相当于>=
*/
@Test
void testGetAll4(){
LambdaQueryWrapper<User> lqw = new LambdaQueryWrapper<>();
lqw.between(User::getAge,10,20);
List<User> users = userMapper.selectList(lqw);
System.out.println("users = " + users);
}
- 模糊查询:like(),likeLeft(),likeRight()
/**
* 模糊查询
* 需求:查询表中name属性的值以`J`开头的用户信息,使用like进行模糊查询
* like():在属性值字段的左右都加%,eg:lqw.like(User::getName,"J") ==> %J%
* likeLeft():在属性值字段左边加%,eg:%J
* likeRight():在属性值字段的右边加%,eg:J%
*/
@Test
void testGetAll5() {
LambdaQueryWrapper<User> lqw = new LambdaQueryWrapper<>();
lqw.likeRight(User::getName,"J");
List<User> users = userMapper.selectList(lqw);
System.out.println("users = " + users);
}
- 排序查询:orderBy(),orderByAsc(),orderByDesc()
/**
* 排序查询
* 需求:查询所有数据,然后按照id降序
* orderBy(boolean condition, boolean isAsc, R... columns):
* 第一个参数:是否排序,第二个参数:是否升序,true升序,false降序,第三个参数:字段名,可设置一个或多个
* - orderByAsc/Desc(单个column):按照指定字段进行升序/降序
* - orderByAsc/Desc(多个column):按照多个字段进行升序/降序
* - orderByAsc/Desc
* - condition:条件,true添加排序,false不添加排序
* - 多个columns:按照多个字段进行排序
* 此外还有isNull,isNotNull,in,notIn等方法
*/
@Test
void testGetAll6() {
LambdaQueryWrapper<User> lqw = new LambdaQueryWrapper<>();
lqw.orderBy(true,false,User::getId);
List<User> users = userMapper.selectList(lqw);
System.out.println("users = " + users);
}
映射匹配兼容性
-
@TableField(value=“表字段名”)
-
@TableField(select = false)
-
@TableField(exist = false)
-
@TableName(“表名”)
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@TableName("tb_user")//映射数据库表名,用于数据库表名和实体类名不一致情况
public class User {
private Long id;
private String name;
@TableField(value = "pwd",select = false)//字段映射,value属性为数据库表中字段名,selcet属性用于设置该字段是否被查询
private String password;
private Integer age;
private String tel;
@TableField(exist = false)//exist属性用于表明该字段在数据库表中是否存在
private Integer online;
}
DML编程控制
id生成策略
- @TableId(type = IdType.NONE)
NONE表示不使用id生成策略,与INPUT类似,需要手动注入id
- @TableId(type = IdType.AUTO)
AUTO表示使用数据库的主键id自增,前提是数据库的主键id设置了自增属性
- @TableId(type = IdType.INPUT)
INPUT表示id使用手动注入的方式
- @TableId(type = IdType.ASSIGN_ID)
ASSIGN_ID表示使用MP的雪花算法生成id
- @TableId(type = IdType.ASSIGN_UUID)
ASSIGN_UUID表示使用UUID生成id
实际使用案例:
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
//@TableName("tb_user")//映射数据库表名,用于数据库表名和实体类名不一致情况
public class User {
//@TableId(type = IdType.NONE)
// 不使用主键生成策略,类似Input
//@TableId(type= IdType.AUTO)
// 使用数据库主键自增,需要保证数据库主键字段有自增属性
//@TableId(type = IdType.Input)
// 用户手动输入,需要去除数据库主键自增属性,若不给定id值会报错
//@TableId(type = IdType.ASSIGN_UUID)
// 使用UUID生成主键id,主键类型应为字符串,数据库表中id长度至少32位,长度过小会插入失败
//@TableId(type = IdType.ASSIGN_ID)
// 使用雪花算法生成主键id(可兼容数值型与字符串型),若所传递的id为null,默认使用此策略,若传递了值则使用传递的值
private Long id;
private String name;
@TableField(value = "pwd",select = false)
//字段映射,value属性为数据库表中字段名,selcet属性用于设置该字段是否被查询
private String password;
private Integer age;
private String tel;
@TableField(exist = false)
//exist属性用于表明该字段在数据库表中是否存在
private Integer online;
}
简化配置
在配置文件中添加如下配置即可实现全局使用雪花算法生成id
mybatis-plus:
global-config:
db-config:
id-type: assign_id
以下配置用于配置全局表名前缀
mybatis-plus:
global-config:
db-config:
table-prefix: tbl_
测试
/**
* 测试主键自增策略
* 拓展:分布式id
* 当数据量足够大时就需要将数据存储在多台数据库服务器上
* 例如订单表存储在不同服务器上,而此时主键若使用自增策略就可能出现冲突
* 这时就需要一个全局唯一的id,称为分布式id
*
* 雪花算法:
* 雪花算法(SnowFlake),是Twitter官方给出的算法实现 是用Scala写的。
* 其生成的结果是一个64bit大小整数,它的结构分为四部分,如下:
* 0-00000000 00000000 00000000 00000000 00000000 0-00000000 00-00000000 0000
* 第一部分: 1bit,不用,因为二进制中最高位是符号位,1表示负数,0表示正数。生成的id一般都是用整数,所以最高位固定为0。
* 第二部分: 41bit-时间戳,用来记录时间戳,毫秒级
* 第三部分: 10bit-工作机器id,用来记录工作机器id,其中高位5bit是数据中心ID其取值范围0-31,低位5bit是工作节点ID其取值范围0-31,两个组合起来最多可以容纳1024个节点
* 第四部分: 序列号占用12bit,每个节点每毫秒从0开始不断累加,最多可以累加到4095,每毫秒内一共可以产生4096个ID
* id生成策略对比:
* - NONE: 不设置id生成策略,MP不自动生成,约等于INPUT,所以这两种方式都需要用户手动设置,但是手动设置第一个问题是容易出现相同的ID造成主键冲突,为了保证主键不冲突就需要做很多判定,实现起来比较复杂
* - AUTO:数据库ID自增,这种策略适合在数据库服务器只有1台的情况下使用,不可作为分布式ID使用
* - ASSIGN_UUID:可以在分布式的情况下使用,而且能够保证唯一,但是生成的主键是32位的字符串,长度过长占用空间而且还不能排序,查询性能也慢
* - ASSIGN_ID:可以在分布式的情况下使用,生成的是Long类型的数字,可以排序性能也高,但是生成的策略和服务器时间有关,如果修改了系统时间就有可能导致出现重复主键
* 综上:根据实际情况选择
*/
@Test
void testInsert(){
User user = User.builder()
.name("xiaoming")
.age(23)
.tel("12345678345")
.password("333")
.build();
userMapper.insert(user);
}
多记录操作
需求:批量删除用户数据,批量查询用户数据
测试如下:
/**
* 测试批量删除
*/
@Test
void testDeleteByIds(){
List<Long> list = new ArrayList<>();
list.add(1647447562652774401L);
list.add(1647452538326233089L);
list.add(1647452707453181954L);
userMapper.deleteBatchIds(list);
}
/**
* 测试批量查询
*/
@Test
void testSelectByIds(){
List<Long> list = new ArrayList<>();
list.add(1L);
list.add(3L);
list.add(4L);
List<User> users = userMapper.selectBatchIds(list);
System.out.println("users = " + users);
}
逻辑删除
- @TableLogic
实现步骤:
- 在数据库表中添加标识逻辑删除的字段
- 在实体类中添加对应的标识字段,并在此字段上添加@TableLogic注解
- 设置@TableLogic的value和delval属性值,value属性表示当前数据为正常数据,delval属性表示当前数据为
已被删除的数据
实际应用:
//@TableLogic(value = "0",delval = "1")
//逻辑删除字段,标记当前数据是否被删除,value表示正常数据,delval表示该数据已被删除
private Integer deleted;
全局配置:
mybatis-plus:
global-config:
db-config:
# 逻辑删除字段名
logic-delete-field: deleted
# 逻辑删除字面值:未删除为0
logic-not-delete-value: 0
# 逻辑删除字面值:删除为1
logic-delete-value: 1
测试
/**
* 测试逻辑删除
* 用途:解决物理删除(直接删除表中数据,delete操作)会对数据造成伤害的问题
* 步骤:在表中添加一个字段(添加默认值属性),用于标识当前数据是否被删除
* 在实体类中添加对应的字段并使用@TableLogic注解标识该字段,给定value和delval属性的值即可
* 逻辑删除本质上执行的是更新语句,配置了逻辑删除字段后,查询也会自动带上该字段,只查询未被删除的数据
* 若需要查询已被删除的数据则需要自己手写实现
*/
@Test
void testLogicDelete(){
userMapper.deleteById(1L);
List<User> users = userMapper.selectList(null);
System.out.println("users = " + users);
}
乐观锁
概念
主要用于解决修改数据时,期望当前数据未被其他人修改
实现思路
步骤
/**
* 乐观锁测试
* 原理:
* 给数据库表添加字段,如version默认值为1
* 当两个线程同时修该一条数据时,需要先获取表中数据,拿到version的值
* eg:线程A和B同时来修改id为1的用户的信息
* 线程A查询id为1的用户信息,得到此时用户的version字段值为1
* 线程B同样查询到id为1的用户信息,此时用户的version字段值为1
* 假设线程B先于线程A修改了id为1的用户的信息,并将version的值加了1
* 即:update user set name = ? , age = ? ,version = version + 1 where id = 1 and version = 1
* 此时线程A再去修改用户信息时就会修改失败,因其修改语句为:
* update user set name = ? , age = ? ,version = version + 1 where id = 1 and version = 1
* 而version值已被修改为2,故修改失败
* 实现步骤:
* 1.在数据库表中添加标识字段,并设置默认值,eg:version字段,默认值1
* 2.在实体类中添加对应的属性字段,并加上注解@Version
* 3.添加乐观锁拦截器
* 4.查询需要修改的数据,获取version值
* 5.更改数据值,更新数据
*/
@Test
void testOptimisticLock(){
//没有设置version值,无法实现乐观锁
/*
User user = User.builder()
.id(3L)
.name("mp")
.password("399")
.tel("322")
.age(10)
.build();
*/
//先查询需要修改的数据
User user = userMapper.selectById(3L);
//修改数据
user.setName("mp");
user.setAge(10);
user.setPassword("3");
//更新数据
userMapper.updateById(user);
}
/**
* 模拟多线程情况修改同一条数据
*/
@Test
void testOptimisticLock1(){
//先查询需要修改的数据
User user = userMapper.selectById(3L);//取得的version值为2
User user1 = userMapper.selectById(3L);//取得的version值为2
user1.setName("user1");
userMapper.updateById(user1);//修改成功,version->3
user.setName("user");
userMapper.updateById(user);//修改失败,此时version值已被修改为3
}