MybatisPlus学习笔记
1、简介
简介
MybatisPlus时Mybatis的增加工具,在Mybatis的基础上只做增强不做改变,为简化开发、提升效率而生。
特性
- 无侵入:只做增强不做改变
- 损耗小:内置通用Mapper、通用Service,仅仅通过少量配置即可实现大部分CRUD操作,更有强大的条件构造器,满足各类使用需求
- 支持Lambda调用:方便编写各类查询条件
- 支持主键自动生成:4种策略,内含分布式唯一ID生成器
- 支持ActiveRecord模式: 实体类只需继承Mocdel类即可进行强大的CRUD操作
- 支持自定义全局通用操作:支持全局通用方法注入(write once,use anywhere)
- 内置代码生成器:采用代码或者Maven插件可快速生成Mapper、Model、Servcice、Controller代码
- 内置分页插件:基于Mybatis物理分页,配置好插件后,写分页等同于list查询
- 分页插件支持多种数据库
- 内置性能分析插件:可输出SQL语句即执行时间,建议开发测试时启用
- 内置全局拦截插件:提供全表delete、update操作智能分析阻断
框架结构
快速使用
数据准备
我们将通过一个简单的 Demo 来阐述 MyBatis-Plus 的强大功能,在此之前,我们假设您已经:
- 拥有 Java 开发环境以及相应 IDE
- 熟悉 Spring Boot
- 熟悉 Maven
CREATE DATABASE `test_mpdb`;
USE test_mpdb;
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');
初始化工程
使用 Spring Initializer (opens new window)快速初始化一个 Spring Boot 工程
添加依赖
<!-- mybatis-plus启动器 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.2</version>
</dependency>
<!-- mysql 依赖 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- lombok 依赖 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
配置spring的datasouce信息
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://121.4.97.105:53306/test_mpdb?useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT
username: root
password: root
根据MybatisX插件生成基本代码
注意:点击下一步有时候插件可能会有bug,不要担心,点击上一步,然后再点一次下一步就ok了。
配置Mapper扫描
在 Spring Boot 启动类中添加 @MapperScan 注解,扫描 Mapper 文件夹:
package com.example.mptest;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
@MapperScan("com.example.mptest.mapper")
public class MptestApplication {
public static void main(String[] args) {
SpringApplication.run(MptestApplication.class, args);
}
}
** 测试 **
如果出现这种,可以继续操作,这是识别的问题。或者在Mapper上加上@Repository注解
package com.example.mptest;
import com.example.mptest.domain.User;
import com.example.mptest.mapper.UserMapper;
import org.junit.Assert;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.List;
@SpringBootTest
class MptestApplicationTests {
@Autowired
private UserMapper userMapper;
@Test
void contextLoads() {
List<User> list = userMapper.selectList(null);
Assert.assertEquals(5, list.size());
list.forEach(System.out::println);
}
}
小结
通过以上几个简单的步骤,我们就实现了 User 表的 CRUD 功能,甚至连 XML 文件都不用编写!
从以上步骤中,我们可以看到集成MyBatis-Plus非常的简单,只需要引入 starter 工程,并配置 mapper 扫描路径即可。
但 MyBatis-Plus 的强大远不止这些功能,想要详细了解 MyBatis-Plus 的强大功能?那就继续往下看吧!
MybatisPlus 常用注解
写在前面的:以下都只是一下最简单的使用,这些注解中的其他属性请参考官方文档
https://baomidou.com/pages/223848
@TableName
- 描述:表名注解,标识实体类对应的表
- 使用位置:实体类
如:
@TableName("sys_user")
public class User {
private Long id;
private String name;
private Integer age;
private String email;
}
@TableId
- 描述:字段注解(非主键)
- 使用位置:实体类主键字段
@TableName("sys_user")
public class User {
@TableId
private Long id;
private String name;
private Integer age;
private String email;
}
IdType
TableField
- 描述:字段注解(非主键)
@TableName("sys_user")
public class User {
@TableId
private Long id;
@TableField("nickname")
private String name;
private Integer age;
private String email;
}
关于
jdbcType
和typeHandler
以及numericScale
的说明:
numericScale只生效于 update 的 sql. jdbcType和typeHandler如果不配合@TableName#autoResultMap = true一起使用,也只生效于 update 的 sql. 对于typeHandler如果你的字段类型和 set 进去的类型为equals关系,则只需要让你的typeHandler让 Mybatis 加载到即可,不需要使用注解
FieldStrategy
FieldFill
@Version
- 描述:乐观锁注解、标记 @Verison 在字段上
- 使用:一般配合乐观锁插件 OptimisticLockerInnerInterceptor 使用
数据准备
create table t_product
(
t_id bigint not null,
t_desc varchar(255) null,
price bigint default 0 not null,
version int default 0 not null,
constraint t_product_pk
primary key (t_id)
)
comment '商品';
在实体类使用
使用mybatisX插件,生成相应的Service、Mapper、Domain的代码
在实体类相应字段加@Version注解
@TableName(value ="t_product")
@Data
public class Product implements Serializable {
@TableId("t_id")
private Long id;
@TableField("t_desc")
private String description;
private Long price;
private Integer version;
@TableField(exist = false)
private static final long serialVersionUID = 1L;
}
在Mybatis配置类加乐观锁拦截器
@Configuration
public class MybatisConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor(){
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
//添加乐观锁
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
return interceptor;
}
}
测试
@SpringBootTest
class OtimisticLockerInterceptorTests {
@Autowired
private ProductMapper productMapper;
@Test
void test0(){
//新增
// Product product = new Product();
// product.setDesc("xiao mi phone");
// product.setPrice(200L);
// productMapper.insert(product);
//模拟两个消费者同时操作一条数据
// consumer1
Product p1 = productMapper.selectList(null).get(0);
// consumer 2
Product p2 = productMapper.selectById(p1.getId());
// price + 100
p1.setPrice(p1.getPrice() + 100);
productMapper.updateById(p1);
//price - 50
p2.setPrice(p2.getPrice() - 50);
productMapper.updateById(p2);
//consumer 3
Product p3 = productMapper.selectById(p1.getId());
// price = ? is 250 ? or 150 or 300 ?
System.out.println(p3.getPrice()); // 300
}
}
sql 语句如下:
==> Preparing: SELECT t_id AS id,t_desc AS description,price,version FROM t_product WHERE t_id=?
==> Parameters: 1505798219102187522(Long)
<== Columns: id, description, price, version
<== Row: 1505798219102187522, xiao mi phone, 200, 0
<== Total: 1
==> Preparing: UPDATE t_product SET t_desc=?, price=?, version=? WHERE t_id=? AND version=?
==> Parameters: xiao mi phone(String), 300(Long), 1(Integer), 1505798219102187522(Long), 0(Integer)
<== Updates: 1
==> Preparing: UPDATE t_product SET t_desc=?, price=?, version=? WHERE t_id=? AND version=?
==> Parameters: xiao mi phone(String), 150(Long), 1(Integer), 1505798219102187522(Long), 0(Integer)
<== Updates: 0
==> Preparing: SELECT t_id AS id,t_desc AS description,price,version FROM t_product WHERE t_id=?
==> Parameters: 1505798219102187522(Long)
<== Columns: id, description, price, version
<== Row: 1505798219102187522, xiao mi phone, 300, 1
<== Total: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@43bdaa1b]
300
小结
乐观锁配置好后,可以发现每次修改都会在条件后面加上version=?,当修改时,version条件不满足,则不会修改。由此解决了冲突。
@EnumValue
- 描述: 普通枚举类注解(注解在枚举字段上)
- 使用: 通常需要配合mybatisplus的 type-enums-package(扫描通用枚举类)配置
数据准备
给user新增sex字段
编写枚举类
@Getter
public enum SexEnum {
MALE(1,"男"),FEMALE(2, "女");
@EnumValue # 指明枚举类的值是sex字段
private Integer sex;
private String desc;
SexEnum(Integer sex, String desc) {
this.sex = sex;
this.desc = desc;
}
}
在实体中使用
@TableName(value ="user")
@Data
public class User implements Serializable {
/**
* 主键ID
*/
@TableId
private Long id;
/**
* 姓名
*/
private String name;
/**
* 年龄
*/
private Integer age;
/**
* 邮箱
*/
private String email;
private SexEnum sex;
@TableField(exist = false)
private static final long serialVersionUID = 1L;
}
配置枚举类包扫描
mybatis-plus:
type-enums-package: com.example.mptest.enums
测试
@SpringBootTest
class MptestApplicationTests {
@Autowired
private UserMapper userMapper;
@Test
void contextLoads() {
User user = new User();
user.setName("zhangsan");
user.setEmail("zhangsan@163.com");
user.setAge(15);
user.setSex(SexEnum.MALE);
int row = userMapper.insert(user);
System.out.println(row);
System.out.println(user);
}
}
// 输出
1
User(id=1505776731460476929, name=zhangsan, age=15, email=zhangsan@163.com, sex=MALE)
@TableLogic
- 描述:表字段逻辑处理注解(逻辑删除)
数据准备
alter table user
add is_delete int(2) default 0 not null;
@TableName(value ="user")
@Data
public class User implements Serializable {
/**
* 主键ID
*/
@TableId
private Long id;
/**
* 姓名
*/
private String name;
/**
* 年龄
*/
private Integer age;
/**
* 邮箱
*/
private String email;
private SexEnum sex;
@TableLogic(value = "0", delval = "1")
private Integer is_delete;
@TableField(exist = false)
private static final long serialVersionUID = 1L;
}
测试
删除前
删除
@Test
void contextLoads() {
int row = userMapper.deleteById(1L);
System.out.println(row);
}
// SQL 如下:
==> Preparing: UPDATE user SET is_delete=1 WHERE id=? AND is_delete=0
==> Parameters: 1(Long)
<== Updates: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@10358c32]
1
删除后
@OrderBy
- 描述:内置 SQL 默认指定排序,优先级低于 wrapper 条件查询
Mybatis核心功能
代码生成器
安装
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.5.2</version>
</dependency>
注意
当前包未传递依赖 MP 包,需要自己引入!
使用
快速生成
FastAutoGenerator.create("url", "username", "password")
.globalConfig(builder -> {
builder.author("baomidou") // 设置作者
.enableSwagger() // 开启 swagger 模式
.fileOverride() // 覆盖已生成文件
.outputDir("D://"); // 指定输出目录
})
.packageConfig(builder -> {
builder.parent("com.baomidou.mybatisplus.samples.generator") // 设置父包名
.moduleName("system") // 设置父包模块名
.pathInfo(Collections.singletonMap(OutputFile.mapperXml, "D://")); // 设置mapperXml生成路径
})
.strategyConfig(builder -> {
builder.addInclude("t_simple") // 设置需要生成的表名
.addTablePrefix("t_", "c_"); // 设置过滤表前缀
})
.templateEngine(new FreemarkerTemplateEngine()) // 使用Freemarker引擎模板,默认的是Velocity引擎模板
.execute();
交互式生成
FastAutoGenerator.create(DATA_SOURCE_CONFIG)
// 全局配置
.globalConfig((scanner, builder) -> builder.author(scanner.apply("请输入作者名称?")).fileOverride())
// 包配置
.packageConfig((scanner, builder) -> builder.parent(scanner.apply("请输入包名?")))
// 策略配置
.strategyConfig((scanner, builder) -> builder.addInclude(getTables(scanner.apply("请输入表名,多个英文逗号分隔?所有输入 all")))
.controllerBuilder().enableRestStyle().enableHyphenStyle()
.entityBuilder().enableLombok().addTableFills(
new Column("create_time", FieldFill.INSERT)
).build())
/*
模板引擎配置,默认 Velocity 可选模板引擎 Beetl 或 Freemarker
.templateEngine(new BeetlTemplateEngine())
.templateEngine(new FreemarkerTemplateEngine())
*/
.execute();
// 处理 all 情况
protected static List<String> getTables(String tables) {
return "all".equals(tables) ? Collections.emptyList() : Arrays.asList(tables.split(","));
}
CRUD 接口
说明:
通用 CRUD 封装BaseMapper (opens new window)接口,为 Mybatis-Plus 启动时自动解析实体表关系映射转换为 Mybatis 内部对象注入容器
泛型 T 为任意实体对象
参数 Serializable 为任意类型主键 Mybatis-Plus 不推荐使用复合主键约定每一张表都有自己的唯一 id 主键
对象 Wrapper 为 条件构造器
Mapper CRUD
Insert
// 插入一条记录
int insert(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);
Update
// 根据 whereWrapper 条件,更新记录
int update(@Param(Constants.ENTITY) T updateEntity, @Param(Constants.WRAPPER) Wrapper<T> whereWrapper);
// 根据 ID 修改
int updateById(@Param(Constants.ENTITY) T entity);
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);
分页拦截器(limit)
Mybatis自带分页插件 PaginationInnerInterceptor
在配置类中配置分页拦截器
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor(){
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
//添加分页拦截
PaginationInnerInterceptor pgInterceptor = new PaginationInnerInterceptor(DbType.MYSQL);
pgInterceptor.setMaxLimit(2L);
interceptor.addInnerInterceptor(pgInterceptor);
return interceptor;
}
测试
@Test
void contextLoads() {
Page<User> page = new Page<>();
Page<User> userPage = userMapper.selectPage(page, null);
System.out.println(userPage.getRecords());
System.out.println(userPage.getTotal());
System.out.println(userPage.getSize());
System.out.println(userPage.getMaxLimit());
}
// sql 如下:
==> Preparing: SELECT COUNT(*) FROM user WHERE is_delete = 0
==> Parameters:
<== Columns: COUNT(*)
<== Row: 6
<== Total: 1
==> Preparing: SELECT id,name,age,email,sex,is_delete FROM user WHERE is_delete=0 LIMIT ?
==> Parameters: 2(Long)
<== Columns: id, name, age, email, sex, is_delete
<== Row: 1, Jone, 18, test1@baomidou.com, 1, 0
<== Row: 2, Jack, 20, test2@baomidou.com, 1, 0
<== Total: 2
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@5f96f6a2]
[User(id=1, name=Jone, age=18, email=test1@baomidou.com, sex=MALE, is_delete=null), User(id=2, name=Jack, age=20, email=test2@baomidou.com, sex=MALE, is_delete=null)]
6
2
null
Service CRUD
说明:
通用 Service CRUD 封装IService (opens new window)接口,进一步封装 CRUD 采用 get 查询单行 remove 删除 list 查询集合 page 分页 前缀命名方式区分 Mapper 层避免混淆,
泛型 T 为任意实体对象
建议如果存在自定义通用 Service 方法的可能,请创建自己的 IBaseService 继承 Mybatis-Plus 提供的基类
对象 Wrapper 为 条件构造器
Save
// 插入一条记录(选择字段,策略插入)
boolean save(T entity);
// 插入(批量)
boolean saveBatch(Collection<T> entityList);
// 插入(批量)
boolean saveBatch(Collection<T> 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);
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);
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);
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);
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);
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);
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);
条件构造器
-
QueryWrapper: 查询条件封装
-
UpdateWrapper: Update条件封装
-
LambdaQueryWrapper:Lambda语法使用的查询QueryWrapper
-
LambdaUpdateWrapper:Lambda语法更新封装UpdateWrapper
QueryWrapper
查询用户名中包含a,年龄在20-30之间,邮箱信息不为null的用户信息,并将结果按照年龄降序排列,若年龄相同,则按照id升序
@Test
void contextLoads() {
//查询用户名中包含a,年龄在20-30之间,邮箱信息不为null的用户信息
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.like("name", "a")
.between("age", 20, 30)
.isNotNull("email")
.orderByDesc("age")
.orderByAsc("id");
List<User> list = userMapper.selectList(wrapper);
list.forEach(System.out::println);
//SELECT id,name,age,email,sex,is_delete FROM user WHERE is_delete=0 AND (name LIKE ? AND age BETWEEN ? AND ? AND email IS NOT NULL) ORDER BY age DESC,id ASC
}
删除邮箱地址为null的用户信息
@Test
void testD() {
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.isNull("email");
int row = userMapper.delete(wrapper);
System.out.println(row);
//UPDATE user SET is_delete=1 WHERE is_delete=0 AND (email IS NULL) 注意:有@TabLogic
}
将(年龄大于20并且用户名包含a)或者邮箱为null的用户信息修改 and() | or()
@Test
void testU1(){
// 将(年龄大于20并且用户名包含a)或者邮箱为null的用户信息修改
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.gt("age", 20)
.like("name","a")
.or()
.isNull("email");
User u = new User();
u.setEmail("aaa@aaa.aaa");
int row = userMapper.update(u, wrapper);
System.out.println(row);
//UPDATE user SET email=? WHERE is_delete=0 AND (age > ? AND name LIKE ? OR email IS NULL)
}
将用户名包含a并且(年龄大于20或者邮箱为null)的用户信息修改(and|or(consumer->{…}))
@Test
void testU2(){
// 将用户名包含a并且(年龄大于20或者邮箱为null)的用户信息修改
// and 或 or 方法括号内的内容会优先执行
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.like("name","a")
.and(w->w.gt("age", 20)
.or()
.isNull("email"));
User u = new User();
u.setEmail("bbb@bbb.bbb");
int row = userMapper.update(u, wrapper);
System.out.println(row);
//UPDATE user SET email=? WHERE is_delete=0 AND (name LIKE ? AND (age > ? OR email IS NULL))
}
查询用户的用户名、年龄、邮箱(select(…))
@Test
void testS(){
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.select("name","age","email");
List<Map<String, Object>> maps = userMapper.selectMaps(wrapper);
maps.forEach(System.out::println);
//SELECT name,age,email FROM user WHERE is_delete=0
}
ID小于等于100 (使用子查询)
@Test
void testS1(){
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.inSql("id", "select id from user where id <= 100");
List<User> list = userMapper.selectList(wrapper);
list.forEach(System.out::println);
//SELECT id,name,age,email,sex,is_delete FROM user WHERE is_delete=0 AND (id IN (select id from user where id <= 100))
}
condition 条件组装
@Test
void testQ3(){
//模拟组装条件
String name = "";
Integer ageStart = 20;
Integer ageEnd = 26;
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.like(StringUtils.isNotBlank(name), "name", name)
.between(ageStart != null && ageEnd != null && ageStart <= ageEnd, "age", ageStart ,ageEnd);
List<User> list = userMapper.selectList(wrapper);
list.forEach(System.out::println);
//SELECT id,name,age,email,sex,is_delete FROM user WHERE is_delete=0 AND (age BETWEEN ? AND ?)
}
UpdateWrapper
将用户名包含a并且(年龄大于20或者邮箱为null)的用户信息修改(and|or(consumer->{…}))
@Test
void testU3(){
//将用户名包含a并且(年龄大于20或者邮箱为null)的用户信息修改(and\|or(consumer->{...}))
UpdateWrapper<User> wrapper = new UpdateWrapper<>();
wrapper.like("name", "a")
.and(w->{w.gt("age", 20).or().isNull("email");})
.set("name","wangermazi")
.set("email", "ccc@ccc.ccc");
int row = userMapper.update(null, wrapper);
System.out.println(row);
//UPDATE user SET name=?,email=? WHERE is_delete=0 AND (name LIKE ? AND (age > ? OR email IS NULL))
}
LambdaQueryWrapper
@Test
void testLQ1(){
//模拟组装条件
String name = "";
Integer ageStart = 20;
Integer ageEnd = 26;
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.like(StringUtils.isNotBlank(name), User::getName, name)
.between(ageStart != null && ageEnd != null && ageStart <= ageEnd, User::getAge, ageStart, ageEnd);
List<User> list = userMapper.selectList(wrapper);
list.forEach(System.out::println);
//SELECT id,name,age,email,sex,is_delete FROM user WHERE is_delete=0 AND (age BETWEEN ? AND ?)
}
LambdaUpdateWrapper
@Test
void testLU1(){
//将用户名包含a并且(年龄大于20或者邮箱为null)的用户信息修改(and\|or(consumer->{...}))
LambdaUpdateWrapper<User> wrapper = new LambdaUpdateWrapper<>();
wrapper.like(User::getName,"a")
.and(w->{w.gt(User::getAge, 20).or().isNull(User::getEmail);})
.set(User::getName,"wangermazi")
.set(User::getEmail, "ccc@ccc.ccc");
int row = userMapper.update(null, wrapper);
System.out.println(row);
//UPDATE user SET name=?,email=? WHERE is_delete=0 AND (name LIKE ? AND (age > ? OR email IS NULL))
}
主键策略
提示
主键生成策略必须使用 INPUT
支持父类定义 @KeySequence 子类继承使用
如:
@KeySequence(value = "SEQ_ORACLE_STRING_KEY", clazz = String.class)
public class YourEntity {
@TableId(value = "ID_STR", type = IdType.INPUT)
private String idStr;
}
SpringBoot 配置
方式一:使用配置类
@Bean
public IKeyGenerator keyGenerator() {
return new H2KeyGenerator();
}
方式二:通过MybatisPlusPropertiesCustomizer 自定义
@Bean
public MybatisPlusPropertiesCustomizer plusPropertiesCustomizer() {
return plusProperties -> plusProperties.getGlobalConfig().getDbConfig().setKeyGenerator(new H2KeyGenerator());
}
自定义ID生成器
方式一:使用Bean扫描注入
@Component
public class CustomIdGenerator implements IdentifierGenerator {
@Override
public Long nextId(Object entity) {
//可以将当前传入的class全类名来作为bizKey,或者提取参数来生成bizKey进行分布式Id调用生成.
String bizKey = entity.getClass().getName();
//根据bizKey调用分布式ID生成
long id = ....;
//返回生成的id值即可.
return id;
}
}
方式二:使用配置类
@Bean
public IdentifierGenerator idGenerator() {
return new CustomIdGenerator();
}
方式三:通过 MybatisPlusPropertiesCustomizer 自定义
@Bean
public MybatisPlusPropertiesCustomizer plusPropertiesCustomizer() {
return plusProperties -> plusProperties.getGlobalConfig().setIdentifierGenerator(new CustomIdGenerator());
}
扩展功能
多数据源
特性
- 支持 数据源分组 ,适用于多种场景 纯粹多库 读写分离 一主多从 混合模式。
- 支持数据库敏感配置信息 加密 ENC()。
- 支持每个数据库独立初始化表结构schema和数据库database。
- 支持无数据源启动,支持懒加载数据源(需要的时候再创建连接)。
- 支持 自定义注解 ,需继承DS(3.2.0+)。
- 提供并简化对Druid,HikariCp,BeeCp,Dbcp2的快速集成。
- 提供对Mybatis-Plus,Quartz,ShardingJdbc,P6sy,Jndi等组件的集成方案。
- 提供 自定义数据源来源 方案(如全从数据库加载)。
- 提供项目启动后 动态增加移除数据源 方案。
- 提供Mybatis环境下的 纯读写分离 方案。
- 提供使用 spel动态参数 解析数据源方案。内置spel,session,header,支持自定义。
- 支持 多层数据源嵌套切换 。(ServiceA >>> ServiceB >>> ServiceC)。
- 提供 **基于seata的分布式事务方案。
- 提供 本地多数据源事务方案。
约定
- 本框架只做 切换数据源 这件核心的事情,并不限制你的具体操作,切换了数据源可以做任何CRUD。
- 配置文件所有以下划线 _ 分割的数据源 首部 即为组的名称,相同组名称的数据源会放在一个组下。
- 切换数据源可以是组名,也可以是具体数据源名称。组名则切换时采用负载均衡算法切换。
- 默认的数据源名称为 master ,你可以通过 spring.datasource.dynamic.primary 修改。
- 方法上的注解优先于类上注解。
- DS支持继承抽象类上的DS,暂不支持继承接口上的DS
使用方法
引入依赖:dynamic-datasource-spring-boot-starter
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
<version>${version}</version>
</dependency>
配置数据源
spring:
datasource:
dynamic:
primary: master #设置默认的数据源或者数据源组,默认值即为master
strict: false #严格匹配数据源,默认false. true未匹配到指定数据源时抛异常,false使用默认数据源
datasource:
master:
url: jdbc:mysql://xx.xx.xx.xx:3306/dynamic
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver # 3.2.0开始支持SPI可省略此配置
slave_1:
url: jdbc:mysql://xx.xx.xx.xx:3307/dynamic
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver
slave_2:
url: ENC(xxxxx) # 内置加密,使用请查看详细文档
username: ENC(xxxxx)
password: ENC(xxxxx)
driver-class-name: com.mysql.jdbc.Driver
#......省略
#以上会配置一个默认库master,一个组slave下有两个子库slave_1,slave_2
使用 @DS 切换数据源
@DS 可以注解在方法上或类上,同时存在就近原则 方法上注解 优先于 类上注解
测试
准备
再启动一个mysql服务,端口号为53307,再把53306服务的user表复制一份。
<!-- pom.xml -->
<!-- 动态数据源 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
<version>3.5.0</version>
</dependency>
spring:
datasource:
dynamic:
primary: master
strict: false
datasource:
master:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://121.4.97.105:53306/test_mpdb?useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT
username: root
password: root
slave_1:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://121.4.97.105:53307/test_mpdb?useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT
username: root
password: root
再ServiceImpl或Mapper中的方法中使用@DS注解
@Repository
public interface UserMapper extends BaseMapper<User> {
@DS("master")
User selectByIdFromMysql01(Long id);
@DS("slave_1")
User selectByIdFromMysql02(Long id);
}
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User>
implements UserService{
@Autowired
private UserMapper userMapper;
@DS("master")
@Override
public User getUserByIdFromMysql01(Long id) {
return userMapper.selectById(id);
}
@DS("slave_1")
@Override
public User getUserByIdFromMysql02(Long id) {
return userMapper.selectById(id);
}
}
测试
@Test
void contextLoads() {
// User u1 = userMapper.selectByIdFromMysql01(1L);
// System.out.println(u1);
//
// User u2 = userMapper.selectByIdFromMysql02(1l);
// System.out.println(u2);
User u1 = userService.getUserByIdFromMysql01(1l);
System.out.println(u1);
User u2 = userService.getUserByIdFromMysql02(1l);
System.out.println(u2);
}
}
字段类型处理器
类型处理器,用于 JavaType 与 JdbcType 之间的转换,用于 PreparedStatement 设置参数值和从 ResultSet 或 CallableStatement 中取出一个值,本文讲解 mybaits-plus 内置常用类型处理器如何通过TableField注解快速注入到 mybatis 容器中
@Data
@Accessors(chain = true)
@TableName(autoResultMap = true)
public class User {
private Long id;
...
/**
* 注意!! 必须开启映射注解
*
* @TableName(autoResultMap = true)
*
* 以下两种类型处理器,二选一 也可以同时存在
*
* 注意!!选择对应的 JSON 处理器也必须存在对应 JSON 解析依赖包
*/
@TableField(typeHandler = JacksonTypeHandler.class)
// @TableField(typeHandler = FastjsonTypeHandler.class)
private OtherInfo otherInfo;
}
SQL 打印分析
该功能依赖 p6spy 组件,完美的输出打印 SQL 及执行时长 3.1.0 以上版本
操作步骤
依赖导入
<dependency>
<groupId>p6spy</groupId>
<artifactId>p6spy</artifactId>
<version>最新版本</version>
</dependency>
application.yml配置
spring:
datasource:
driver-class-name: com.p6spy.engine.spy.P6SpyDriver
url: jdbc:p6spy:mysql://127.0.0.1:3360/test?userSSL=false
username: root
password: 123456
profiles:
active: dev
spy.properties 配置:
module.log=com.p6spy.engine.logging.P6LogFactory,com.p6spy.engine.outage.P6OutageFactory
# 自定义日志打印
logMessageFormat=com.baomidou.mybatisplus.extension.p6spy.P6SpyLogger
#日志输出到控制台,解开注释就行了
# appender=com.baomidou.mybatisplus.extension.p6spy.StdoutLogger
# 指定输出文件位置
logfile=sql.log
# 使用日志系统记录 sql
#appender=com.p6spy.engine.spy.appender.Slf4JLogger
# 设置 p6spy driver 代理
deregisterdrivers=true
# 取消JDBC URL前缀
useprefix=true
# 配置记录 Log 例外,可去掉的结果集有error,info,batch,debug,statement,commit,rollback,result,resultset.
excludecategories=info,debug,result,batch,resultset
# 日期格式
dateformat=yyyy-MM-dd HH:mm:ss
# 实际驱动可多个
#driverlist=org.h2.Driver
# 是否开启慢SQL记录
outagedetection=true
# 慢SQL记录标准 2 秒
outagedetectioninterval=2
Mybatis 代码生成器
依赖
<!-- myvatisPlus依赖 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.1</version>
</dependency>
<!-- 代码生成器依赖 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.5.2</version>
</dependency>
<!-- freemarker 模板引擎依赖 -->
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
<version>2.3.30</version>
</dependency>
<!-- freemarker模板和velocity模板引擎根据选择配一个就好了 -->
<!-- velocity 模板引擎依赖 -->
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity</artifactId>
<version>1.7</version>
</dependency>
快速使用
FastAutoGenerator.create("url", "username", "password")
.globalConfig(builder -> { //全局配置
builder.author("baomidou") // 设置作者
.enableSwagger() // 开启 swagger 模式
.fileOverride() // 覆盖已生成文件
.outputDir("D://"); // 指定输出目录
})
.packageConfig(builder -> { //包配置
builder.parent("com.baomidou.mybatisplus.samples.generator") // 设置父包名
.moduleName("system") // 设置父包模块名
.pathInfo(Collections.singletonMap(OutputFile.mapperXml, "D://")); // 设置mapperXml生成路径
})
.strategyConfig(builder -> { //策略配置
builder.addInclude("t_simple") // 设置需要生成的表名
.addTablePrefix("t_", "c_"); // 设置过滤表前缀
})
.templateEngine(new FreemarkerTemplateEngine()) // 使用Freemarker引擎模板,默认的是Velocity引擎模板
.execute();
自定义
/**
* 数据源配置
*/
private static final DataSourceConfig DATA_SOURCE_CONFIG = new DataSourceConfig
.Builder("jdbc:mysql://ip:port/bmos_cms?useSSL=false", "root", "root")
.build();
public static void execute() {
new AutoGenerator(DATA_SOURCE_CONFIG)
// 全局配置
.global(new GlobalConfig.Builder()
.fileOverride()
.outputDir("/Users/guowenjia/Desktop/code/mptest/")
.author("wguo")
.enableSwagger()
.dateType(DateType.TIME_PACK)
.commentDate("yyyy-MM-dd")
.build())
// 包配置
.packageInfo(new PackageConfig.Builder()
.parent("com.wguo.mybatisplusgenertortemplate") // 设置父包名
.moduleName("test") // 设置父包模块名
.entity("domain")
.controller("controller")
.service("service")
.serviceImpl("service.impl")
.mapper("mapper")
.xml("mapper.xml")
.pathInfo(Collections.singletonMap(OutputFile.xml ,"/Users/guowenjia/Desktop/code/mptest/mapper/"))//设置mapperXml生成路径
.build())
// 策略配置
.strategy(new StrategyConfig.Builder()
.enableCapitalMode()
.enableSkipView()
.disableSqlFilter()
// .likeTable(new LikeTable("USER"))
.addInclude("t_news")
.addTablePrefix("t_", "c_")
// .addFieldSuffix("_flag")
//entity策略
.entityBuilder()
// .superClass(BaseEntity.class)
// .disableSerialVersionUID()
// .enableChainModel()
.enableLombok()
// .enableRemoveIsPrefix()
.enableTableFieldAnnotation()
// .enableActiveRecord()
.versionColumnName("version")
.versionPropertyName("version")
.logicDeleteColumnName("is_deleted")
.logicDeletePropertyName("is_deleted")
.naming(NamingStrategy.underline_to_camel)
.columnNaming(NamingStrategy.underline_to_camel)
// .addSuperEntityColumns("created", "created_time", "updated", "last_updated_time")
// .addIgnoreColumns("password")
// .addTableFills(new Column("create_time", FieldFill.INSERT))
// .addTableFills(new Property("updateTime", FieldFill.INSERT_UPDATE))
// .idType(IdType.AUTO)
.formatFileName("%s")
//Controller 策略配置
.controllerBuilder()
// .superClass(BaseController.class)
.enableHyphenStyle()
.enableRestStyle()
.formatFileName("%sController")
//Service 策略配置
.serviceBuilder()
// .superServiceClass(BaseService.class)
// .superServiceImplClass(BaseServiceImpl.class)
.formatServiceFileName("%sService")
.formatServiceImplFileName("%sServiceImp")
//Mapper 策略配置
.mapperBuilder()
.superClass(BaseMapper.class)
.enableMapperAnnotation()
.enableBaseResultMap()
.enableBaseColumnList()
// .cache(MyMapperCache.class)
.formatMapperFileName("%sMapper")
.formatXmlFileName("%sXml")
.build())
// 注入配置
.injection(new InjectionConfig.Builder()
// .beforeOutputFile((tableInfo, objectMap) -> {
// System.out.println("tableInfo: " + tableInfo.getEntityName() + " objectMap: " + objectMap.size());
// })
// .customMap(Collections.singletonMap("test", "baomidou"))
// .customFile(Collections.singletonMap("cus-controller.java.ftl", "/templates/cus-controller.java.ftl"))
.build())
// 模板配置,注意在mybatis-plus-generator-3.5.2包下的templates下有一些定义好的模板
//如果我们使用的是Freemarker模板,就是以ftl结尾的;使用Velocity,以vm结尾;使用Beetl,以btl结尾
.template(new TemplateConfig.Builder()
// .disable(TemplateType.ENTITY)
.entity("/templates/entity.java")
.service("/templates/service.java")
.serviceImpl("/templates/serviceImpl.java")
.mapper("/templates/mapper.java")
.xml("/templates/mapper.xml")
.controller("/templates/cus-controller.java") //controller 使用我们自定义的模板
.build())
// 执行
.execute(new FreemarkerTemplateEngine());
}
自定义controller模板如下
${entity?uncap_first} uncap_first 首字母小写
package ${package.Controller};
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import ${package.Entity}.${entity};
import ${package.Service}.${table.serviceName};
import com.bmos.cms.support.JsonData;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* <p>
* ${table.comment!} 前端控制器
* </p>
*
* @author ${author}
* @since ${date}
*/
@Api(tags = "${table.comment!}管理控制器")
@RestController
@RequestMapping("/api/v1/${entity?uncap_first}")
public class ${table.controllerName} {
@Autowired
private ${table.serviceName} ${entity?uncap_first}Service;
@ApiOperation("根据ID获取唯一的${table.comment!}")
@GetMapping("/{key:[0-9]+}")
public JsonData get(@ApiParam(name = "ID,路径参数", example = "0") @PathVariable Integer key){
LambdaQueryWrapper<${entity}> queryWrapper = new LambdaQueryWrapper();
queryWrapper.eq(key != null, ${entity}::getId, key);
return JsonData.buildSuccess(${entity?uncap_first}Service.getOne(queryWrapper));
}
@ApiOperation("分页查询${table.comment!}")
@GetMapping("/page")
public JsonData page(@ApiParam(name = "当前页", required = true, example = "1") @RequestParam(name = "pageNumber",required = true, defaultValue = "1") Integer pageNumber,
@ApiParam(name = "每页显示的条数", required = true, example = "50") @RequestParam(name = "pageSize",required = true, defaultValue = "50") Integer pageSize
){
Page<${entity}> page = new Page<>();
page.setSize(pageSize);
page.setCurrent(pageNumber);
LambdaQueryWrapper<${entity}> queryWrapper = new LambdaQueryWrapper<>();
${entity?uncap_first}Service.page(page, queryWrapper);
return JsonData.buildSuccess(page);
}
@ApiOperation("新增或修改一个${table.comment!},ID存在则修改,不存在则新增")
@PostMapping("/")
public JsonData save(@ApiParam(name = "${table.comment!}信息")@RequestBody ${entity} entity){
LambdaUpdateWrapper<${entity}> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(${entity}::getId, entity.getId() != null ?entity.getId():0);
${entity?uncap_first}Service.saveOrUpdate(entity, wrapper);
return JsonData.buildSuccess(entity);
}
@ApiOperation("删除${table.comment!}")
@DeleteMapping("/{id:[0-9]+}")
public JsonData delete(@ApiParam(name = "${table.comment!}编码", example = "0") @PathVariable("id") Integer id){
LambdaQueryWrapper<${entity}> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(id != null, ${entity}::getId, id);
boolean b = ${entity?uncap_first}Service.remove(wrapper);
return b?JsonData.buildSuccess("删除成功!"):JsonData.buildError(-1, "删除失败");
}
@ApiOperation("批量删除")
@PostMapping("/delBatch")
public JsonData delBatch(@ApiParam(name = "id集合") @RequestBody List<Integer> ids){
${entity?uncap_first}Service.removeBatchByIds(ids);
return JsonData.buildSuccess("删除成功");
}
}
}