SpringBoot之mybatis-plus实战
一、概述
1,综述
mybatis-plus(简称MP)是一个 Mybatis 的增强工具,在 Mybatis 的基础上只做增强不做改变,为简化开发、提高效率而生。
2、特性
- 无侵入:只做增强不做改变,引入它不会对现有工程产生影响
- 损耗小:启动即会自动注入基本 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 语句以及其执行时间,建议开发测试时启用该功能,能快速揪出慢查询
- 内置全局拦截插件:提供全表 delete 、 update 操作智能分析阻断,也可自定义拦截规则,预防误操作
二、实战
准备
全新的 MyBatis-Plus
3.0 版本基于 JDK8,提供了 lambda
形式的调用,所以安装集成 MP3.0 要求 JDK 8+
创建一个springboot 项目,引入springboot依赖
mysql 数据库,新建一张表
脚本如下
DROP TABLE IF EXISTS user;
CREATE TABLE user
(
id BIGINT(20) NOT NULL COMMENT '主键ID',
name VARCHAR(30) NULL DEFAULT NULL COMMENT '姓名',
age INT(11) NULL DEFAULT NULL COMMENT '年龄',
email VARCHAR(50) NULL DEFAULT NULL COMMENT '邮箱',
PRIMARY KEY (id)
);
DELETE FROM user;
INSERT INTO user (id, name, age, email) VALUES
(1, 'Jone', 18, 'test1@baomidou.com'),
(2, 'Jack', 20, 'test2@baomidou.com'),
(3, 'Tom', 28, 'test3@baomidou.com'),
(4, 'Sandy', 21, 'test4@baomidou.com'),
(5, 'Billie', 24, 'test5@baomidou.com');
1、核心依赖库
<dependencies>
<!--mybatis -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.3.1</version>
</dependency>
<!--数据库-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
</dependencies>
2、配置
1、 最基本的配置
在 application.yml
配置文件中添加 mysql 数据库的相关配置:
spring:
datasource:
url: jdbc:mysql://localhost:3306/test
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver
#开启大小写转换
mybatis:
configuration:
map-underscore-to-camel-case: true
server:
port: 8081
3、启动类
在 Spring Boot 启动类中添加 @MapperScan
注解,扫描 Mapper 文件夹:
@SpringBootApplication
@MapperScan("com.baomidou.mybatisplus.samples.quickstart.mapper")
public class Application {
public static void main(String[] args) {
SpringApplication.run(QuickStartApplication.class, args);
}
}
4、核心代码
编写实体类 User.java
@Data
public class User {
private Long id;
private String name;
private Integer age;
private String email;
}
编写Mapper类 UserMapper.java
public interface UserMapper extends BaseMapper<User> {
}
添加测试类,进行功能测试:
@SpringBootTest
public class SampleTest {
@Autowired
private UserMapper userMapper;
@Test
public void testSelect() {
System.out.println(("----- selectAll method test ------"));
List<User> userList = userMapper.selectList(null);
Assert.assertEquals(5, userList.size());
userList.forEach(System.out::println);
}
}
以上几个简单的步骤,实现了 User 表的 CRUD 功能,甚至连 XML 文件都不用编写!
从以上步骤中,我们可以看到集成MyBatis-Plus
非常的简单,只需要引入 starter 工程,并配置 mapper 扫描路径即可。
三、代码生成器
AutoGenerator 是 MyBatis-Plus 的代码生成器,通过 AutoGenerator 可以快速生成 Entity、Mapper、Mapper XML、Service、Controller 等各个模块的代码,极大的提升了开发效率。
使用教程
添加依赖
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.4.1</version>
</dependency>
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity-engine-core</artifactId>
<version>2.3</version>
</dependency>
MyBatis-Plus 支持 Velocity(默认)、Freemarker、Beetl,用户可以选择自己熟悉的模板引擎,如果都不满足您的要求,可以采用自定义模板引擎。
演示例子
import com.baomidou.mybatisplus.core.exceptions.MybatisPlusException;
import com.baomidou.mybatisplus.generator.AutoGenerator;
import com.baomidou.mybatisplus.generator.InjectionConfig;
import com.baomidou.mybatisplus.generator.config.*;
import com.baomidou.mybatisplus.generator.config.po.TableInfo;
import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;
import com.baomidou.mybatisplus.generator.engine.FreemarkerTemplateEngine;
import org.apache.commons.lang3.StringUtils;
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;
/**
* 代码生成器
*
* @Author Lizhou
*/
public class CodeGenerator {
/**
* <p>
* 读取控制台内容
* </p>
*/
public static String scanner(String tip) {
Scanner scanner = new Scanner(System.in);
StringBuilder help = new StringBuilder();
help.append("请输入" + tip + ":");
System.out.println(help.toString());
if (scanner.hasNext()) {
String ipt = scanner.next();
if (StringUtils.isNotEmpty(ipt)) {
return ipt;
}
}
throw new MybatisPlusException("请输入正确的" + tip + "!");
}
/**
* 自动生成代码
*/
public static void main(String[] args) {
// 代码生成器
AutoGenerator mpg = new AutoGenerator();
// TODO 全局配置
GlobalConfig gc = new GlobalConfig();
String projectPath = System.getProperty("user.dir");
// 生成文件的输出目录【默认 D 盘根目录】
gc.setOutputDir(projectPath + "/src/main/java");
// 作者
gc.setAuthor("lizhou");
// 是否打开输出目录
gc.setOpen(false);
// controller 命名方式,注意 %s 会自动填充表实体属性
gc.setControllerName("%sController");
// service 命名方式
gc.setServiceName("%sService");
// serviceImpl 命名方式
gc.setServiceImplName("%sServiceImpl");
// mapper 命名方式
gc.setMapperName("%sMapper");
// xml 命名方式
gc.setXmlName("%sMapper");
// 开启 swagger2 模式
gc.setSwagger2(true);
// 是否覆盖已有文件
gc.setFileOverride(true);
// 是否开启 ActiveRecord 模式
gc.setActiveRecord(true);
// 是否在xml中添加二级缓存配置
gc.setEnableCache(false);
// 是否开启 BaseResultMap
gc.setBaseResultMap(true);
// XML columList
gc.setBaseColumnList(false);
// 全局 相关配置
mpg.setGlobalConfig(gc);
// TODO 数据源配置
DataSourceConfig dsc = new DataSourceConfig();
dsc.setUrl("jdbc:mysql://127.0.0.1:3306/sbm?useUnicode=true&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTC&useSSL=true&characterEncoding=UTF-8");
dsc.setDriverName("com.mysql.jdbc.Driver");
dsc.setUsername("root");
dsc.setPassword("123456");
mpg.setDataSource(dsc);
// TODO 包配置
PackageConfig pc = new PackageConfig();
// 父包名。如果为空,将下面子包名必须写全部, 否则就只需写子包名
pc.setParent("com.zyxx.sbm");
// Entity包名
pc.setEntity("entity");
// Service包名
pc.setService("service");
// Service Impl包名
pc.setServiceImpl("service.impl");
mpg.setPackageInfo(pc);
// TODO 自定义配置
InjectionConfig cfg = new InjectionConfig() {
@Override
public void initMap() {
// to do nothing
}
};
// 输出文件配置
List<FileOutConfig> focList = new ArrayList<>();
focList.add(new FileOutConfig("/templates/mapper.xml.ftl") {
@Override
public String outputFile(TableInfo tableInfo) {
// 自定义输入文件名称
return projectPath + "/src/main/resources/mapper/" + tableInfo.getEntityName() + "Mapper.xml";
}
});
// 自定义输出文件
cfg.setFileOutConfigList(focList);
mpg.setCfg(cfg);
mpg.setTemplate(new TemplateConfig().setXml(null));
// TODO 策略配置
StrategyConfig strategy = new StrategyConfig();
// 数据库表映射到实体的命名策略,驼峰原则
strategy.setNaming(NamingStrategy.underline_to_camel);
// 字数据库表字段映射到实体的命名策略,驼峰原则
strategy.setColumnNaming(NamingStrategy.underline_to_camel);
// 实体是否生成 serialVersionUID
strategy.setEntitySerialVersionUID(false);
// 是否生成实体时,生成字段注解
strategy.setEntityTableFieldAnnotationEnable(true);
// 使用lombok
strategy.setEntityLombokModel(true);
// 设置逻辑删除键
strategy.setLogicDeleteFieldName("del_flag");
// TODO 指定生成的bean的数据库表名
strategy.setInclude(scanner("表名,多个英文逗号分割").split(","));
// 驼峰转连字符
strategy.setControllerMappingHyphenStyle(true);
mpg.setStrategy(strategy);
// 选择 freemarker 引擎需要指定如下加,注意 pom 依赖必须有!
mpg.setTemplateEngine(new FreemarkerTemplateEngine());
mpg.execute();
}
}
我们需要改变的就是:
- 数据库的连接信息,地址,用户,密码
- controller、service、mapper、xml 等文件的命名规则
- 生成文件 controller、service、mapper、xml 文件的存放位置
- 数据库与实体类之间的一些策略配置等
自定义代码模板
Controller.java.vm
package ${package}.${moduleName}.controller;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import ${package}.${moduleName}.entity.${className};
import ${package}.${moduleName}.service.${className}Service;
import org.springframework.security.access.prepost.PreAuthorize;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
/**
* ${comments}
*
* @author ${author}
* @date ${datetime}
*/
@RestController
@RequiredArgsConstructor
@RequestMapping("/${pathName}" )
@Api(value = "${pathName}", tags = "${comments}管理")
public class ${className}Controller {
private final ${className}Service ${classname}Service;
/**
* 分页查询
* @param page 分页对象
* @param ${classname} ${comments}
* @return
*/
@ApiOperation(value = "分页查询", notes = "分页查询")
@GetMapping("/page" )
@PreAuthorize("@pms.hasPermission('${moduleName}_${pathName}_get')" )
public R get${className}Page(Page page, ${className} ${classname}) {
return R.ok(${classname}Service.page(page, Wrappers.query(${classname})));
}
/**
* 通过id查询${comments}
* @param ${pk.lowerAttrName} id
* @return R
*/
@ApiOperation(value = "通过id查询", notes = "通过id查询")
@GetMapping("/{${pk.lowerAttrName}}" )
@PreAuthorize("@pms.hasPermission('${moduleName}_${pathName}_get')" )
public R getById(@PathVariable("${pk.lowerAttrName}" ) ${pk.attrType} ${pk.lowerAttrName}) {
return R.ok(${classname}Service.getById(${pk.lowerAttrName}));
}
/**
* 新增${comments}
* @param ${classname} ${comments}
* @return R
*/
@ApiOperation(value = "新增${comments}", notes = "新增${comments}")
@SysLog("新增${comments}" )
@PostMapping
@PreAuthorize("@pms.hasPermission('${moduleName}_${pathName}_add')" )
public R save(@RequestBody ${className} ${classname}) {
return R.ok(${classname}Service.save(${classname}));
}
/**
* 修改${comments}
* @param ${classname} ${comments}
* @return R
*/
@ApiOperation(value = "修改${comments}", notes = "修改${comments}")
@SysLog("修改${comments}" )
@PutMapping
@PreAuthorize("@pms.hasPermission('${moduleName}_${pathName}_edit')" )
public R updateById(@RequestBody ${className} ${classname}) {
return R.ok(${classname}Service.updateById(${classname}));
}
/**
* 通过id删除${comments}
* @param ${pk.lowerAttrName} id
* @return R
*/
@ApiOperation(value = "通过id删除${comments}", notes = "通过id删除${comments}")
@SysLog("通过id删除${comments}" )
@DeleteMapping("/{${pk.lowerAttrName}}" )
@PreAuthorize("@pms.hasPermission('${moduleName}_${pathName}_del')" )
public R removeById(@PathVariable ${pk.attrType} ${pk.lowerAttrName}) {
return R.ok(${classname}Service.removeById(${pk.lowerAttrName}));
}
}
Entity.java.vm
package ${package}.${moduleName}.entity;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.extension.activerecord.Model;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
#if(${hasBigDecimal})
import java.math.BigDecimal;
#end
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* ${comments}
*
* @author ${author}
* @date ${datetime}
*/
@Data
@TableName("${tableName}")
@EqualsAndHashCode(callSuper = true)
@ApiModel(value = "${comments}")
public class ${className} extends Model<${className}> {
private static final long serialVersionUID = 1L;
#foreach ($column in $columns)
/**
* $column.comments
*/
#if($column.columnName == $pk.columnName)
@TableId
#end
@ApiModelProperty(value="$column.comments"#if($column.hidden),hidden=$column.hidden#end)
private $column.attrType $column.lowerAttrName;
#end
}
package ${package}.${moduleName}.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import ${package}.${moduleName}.entity.${className};
import org.apache.ibatis.annotations.Mapper;
/**
* ${comments}
*
* @author ${author}
* @date ${datetime}
*/
@Mapper
public interface ${className}Mapper extends BaseMapper<${className}> {
}
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="${package}.${moduleName}.mapper.${className}Mapper">
<resultMap id="${classname}Map" type="${package}.${moduleName}.entity.${className}">
#foreach($column in $columns)
#if($column.lowerAttrName==$pk.lowerAttrName)
<id property="${pk.lowerAttrName}" column="${pk.columnName}"/>
#else
<result property="${column.lowerAttrName}" column="${column.columnName}"/>
#end
#end
</resultMap>
</mapper>
package ${package}.${moduleName}.service;
import com.baomidou.mybatisplus.extension.service.IService;
import ${package}.${moduleName}.entity.${className};
/**
* ${comments}
*
* @author ${author}
* @date ${datetime}
*/
public interface ${className}Service extends IService<${className}> {
}
package ${package}.${moduleName}.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import ${package}.${moduleName}.entity.${className};
import ${package}.${moduleName}.mapper.${className}Mapper;
import ${package}.${moduleName}.service.${className}Service;
import org.springframework.stereotype.Service;
/**
* ${comments}
*
* @author ${author}
* @date ${datetime}
*/
@Service
public class ${className}ServiceImpl extends ServiceImpl<${className}Mapper, ${className}> implements ${className}Service {
}
四,基类CRUD介绍
注解
mp一共提供了8个注解,这些注解是用在Java的实体类上面的。
-
@TableName
注解在类上,指定类和数据库表的映射关系。实体类的类名(转成小写后)和数据库表名相同时,可以不指定该注解。 -
@TableId
注解在实体类的某一字段上,表示这个字段对应数据库表的主键。当主键名为id时(表中列名为id,实体类中字段名为id),无需使用该注解显式指定主键,mp会自动关联。若类的字段名和表的列名不一致,可用value
属性指定表的列名。另,这个注解有个重要的属性type
,用于指定主键策略 -
@TableField
注解在某一字段上,指定Java实体类的字段和数据库表的列的映射关系。这个注解有如下几个应用场景。 -
- 排除非表字段
若Java实体类中某个字段,不对应表中的任何列,它只是用于保存一些额外的,或组装后的数据,则可以设置exist
属性为false
,这样在对实体对象进行插入时,会忽略这个字段。排除非表字段也可以通过其他方式完成,如使用static
或transient
关键字,但个人觉得不是很合理,不做赘述 - 字段验证策略
通过insertStrategy
,updateStrategy
,whereStrategy
属性进行配置,可以控制在实体对象进行插入,更新,或作为WHERE条件时,对象中的字段要如何组装到SQL语句中。 - 字段填充策略
通过fill
属性指定,字段为空时会进行自动填充
- 排除非表字段
-
@Version
乐观锁注解 -
@EnumValue
注解在枚举字段上 -
@TableLogic
逻辑删除, -
KeySequence
序列主键策略(oracle
) -
InterceptorIgnore
插件过滤规则
CRUD 接口
Service CRUD 接口
Save
// 插入一条记录(选择字段,策略插入)
boolean save(T entity);
// 插入(批量)
boolean saveBatch(Collection<T> entityList);
// 插入(批量)
boolean saveBatch(Collection<T> entityList, int batchSize);
参数说明
类型 | 参数名 | 描述 |
---|---|---|
T | entity | 实体对象 |
Collection | entityList | 实体对象集合 |
int | batchSize | 插入批次数量 |
SaveOrUpdate
// TableId 注解存在更新记录,否插入一条记录
boolean saveOrUpdate(T entity);
// 根据updateWrapper尝试更新,否继续执行saveOrUpdate(T)方法
boolean saveOrUpdate(T entity, Wrapper<T> updateWrapper);
// 批量修改插入
boolean saveOrUpdateBatch(Collection<T> entityList);
// 批量修改插入
boolean saveOrUpdateBatch(Collection<T> entityList, int batchSize);
参数说明
类型 | 参数名 | 描述 |
---|---|---|
T | entity | 实体对象 |
Wrapper | updateWrapper | 实体对象封装操作类 UpdateWrapper |
Collection | entityList | 实体对象集合 |
int | batchSize | 插入批次数量 |
Remove
// 根据 entity 条件,删除记录
boolean remove(Wrapper<T> queryWrapper);
// 根据 ID 删除
boolean removeById(Serializable id);
// 根据 columnMap 条件,删除记录
boolean removeByMap(Map<String, Object> columnMap);
// 删除(根据ID 批量删除)
boolean removeByIds(Collection<? extends Serializable> idList);
参数说明
类型 | 参数名 | 描述 |
---|---|---|
Wrapper | queryWrapper | 实体包装类 QueryWrapper |
Serializable | id | 主键ID |
Map<String, Object> | columnMap | 表字段 map 对象 |
Collection<? extends Serializable> | idList | 主键ID列表 |
Update
// 根据 UpdateWrapper 条件,更新记录 需要设置sqlset
boolean update(Wrapper<T> updateWrapper);
// 根据 whereWrapper 条件,更新记录
boolean update(T updateEntity, Wrapper<T> whereWrapper);
// 根据 ID 选择修改
boolean updateById(T entity);
// 根据ID 批量更新
boolean updateBatchById(Collection<T> entityList);
// 根据ID 批量更新
boolean updateBatchById(Collection<T> entityList, int batchSize);
参数说明
类型 | 参数名 | 描述 |
---|---|---|
Wrapper | updateWrapper | 实体对象封装操作类 UpdateWrapper |
T | entity | 实体对象 |
Collection | entityList | 实体对象集合 |
int | batchSize | 更新批次数量 |
修改参数丢失(更新部分数据时)
注意:基本数据类型默认值的存在导致属性参与 sql 拼接,引起参数丢失问题
参数实体属性值如果为 null,不参与 sql 拼接;
如果参数实体属性类型是 8 大基本数据类型,有不为 null 的默认值,mybatis-plus 认为有属性值,参数参与 sql 拼接;
解决:
方案一:将基本数据类型属性改为包装类型;
方案二:1> 先查询得到整条数据;2> 修改该条数据上想要修改的字段;3> 更新整条数据;
方案三:使用 update(null,wrapper)方法操作,部分字段更新方法;
updateById(entry) 跟 update(null, wrapper) 怎么选择?
updateById(entry)
1> sql 的 where 条件是 id 时使用;
2> 全部字段更新时使用;
update(null, wrapper)
1> sql 的 where 条件不一定是 id 时使用,例如 eg:where age > 19;
2> 部分字段更新时使用;
Get
// 根据 ID 查询
T getById(Serializable id);
// 根据 Wrapper,查询一条记录。结果集,如果是多个会抛出异常,随机取一条加上限制条件 wrapper.last("LIMIT 1")
T getOne(Wrapper<T> queryWrapper);
// 根据 Wrapper,查询一条记录
T getOne(Wrapper<T> queryWrapper, boolean throwEx);
// 根据 Wrapper,查询一条记录
Map<String, Object> getMap(Wrapper<T> queryWrapper);
// 根据 Wrapper,查询一条记录
<V> V getObj(Wrapper<T> queryWrapper, Function<? super Object, V> mapper);
参数说明
类型 | 参数名 | 描述 |
---|---|---|
Serializable | id | 主键ID |
Wrapper | queryWrapper | 实体对象封装操作类 QueryWrapper |
boolean | throwEx | 有多个 result 是否抛出异常 |
T | entity | 实体对象 |
Function<? super Object, V> | mapper | 转换函数 |
List
// 查询所有
List<T> list();
// 查询列表
List<T> list(Wrapper<T> queryWrapper);
// 查询(根据ID 批量查询)
Collection<T> listByIds(Collection<? extends Serializable> idList);
// 查询(根据 columnMap 条件)
Collection<T> listByMap(Map<String, Object> columnMap);
// 查询所有列表
List<Map<String, Object>> listMaps();
// 查询列表
List<Map<String, Object>> listMaps(Wrapper<T> queryWrapper);
// 查询全部记录
List<Object> listObjs();
// 查询全部记录
<V> List<V> listObjs(Function<? super Object, V> mapper);
// 根据 Wrapper 条件,查询全部记录
List<Object> listObjs(Wrapper<T> queryWrapper);
// 根据 Wrapper 条件,查询全部记录
<V> List<V> listObjs(Wrapper<T> queryWrapper, Function<? super Object, V> mapper);
参数说明
类型 | 参数名 | 描述 |
---|---|---|
Wrapper | queryWrapper | 实体对象封装操作类 QueryWrapper |
Collection<? extends Serializable> | idList | 主键ID列表 |
Map<?String, Object> | columnMap | 表字段 map 对象 |
Function<? super Object, V> | mapper | 转换函数 |
Page
// 无条件分页查询
IPage<T> page(IPage<T> page);
// 条件分页查询
IPage<T> page(IPage<T> page, Wrapper<T> queryWrapper);
// 无条件分页查询
IPage<Map<String, Object>> pageMaps(IPage<T> page);
// 条件分页查询
IPage<Map<String, Object>> pageMaps(IPage<T> page, Wrapper<T> queryWrapper);
参数说明
类型 | 参数名 | 描述 |
---|---|---|
IPage | page | 翻页对象 |
Wrapper | queryWrapper | 实体对象封装操作类 QueryWrapper |
Count
// 查询总记录数
int count();
// 根据 Wrapper 条件,查询总记录数
int count(Wrapper<T> queryWrapper);
#参数说明
类型 | 参数名 | 描述 |
---|---|---|
Wrapper | queryWrapper | 实体对象封装操作类 QueryWrapper |
Chain
query
// 链式查询 普通
QueryChainWrapper<T> query();
// 链式查询 lambda 式。注意:不支持 Kotlin
LambdaQueryChainWrapper<T> lambdaQuery();
// 示例:
query().eq("column", value).one();
lambdaQuery().eq(Entity::getId, value).list();
update
// 链式更改 普通
UpdateChainWrapper<T> update();
// 链式更改 lambda 式。注意:不支持 Kotlin
LambdaUpdateChainWrapper<T> lambdaUpdate();
// 示例:
update().eq("column", value).remove();
lambdaUpdate().eq(Entity::getId, value).update(entity);
Mapper CRUD 接口
说明:
- 通用 CRUD 封装接口,为
Mybatis-Plus
启动时自动解析实体表关系映射转换为Mybatis
内部对象注入容器 - 泛型
T
为任意实体对象 - 参数
Serializable
为任意类型主键Mybatis-Plus
不推荐使用复合主键约定每一张表都有自己的唯一id
主键 - 对象
Wrapper
Insert
// 插入一条记录
int insert(T entity);
参数说明
类型 | 参数名 | 描述 |
---|---|---|
T | entity | 实体对象 |
Delete
// 根据 entity 条件,删除记录
int delete(@Param(Constants.WRAPPER) Wrapper<T> wrapper);
// 删除(根据ID 批量删除)
int deleteBatchIds(@Param(Constants.COLLECTION) Collection<? extends Serializable> idList);
// 根据 ID 删除
int deleteById(Serializable id);
// 根据 columnMap 条件,删除记录
int deleteByMap(@Param(Constants.COLUMN_MAP) Map<String, Object> columnMap);
参数说明
类型 | 参数名 | 描述 |
---|---|---|
Wrapper | wrapper | 实体对象封装操作类(可以为 null) |
Collection<? extends Serializable> | idList | 主键ID列表(不能为 null 以及 empty) |
Serializable | id | 主键ID |
Map<String, Object> | columnMap | 表字段 map 对象 |
Update
// 根据 whereWrapper 条件,更新记录
int update(@Param(Constants.ENTITY) T updateEntity, @Param(Constants.WRAPPER) Wrapper<T> whereWrapper);
// 根据 ID 修改
int updateById(@Param(Constants.ENTITY) T entity);
参数说明
类型 | 参数名 | 描述 |
---|---|---|
T | entity | 实体对象 (set 条件值,可为 null) |
Wrapper | updateWrapper | 实体对象封装操作类(可以为 null,里面的 entity 用于生成 where 语句) |
Select
// 根据 ID 查询
T selectById(Serializable id);
// 根据 entity 条件,查询一条记录
T selectOne(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
// 查询(根据ID 批量查询)
List<T> selectBatchIds(@Param(Constants.COLLECTION) Collection<? extends Serializable> idList);
// 根据 entity 条件,查询全部记录
List<T> selectList(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
// 查询(根据 columnMap 条件)
List<T> selectByMap(@Param(Constants.COLUMN_MAP) Map<String, Object> columnMap);
// 根据 Wrapper 条件,查询全部记录
List<Map<String, Object>> selectMaps(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
// 根据 Wrapper 条件,查询全部记录。注意: 只返回第一个字段的值
List<Object> selectObjs(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
// 根据 entity 条件,查询全部记录(并翻页)
IPage<T> selectPage(IPage<T> page, @Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
// 根据 Wrapper 条件,查询全部记录(并翻页)
IPage<Map<String, Object>> selectMapsPage(IPage<T> page, @Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
// 根据 Wrapper 条件,查询总记录数
Integer selectCount(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
参数说明
类型 | 参数名 | 描述 |
---|---|---|
Serializable | id | 主键ID |
Wrapper | queryWrapper | 实体对象封装操作类(可以为 null) |
Collection<? extends Serializable> | idList | 主键ID列表(不能为 null 以及 empty) |
Map<String, Object> | columnMap | 表字段 map 对象 |
IPage | page | 分页查询条件(可以为 RowBounds.DEFAULT) |
条件构造
AbstractWrapper
说明:
QueryWrapper(LambdaQueryWrapper) 和 UpdateWrapper(LambdaUpdateWrapper) 的父类
用于生成 sql 的 where 条件, entity 属性也用于生成 sql 的 where 条件
注意: entity 生成的 where 条件与 使用各个 api 生成的 where 条件没有任何关联行为
allEq(map里面的条件用and链接)
allEq(boolean condition, BiPredicate<R, V> filter, Map<R, V> params, boolean null2IsNull)
filter:声明可以接受两个参数的并且返回boolean类型函数式,过滤函数,是否允许字段传入比对条件中,可以用来判断传入字段是否合法
params:key为数据库字段名,value为字段值
null2IsNull:true则在map的value为null时调用isNull方法(即该字段的条件为 is null),为false则不做添加条件
condition:该条件是否加入最后生成的sql中下面对AbstractWrapper
中用于构建SQL语句中的WHERE条件的方法进行部分列举
eq
:equals,等于allEq
:all equals,全等于ne
:not equals,不等于gt
:greater than ,大于>
ge
:greater than or equals,大于等于≥
lt
:less than,小于<
le
:less than or equals,小于等于≤
between
:相当于SQL中的BETWEENnotBetween
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
in
and
:SQL连接符ANDor
:SQL连接符ORapply
:用于拼接SQL,该方法可用于数据库函数,并可以动态传参- …
使用实例
下面通过一些具体的案例来练习条件构造器的使用。(使用前文创建的user
表)
// 案例先展示需要完成的SQL语句,后展示Wrapper的写法
// 1. 名字中包含佳,且年龄小于25
// SELECT * FROM user WHERE name like '%佳%' AND age < 25
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.like("name", "佳").lt("age", 25);
List<User> users = userMapper.selectList(wrapper);
// 下面展示SQL时,仅展示WHERE条件;展示代码时, 仅展示Wrapper构建部分
// 2. 姓名为黄姓,且年龄大于等于20,小于等于40,且email字段不为空
// name like '黄%' AND age BETWEEN 20 AND 40 AND email is not null
wrapper.likeRight("name","黄").between("age", 20, 40).isNotNull("email");
// 3. 姓名为黄姓,或者年龄大于等于40,按照年龄降序排列,年龄相同则按照id升序排列
// name like '黄%' OR age >= 40 order by age desc, id asc
wrapper.likeRight("name","黄").or().ge("age",40).orderByDesc("age").orderByAsc("id");
// 4.创建日期为2021年3月22日,并且直属上级的名字为李姓
// date_format(create_time,'%Y-%m-%d') = '2021-03-22' AND manager_id IN (SELECT id FROM user WHERE name like '李%')
wrapper.apply("date_format(create_time, '%Y-%m-%d') = {0}", "2021-03-22") // 建议采用{index}这种方式动态传参, 可防止SQL注入
.inSql("manager_id", "SELECT id FROM user WHERE name like '李%'");
// 上面的apply, 也可以直接使用下面这种方式做字符串拼接,但当这个日期是一个外部参数时,这种方式有SQL注入的风险
wrapper.apply("date_format(create_time, '%Y-%m-%d') = '2021-03-22'");
// 5. 名字为王姓,并且(年龄小于40,或者邮箱不为空)
// name like '王%' AND (age < 40 OR email is not null)
wrapper.likeRight("name", "王").and(q -> q.lt("age", 40).or().isNotNull("email"));
// 6. 名字为王姓,或者(年龄小于40并且年龄大于20并且邮箱不为空)
// name like '王%' OR (age < 40 AND age > 20 AND email is not null)
wrapper.likeRight("name", "王").or(
q -> q.lt("age",40)
.gt("age",20)
.isNotNull("email")
);
// 7. (年龄小于40或者邮箱不为空) 并且名字为王姓
// (age < 40 OR email is not null) AND name like '王%'
wrapper.nested(q -> q.lt("age", 40).or().isNotNull("email"))
.likeRight("name", "王");
// 8. 年龄为30,31,34,35
// age IN (30,31,34,35)
wrapper.in("age", Arrays.asList(30,31,34,35));
// 或
wrapper.inSql("age","30,31,34,35");
// 9. 年龄为30,31,34,35, 返回满足条件的第一条记录
// age IN (30,31,34,35) LIMIT 1
wrapper.in("age", Arrays.asList(30,31,34,35)).last("LIMIT 1");
// 10. 只选出id, name 列 (QueryWrapper 特有)
// SELECT id, name FROM user;
wrapper.select("id", "name");
// 11. 选出id, name, age, email, 等同于排除 manager_id 和 create_time
// 当列特别多, 而只需要排除个别列时, 采用上面的方式可能需要写很多个列, 可以采用重载的select方法,指定需要排除的列
wrapper.select(User.class, info -> {
String columnName = info.getColumn();
return !"create_time".equals(columnName) && !"manager_id".equals(columnName);
});
分页插件
//Spring boot方式
@Configuration
@MapperScan("com.baomidou.cloud.service.*.mapper*")
public class MybatisPlusConfig {
// 旧版
@Bean
public PaginationInterceptor paginationInterceptor() {
PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
// 设置请求的页面大于最大页后操作, true调回到首页,false 继续请求 默认false
// paginationInterceptor.setOverflow(false);
// 设置最大单页限制数量,默认 500 条,-1 不受限制
// paginationInterceptor.setLimit(500);
// 开启 count 的 join 优化,只针对部分 left join
paginationInterceptor.setCountSqlParser(new JsqlParserCountOptimize(true));
return paginationInterceptor;
}
// 最新版
@Bean
/**
* 分页插件, 对于单一数据库类型来说,都建议配置该值,避免每次分页都去抓取数据库类型
*/
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
逻辑删除
新建一张新表
DROP TABLE IF EXISTS user2;
CREATE TABLE user2 (
id BIGINT(20) PRIMARY KEY NOT NULL COMMENT '主键id',
name VARCHAR(30) DEFAULT NULL COMMENT '姓名',
age INT(11) DEFAULT NULL COMMENT '年龄',
email VARCHAR(50) DEFAULT NULL COMMENT '邮箱',
manager_id BIGINT(20) DEFAULT NULL COMMENT '直属上级id',
create_time DATETIME DEFAULT NULL COMMENT '创建时间',
update_time DATETIME DEFAULT NULL COMMENT '修改时间',
version INT(11) DEFAULT '1' COMMENT '版本',
deleted INT(1) DEFAULT '0' COMMENT '逻辑删除标识,0-未删除,1-已删除',
CONSTRAINT manager_fk FOREIGN KEY(manager_id) REFERENCES user2(id)
) ENGINE = INNODB CHARSET=UTF8;
INSERT INTO user2(id, name, age, email, manager_id, create_time)
VALUES
(1, '老板', 40 ,'boss@baomidou.com' ,NULL, '2021-03-28 13:12:40'),
(2, '王狗蛋', 40 ,'gd@baomidou.com' ,1, '2021-03-28 13:12:40'),
(3, '王鸡蛋', 40 ,'jd@baomidou.com' ,2, '2021-03-28 13:12:40'),
(4, '王鸭蛋', 40 ,'yd@baomidou.com' ,2, '2021-03-28 13:12:40'),
(5, '王猪蛋', 40 ,'zd@baomidou.com' ,2, '2021-03-28 13:12:40'),
(6, '王软蛋', 40 ,'rd@baomidou.com' ,2, '2021-03-28 13:12:40'),
(7, '王铁蛋', 40 ,'td@baomidou.com' ,2, '2021-03-28 13:12:40')
首先,为什么要有逻辑删除呢?直接删掉不行吗?当然可以,但日后若想要恢复,或者需要查看这些数据,就做不到了。逻辑删除是为了方便数据恢复,和保护数据本身价值的一种方案。
日常中,我们在电脑中删除一个文件后,也仅仅是把该文件放入了回收站,日后若有需要还能进行查看或恢复。当我们确定不再需要某个文件,可以将其从回收站中彻底删除。这也是类似的道理。
mp提供的逻辑删除实现起来非常简单
只需要在application.yml
中进行逻辑删除的相关配置即可
mybatis-plus:
global-config:
db-config:
logic-delete-field: deleted # 全局逻辑删除的实体字段名
logic-delete-value: 1 # 逻辑已删除值(默认为1)
logic-not-delete-value: 0 # 逻辑未删除值(默认为0)
# 若逻辑已删除和未删除的值和默认值一样,则可以不配置这2项
删除发出的SQL不再是DELETE
,而是UPDATE
查询语句会自动在WHERE后面拼接逻辑未删除的条件
小结
开启mp的逻辑删除后,会对SQL产生如下的影响
- INSERT语句:没有影响
- SELECT语句:追加WHERE条件,过滤掉已删除的数据
- UPDATE语句:追加WHERE条件,防止更新到已删除的数据
- DELETE语句:转变为UPDATE语句
**注意,上述的影响,只针对mp自动注入的SQL生效。**如果是自己手动添加的自定义SQL,则不会生效。
自动填充
表中常常会有“新增时间”,“修改时间”,“操作人” 等字段。比较原始的方式,是每次插入或更新时,手动进行设置。mp可以通过配置,对某些字段进行自动填充
在实体类中的某些字段上,通过@TableField设置自动填充
public class User2 {
private Long id;
private String name;
private Integer age;
private String email;
private Long managerId;
@TableField(fill = FieldFill.INSERT) // 插入时自动填充
private LocalDateTime createTime;
@TableField(fill = FieldFill.UPDATE) // 更新时自动填充
private LocalDateTime updateTime;
private Integer version;
private Integer deleted;
}
实现自动填充处理器
package com.example.mp.component;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
@Component //需要注册到Spring容器中
public class MyMetaObjectHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
// 插入时自动填充
// 注意第二个参数要填写实体类中的字段名称,而不是表的列名称
strictFillStrategy(metaObject, "createTime", LocalDateTime::now);
}
@Override
public void updateFill(MetaObject metaObject) {
// 更新时自动填充
strictFillStrategy(metaObject, "updateTime", LocalDateTime::now);
}
}
五、插件
乐观锁插件
当出现并发操作时,需要确保各个用户对数据的操作不产生冲突,此时需要一种并发控制手段。悲观锁的方法是,在对数据库的一条记录进行修改时,先直接加锁(数据库的锁机制),锁定这条数据,然后再进行操作;而乐观锁,正如其名,它先假设不存在冲突情况,而在实际进行数据操作时,再检查是否冲突。乐观锁的一种通常实现是版本号,在MySQL中也有名为MVCC的基于版本号的并发事务控制。
在读多写少的场景下,乐观锁比较适用,能够减少加锁操作导致的性能开销,提高系统吞吐量。
在写多读少的场景下,悲观锁比较使用,否则会因为乐观锁不断失败重试,反而导致性能下降。
乐观锁的实现如下:
- 取出记录时,获取当前version
- 更新时,带上这个version
- 执行更新时, set version = newVersion where version = oldVersion
- 如果oldVersion与数据库中的version不一致,就更新失败
这种思想和CAS(Compare And Swap)非常相似。
乐观锁的实现步骤如下
配置乐观锁插件
package com.example.mp.config;
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MybatisPlusConfig {
/** 3.4.0以后的mp版本,推荐用如下的配置方式 **/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
return interceptor;
}
/** 旧版mp可以采用如下方式。注意新旧版本中,新版的类,名称带有Inner, 旧版的不带, 不要配错了 **/
/*
@Bean
public OptimisticLockerInterceptor opLocker() {
return new OptimisticLockerInterceptor();
}
*/
}
在实体类中表示版本的字段上添加注解@Version
@Data
public class User2 {
private Long id;
private String name;
private Integer age;
private String email;
private Long managerId;
private LocalDateTime createTime;
private LocalDateTime updateTime;
@Version
private Integer version;
private Integer deleted;
}
注意,乐观锁插件仅支持updateById(id)
与update(entity, wrapper)
方法
**注意:如果使用wrapper
,则wrapper
不能复用!
性能分析插件
该插件会输出SQL语句的执行时间,以便做SQL语句的性能分析和调优。
注:3.2.0版本之后,mp自带的性能分析插件被官方移除了,而推荐使用第三方性能分析插件
使用步骤
引入maven依赖
<dependency>
<groupId>p6spy</groupId>
<artifactId>p6spy</artifactId>
<version>3.9.1</version>
</dependency>
修改application.yml
spring:
datasource:
driver-class-name: com.p6spy.engine.spy.P6SpyDriver #换成p6spy的驱动
url: jdbc:p6spy:mysql://localhost:3306/yogurt?serverTimezone=Asia/Shanghai #url修改
username: root
password: root
- 在
src/main/resources
资源目录下添加spy.properties
#spy.properties #3.2.1以上使用 modulelist=com.baomidou.mybatisplus.extension.p6spy.MybatisPlusLogFactory,com.p6spy.engine.outage.P6OutageFactory # 真实JDBC driver , 多个以逗号分割,默认为空。由于上面设置了modulelist, 这里可以不用设置driverlist #driverlist=com.mysql.cj.jdbc.Driver # 自定义日志打印 logMessageFormat=com.baomidou.mybatisplus.extension.p6spy.P6SpyLogger #日志输出到控制台 appender=com.baomidou.mybatisplus.extension.p6spy.StdoutLogger #若要日志输出到文件, 把上面的appnder注释掉, 或者采用下面的appender, 再添加logfile配置 #不配置appender时, 默认是往文件进行输出的 #appender=com.p6spy.engine.spy.appender.FileLogger #logfile=log.log # 设置 p6spy driver 代理 deregisterdrivers=true # 取消JDBC URL前缀 useprefix=true # 配置记录 Log 例外,可去掉的结果集有error,info,batch,debug,statement,commit,rollback,result,resultset. excludecategories=info,debug,result,commit,resultset # 日期格式 dateformat=yyyy-MM-dd HH:mm:ss # 是否开启慢SQL记录 outagedetection=true # 慢SQL记录标准 2 秒 outagedetectioninterval=2 # 执行时间设置, 只有超过这个执行时间的才进行记录, 默认值0, 单位毫秒 executionThreshold=10 复制代码
随便运行一个测试用例,可以看到该SQL的执行时长被记录了下来
多数据源
配置
<!--Mybatis-Plus-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.0</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.18</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.oracle</groupId>
<artifactId>ojdbc6</artifactId>
<version>11.2.0.1.0</version>
</dependency>
<!--DS注解-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
<version>2.5.4</version>
</dependency>
spring:
autoconfigure:
# 为了某些版本的springboot @SpringBootApplication(exclude= {DataSourceAutoConfiguration.class}) 无法生效
exclude: com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure
datasource:
dynamic:
primary: master
datasource:
master:
url: jdbc:mysql://localhost:3306/xxxx?useUnicode=true&characterEncoding=utf-8&useSSL=true&serverTimezone=Asia/Shanghai
driver-class-name: com.mysql.cj.jdbc.Driver
username: xxxx
password: xxxx
local:
url: jdbc:oracle:thin:@localhost:1521:xxxx
driver-class-name: oracle.jdbc.driver.OracleDriver
username: xxxx
password: xxxx
使用方式如下:只需在service添加@Ds注解指定使用数据源即可,默认数据源无需添加。;