4 条件构造器
警告:
不支持以及不赞成在 RPC 调用中把 Wrapper 进行传输
- wrapper 很重
- 传输 wrapper 可以类比为你的 controller 用 map 接收值(开发一时爽,维护火葬场)
- 正确的 RPC 调用姿势是写一个 DTO 进行传输,被调用方再根据 DTO 执行相应的操作
4.1 wrapper介绍
带Query字样的是查询用,带Update字样的是update用,带Lambda字样的是可以用Lambda语法的类。
4.2 wrapper使用样例
条件构造器所有使用参见官方文档:https://baomidou.com/pages/10c804/
@Autowired
private UserMapper userMapper;
@Test
void test1() {//like()、between()、isNotNull()
QueryWrapper<User> wrapper = new QueryWrapper<>();
//查询name包含a,年龄20-30,邮箱不为null的users
wrapper.like("user_name", "a").between("age", 20, 30).isNotNull("email");
//==> Preparing: SELECT user_id,user_name AS name,age,email,is_deleted FROM user WHERE is_deleted=0 AND (user_name LIKE ? AND age BETWEEN ? AND ? AND email IS NOT NULL)
//==> Parameters: %a%(String), 20(Integer), 30(Integer)
List<User> users = userMapper.selectList(wrapper);
users.forEach(System.out::println);
}
@Test
void test2() {//orderByDesc()、orderByAsc()
QueryWrapper<User> wrapper = new QueryWrapper<>();
//查询,按照年龄降序,年龄相同按照id升序。
wrapper.orderByDesc("age").orderByAsc("user_id");
//==> Preparing: SELECT user_id,user_name AS name,age,email,is_deleted FROM user WHERE is_deleted=0 ORDER BY age DESC,user_id ASC
List<User> users = userMapper.selectList(wrapper);
users.forEach(System.out::println);
}
@Test
void test3() {//delete
QueryWrapper<User> wrapper = new QueryWrapper<>();
//删除邮箱为null的users
wrapper.isNull("email");
//==> Preparing: UPDATE user SET is_deleted=1 WHERE is_deleted=0 AND (email IS NULL)
userMapper.delete(wrapper);
}
@Test
void test4() {//or()、gt()
QueryWrapper<User> wrapper = new QueryWrapper<>();
//将(age>20且user_name包含a)或者(email不为null)的用户修改
wrapper.gt("age", 20).like("user_name", "a")
.or()
.isNotNull("email");
//修改为name=xiaoming
User user = new User();
user.setName("xiaoming");
//==> Preparing: UPDATE user SET user_name=? WHERE is_deleted=0 AND (age > ? AND user_name LIKE ? OR email IS NOT NULL)
//==> Parameters: xiaoming(String), 20(Integer), %a%(String)
userMapper.update(user, wrapper);//(修改值实体类,被修改对象wrapper)
}
@Test
void test5() {//查询条件的优先级、and()
QueryWrapper<User> wrapper = new QueryWrapper<>();
//注意和test4的情况区分,lambda表达式的条件优先执行
//将age>20且(user_name包含a或者email不为null)的用户查找
wrapper.gt("age", 20)
.and(i -> i.like("name", "a")
.or()
.isNotNull("email")
);
//==> Preparing: SELECT id,name,age,email,is_deleted FROM user WHERE is_deleted=0 AND (age > ? AND (name LIKE ? OR email IS NOT NULL))
//==> Parameters: 20(Integer), %a%(String)
List<User> users = userMapper.selectList(wrapper);
users.forEach(System.out::println);
}
@Test
void test6() {//部分查询
QueryWrapper<User> wrapper = new QueryWrapper<>();
//部分查询,只查id和name
wrapper.select("id", "name");
//==> Preparing: SELECT id,name FROM user WHERE is_deleted=0
List<Map<String, Object>> maps = userMapper.selectMaps(wrapper);
System.out.println(maps);
}
@Test
void test7() {//UpdateWrapper<>()使用、ge()
UpdateWrapper<User> wrapper = new UpdateWrapper<>();
//修改对象:age>20
wrapper.ge("age", 20);
//修改值:name=zhangsan
wrapper.set("name", "zhangsan");
//==> Preparing: UPDATE user SET name=? WHERE is_deleted=0 AND (age >= ?)
//==> Parameters: zhangsan(String), 20(Integer)
userMapper.update(null, wrapper);
}
@Test
void test08() {//根据条件是否符合要求,组装条件进行查询、le()
String name = "";
Integer ageMin = 20;
Integer ageMax = 30;
QueryWrapper<User> wrapper = new QueryWrapper<>();
//StringUtils.isNotBlank(name):name不为null和空
wrapper.like(StringUtils.isNotBlank(name), "name", name).ge(ageMin != null, "age", ageMin).le(ageMax != null, "age", ageMax);
//==> Preparing: SELECT id,name,age,email,is_deleted FROM user WHERE is_deleted=0 AND (age >= ? AND age <= ?)
//==> Parameters: 20(Integer), 30(Integer)
List<User> users = userMapper.selectList(wrapper);
users.forEach(System.out::println);
}
@Test
void test09() {//LambdaQueryWrapper<>()
String name = "";
Integer ageMin = 20;
Integer ageMax = 30;
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
//StringUtils.isNotBlank(name):name不为null和空
wrapper.like(StringUtils.isNotBlank(name), User::getName, name)
//第二个参数非lambda情况下是要写数据库字段名的,但有可能忘记字段名
//lambda情况下直接写属性名,让它自己去映射,博主感觉很nice,不用再记属性-字段怎么对应的
.ge(ageMin != null, User::getAge, ageMin)
.le(ageMax != null, User::getAge, ageMax);
//==> Preparing: SELECT id,name,age,email,is_deleted FROM user WHERE is_deleted=0 AND (age >= ? AND age <= ?)
//==> Parameters: 20(Integer), 30(Integer)
List<User> users = userMapper.selectList(wrapper);
users.forEach(System.out::println);
}
@Test
void test10() {//LambdaUpdateWrapper<>()
LambdaUpdateWrapper<User> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(User::getId, 1);
wrapper.set(User::getEmail, "update@qq.com");
//==> Preparing: UPDATE user SET email=? WHERE is_deleted=0 AND (id = ?)
//==> Parameters: update@qq.com(String), 1(Integer)
userMapper.update(null, wrapper);
}
5 mybatisplus插件:分页、乐观锁
5.1 分页插件
首先配置分页插件,顺便把@MapperScan也从启动类上拿过来:
package com.coderhao.mybatisplus.config;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@MapperScan("com.coderhao.mybatisplus.mapper")
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor(){
MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return mybatisPlusInterceptor;
}
}
使用:
@Test
void test1(){
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
String name="a";
wrapper.like(StringUtils.isNotBlank(name),User::getName,name);
Page<User> page = new Page<>(2,3);//第2页,每页显示3条数据
//==> Preparing: SELECT COUNT(*) FROM user WHERE is_deleted = 0 AND (name LIKE ?)
//==> Parameters: %a%(String)
//==> Preparing: SELECT id,name,age,email,is_deleted FROM user WHERE is_deleted=0 AND (name LIKE ?) LIMIT ?,?
//==> Parameters: %a%(String), 3(Long), 3(Long)
//分页数据会回显到page里
userMapper.selectPage(page, wrapper);
page.getRecords().forEach(System.out::println);
}
5.2 乐观锁
作用:防止出现第一类和第二类更新丢失。(更新丢失:两个事务操作同一个数据,后提交/回滚的事务覆盖了先提交的事务。)
乐观锁使用version字段来解决该问题,提交/回滚时检查数据版本,如果更改时数据版本不是一开始取出时的版本,则该操作不生效。
悲观锁则是事务阻塞,一个事务在执行,另一个事务则不能执行。
注意:这里有一个坑!一定要先查询出这条数据,再更新,乐观锁才会生效!!!
更新丢失可以通过开两个线程,2个线程都对同一数据进行操作,看结果是否符合预期(比如一个线程把一个数据+1,循环500次,另一个-1,循环500次,看结果是不是不变)。
(1)乐观锁配置
mybatisplus的配置类里填上乐观锁组件:
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor(){
MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
mybatisPlusInterceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());//添加乐观锁组件
return mybatisPlusInterceptor;
}
(2)创建数据库表、实体类、mapper
表(因为我们这个emp_salary字段是个金融数字,所以用精确的decimal类型):
CREATE TABLE `emp` (
`emp_id` bigint NOT NULL AUTO_INCREMENT COMMENT '员工id',
`emp_salary` decimal(10,0) DEFAULT NULL COMMENT '员工工资',
`emp_version` bigint DEFAULT '0' COMMENT '乐观锁版本号',
`is_deleted` int DEFAULT '0' COMMENT '逻辑删除',
PRIMARY KEY (`emp_id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb3
填两个数据:
实体类,注意 @Version注解和BigDecimal类型:
@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("emp")
public class Emp {
@TableId(value = "emp_id",type = IdType.AUTO)
private Long empId;
@TableField("emp_salary")
private BigDecimal empSalary;
@Version//乐观锁需要的版本号
private Long empVersion;
@TableLogic
private Integer isDeleted;
}
EmpMapper:
public interface EmpMapper extends BaseMapper<Emp> {
}
(3)测试
注意BigDecimal类型的运算方式
@Test
void test2(){
LambdaQueryWrapper<Emp> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Emp::getEmpId,1L);
Emp emp = empMapper.selectOne(wrapper);
emp.setEmpSalary(emp.getEmpSalary().add(BigDecimal.valueOf(1L)));
//==> Preparing: UPDATE emp SET emp_salary=?, emp_version=? WHERE is_deleted=0 AND (emp_id = ? AND emp_version = ?)
//==> Parameters: 5001(BigDecimal), 1(Long), 1(Long), 0(Long)
empMapper.update(emp,wrapper);
}
发现版本号变了,而且更新过程中sql语句也有判断版本号的情况。
@Version说明:
支持的数据类型只有:int,Integer,long,Long,Date,Timestamp,LocalDateTime
整数类型下 newVersion = oldVersion + 1
newVersion 会回写到 entity 中
仅支持 updateById(id) 与 update(entity, wrapper) 方法
在 update(entity, wrapper) 方法下, wrapper 不能复用!!!
6 通用枚举:@EnumValue+枚举包扫描
数据库表中有很多字段的值是固定的几个里面选一个:比如性别(男女选一)。
这里还是以emp表为例,加个emp_sex:
搞个枚举类,注意@EnumValue,这个标注的是真正往数据库里放的值:
package com.coderhao.mybatisplus.enums;
import com.baomidou.mybatisplus.annotation.EnumValue;
import lombok.AllArgsConstructor;
import lombok.Getter;
@AllArgsConstructor
@Getter
public enum SexEnum {
//设置枚举的内容
MALE(1,"男"),
FEMALE(0,"女");
//枚举的属性
@EnumValue
private Integer sex;
private String sexName;
}
Emp,加个枚举类型的属性:
@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("emp")
public class Emp {
@TableId(value = "emp_id",type = IdType.AUTO)
private Long empId;
@TableField("emp_salary")
private BigDecimal empSalary;
@Version//乐观锁需要的版本号
private Long empVersion;
@TableLogic
private Integer isDeleted;
@TableField("emp_sex")
private SexEnum empSex;
}
yaml里面枚举包扫描:
mybatis-plus:
# 扫描通用枚举包
type-enums-package: com.coderhao.mybatisplus.enums
测试:
@Test
void test3(){
Emp emp = new Emp(null,BigDecimal.valueOf(4000L),null,null, SexEnum.MALE);
empMapper.insert(emp);
}
有数据,枚举类型填了相应的值,而且其他字段也有默认值。
7 代码生成器
mybatisplus的代码生成器可以由表生成entity,controller,service,mapper,mapper.xml
虽然这里讲代码生成器,但博主不建议用代码生成器,MP的功能已经很强大了,没必要代码生成,何况生成的controller,service层可能也不满意,实体类也缺逻辑删除和乐观锁的注解。
(1)创建案例表
CREATE TABLE `pre_table_name_one` (
`t_id` bigint NOT NULL AUTO_INCREMENT,
`t_name` varchar(30) DEFAULT NULL,
`t_date` datetime DEFAULT NULL,
`t_salary` decimal(10,0) DEFAULT NULL,
`t_is_deleted` int DEFAULT '0',
`t_version` bigint DEFAULT '0',
PRIMARY KEY (`t_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3
搞两个数据:
(2)引入依赖,配置代码生成器
<!--代码生成器依赖包-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.5.1</version>
</dependency>
<!--模板引擎-->
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
<version>2.3.31</version>
</dependency>
<!--生成的controller层要这个包依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
代码生成器配置:
@SpringBootTest
public class MPG {
@Value("${spring.datasource.url}")
private String url;
@Value("${spring.datasource.username}")
private String username;
@Value("${spring.datasource.password}")
private String password;
@Test
void test1() {
FastAutoGenerator.create(url, username, password)
//全局配置
.globalConfig(builder -> {
builder.author("coderhao") // 设置作者
// .enableSwagger() // 开启 swagger 模式
.fileOverride() // 覆盖已生成文件
.outputDir("F://project//learn//boot-mybatisplus//src//main//java//"); // 指定输出目录
})
//包配置
.packageConfig(builder -> {
builder.parent("com.coderhao") // 设置父包名
.moduleName("mybatisplus") // 设置父包模块名
.pathInfo(Collections.singletonMap(OutputFile.mapperXml, "F://project//learn//boot-mybatisplus//src//main//resources//mapper//tableName//")); // 设置mapperXml生成路径
})
//策略配置
.strategyConfig(builder -> {
builder.addInclude("pre_table_name_one") // 设置需要生成的表名
.addTablePrefix("pre_", "c_"); // 设置过滤表前缀
})
.templateEngine(new FreemarkerTemplateEngine()) // 使用Freemarker引擎模板,默认的是Velocity引擎模板
.execute();
}
}
- java代码最终路径是:
outputDir+parent+moduleName,在这个路径下面是controller/serivce/entity/mapper等包 - mapper.xml路径:
pathInfo那个里面的路径
注:可以看到生成的东西:
其中,实体类没有逻辑删除和乐观锁的注解。
(3)测试
有个小bug,实体类中用到了LocalDateTime类型,查询时数据转换为实体类时报错,把druid的版本弄成1.1.22就行了。
@Autowired
private ITableNameOneService tableNameOneService;
@Autowired
private TableNameOneMapper tableNameOneMapper;
@Test
void test2() {
List<TableNameOne> list = tableNameOneService.list();
list.forEach(System.out::println);
}
@Test
void test3() {
TableNameOne tableNameOne = new TableNameOne();
tableNameOne.settName("name3");
tableNameOneMapper.insert(tableNameOne);
}
8 多数据源管理
(1)创建案例库/表
数据表很可能分布在多个数据库中,就需要多数据源管理。
读写分离就是在保持主从库的数据一致性的情况下,主库写,从库读。
创建一个新库叫testdb2,创建一个同样的数据表emp,加两条数据:
CREATE TABLE `emp` (
`emp_id` bigint NOT NULL AUTO_INCREMENT COMMENT '员工id',
`emp_salary` decimal(10,0) DEFAULT NULL COMMENT '员工工资',
`emp_version` bigint DEFAULT '0' COMMENT '乐观锁版本号',
`is_deleted` int DEFAULT '0' COMMENT '逻辑删除',
`emp_sex` int DEFAULT '0' COMMENT '性别,只有0/1',
PRIMARY KEY (`emp_id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb3
(2)引入依赖,配置yaml
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
<version>3.5.0</version>
</dependency>
yaml数据源改成这样:
spring:
datasource:
dynamic:
primary: master #设置默认的数据源或者数据源组,默认值即为master
strict: false #严格匹配数据源,默认false. true未匹配到指定数据源时抛异常,false使用默认数据源
datasource:
master:
url: jdbc:mysql://localhost:3306/testdb?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&nullCatalogMeansCurrent=true
username: root
password: password
driver-class-name: com.mysql.cj.jdbc.Driver # 3.2.0开始支持SPI可省略此配置
slave_1:
url: jdbc:mysql://localhost:3306/testdb2?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&nullCatalogMeansCurrent=true
username: root
password: password
driver-class-name: com.mysql.cj.jdbc.Driver
#以上会配置一个默认库master,一个组slave下有1个子库slave_1
(3)@DS切换数据库
package com.coderhao.mybatisplus.service.impl;
import com.baomidou.dynamic.datasource.annotation.DS;
import com.coderhao.mybatisplus.mapper.EmpMapper;
import com.coderhao.mybatisplus.pojo.Emp;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class MDSimpl {
@Autowired
private EmpMapper empMapper;
//添加@DS注解到实现类或者实现类的方法上才可以,同时存在就近原则 方法上注解 优先于 类上注解,测试类发现不太行,不知道为啥
//不加@DS就是默认数据库
@DS("master")
public void f1() {
List<Emp> emps = empMapper.selectList(null);
emps.forEach(System.out::println);
}
//@DS("slave_1")
@DS("slave")//组名或者具体库名都行
public void f2() {
List<Emp> emps = empMapper.selectList(null);
emps.forEach(System.out::println);
}
}
(4)测试
@Autowired
private MDSimpl mdSimpl;
@Test
void test1(){
mdSimpl.f1();
mdSimpl.f2();
}
9 MybatisX插件
idea平台独有的插件,可以解决一些复杂sql、多表联查问题。
这个插件的功能博主写这篇文章的时候觉得还行。
9.1 安装插件
安装后重启idea。
9.2 功能描述
(1)xml与mapper的跳转。
点击小鸟实现跳转。
(2)代码生成器
- idea连上数据库:
- 代码生成器配置
先把之前MPG代码生成的删除,重新用这个生成。
生成的代码文件,没有controller层:
- 补充实体类注解
package com.coderhao.mybatisplus.pojo;
import com.baomidou.mybatisplus.annotation.*;
import java.io.Serializable;
import java.util.Date;
import lombok.Data;
@TableName(value ="pre_table_name_one")
@Data
public class TableNameOne implements Serializable {
@TableId(value = "t_id",type = IdType.AUTO)
private Long id;
@TableField("t_name")
private String name;
@TableField("t_date")
private Date date;
@TableField("t_salary")
private Integer salary;
@TableField("t_is_deleted")
@TableLogic
private Integer isDeleted;
@TableField("t_version")
@Version
private Long version;
@TableField(exist = false)
//博主推测这个设置代替数据库中的版本字段设置初始值
private static final long serialVersionUID = 1L;
}
- 测试
@Autowired
private TableNameOneService tableNameOneService;
@Autowired
private TableNameOneMapper tableNameOneMapper;
@Test
void test2() {
List<TableNameOne> list = tableNameOneService.list();
list.forEach(System.out::println);
}
@Test
void test3() {
TableNameOne tableNameOne = new TableNameOne();
tableNameOne.setName("name4");
tableNameOneMapper.insert(tableNameOne);
}
@Test
void test4(){
tableNameOneMapper.deleteById(1);
test2();
}
@Test
void test5(){
LambdaQueryWrapper<TableNameOne> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(TableNameOne::getId,2);
TableNameOne tableNameOne = tableNameOneMapper.selectOne(wrapper);
tableNameOne.setSalary(4444);
tableNameOneMapper.update(tableNameOne,wrapper);
}
(3)快速生成crud
可以根据mapper的方法快速生成crud。
这里用选择性的条件添加来做例子。
可以看到方法巨多,而且都挺常用的,生成一些复杂的也不在话下,经典前面白学:
增删改查例子,根据提示来写,易上手,太强了:
public interface TableNameOneMapper extends BaseMapper<TableNameOne> {
//添加
int insertSelective(TableNameOne tableNameOne);
//按照id在区间内查找
List<TableNameOne> selectAllByIdBetween(@Param("beginId") Long beginId, @Param("endId") Long endId);
//更新name,salary(按照salary在区间内和时间等于某值)
int updateNameAndSalaryBySalaryBetweenAndDate(@Param("name") String name, @Param("salary") Integer salary, @Param("beginSalary") Integer beginSalary, @Param("endSalary") Integer endSalary, @Param("date") Date date);
//删除从某个时候之后的
int deleteByDateAfter(@Param("date") Date date);
}