简介 MyBatis-Plus (简称 MP)是一个 MyBatis 的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。是一款全自动的ORM持久层框架。
特性:
无侵入 :只做增强不做改变,引入它不会对现有工程产生影响,如丝般顺滑
损耗小 :启动即会自动注入基本 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 操作智能分析阻断,也可自定义拦截规则,预防误操作
支持数据库:
MySQL,Oracle,DB2,H2,HSQL,SQLite,PostgreSQL,SQLServer,Phoenix,Gauss ,ClickHouse,Sybase,OceanBase,Firebird,Cubrid,Goldilocks,csiidb,informix,TDengine,redshift
达梦数据库,虚谷数据库,人大金仓数据库,南大通用(华库)数据库,南大通用数据库,神通数据库,瀚高数据库,优炫数据库
快速入门
准备数据库脚本
现有一张 User 表,其表结构如下:
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) ); 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' );
准备boot工程
导入依赖
<?xml version="1.0" encoding="UTF-8" ?> <project xmlns ="http://maven.apache.org/POM/4.0.0" xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation ="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" > <modelVersion > 4.0.0</modelVersion > <parent > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-parent</artifactId > <version > 3.0.5</version > </parent > <groupId > com.atguigu</groupId > <artifactId > springboot-starter-mybatis-plus-06</artifactId > <version > 1.0-SNAPSHOT</version > <properties > <maven.compiler.source > 17</maven.compiler.source > <maven.compiler.target > 17</maven.compiler.target > <project.build.sourceEncoding > UTF-8</project.build.sourceEncoding > </properties > <dependencies > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter</artifactId > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-test</artifactId > </dependency > <dependency > <groupId > com.baomidou</groupId > <artifactId > mybatis-plus-boot-starter</artifactId > <version > 3.5.3.1</version > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-jdbc</artifactId > </dependency > <dependency > <groupId > com.alibaba</groupId > <artifactId > druid-spring-boot-3-starter</artifactId > <version > 1.2.18</version > </dependency > <dependency > <groupId > mysql</groupId > <artifactId > mysql-connector-java</artifactId > <version > 8.0.28</version > </dependency > <dependency > <groupId > org.projectlombok</groupId > <artifactId > lombok</artifactId > <version > 1.18.28</version > </dependency > </dependencies > <build > <plugins > <plugin > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-maven-plugin</artifactId > </plugin > </plugins > </build > </project >
配置文件和启动类
完善连接池配置:
文件夹:META-INF.spring
文件名:org.springframework.boot.autoconfigure.AutoConfiguration.imports
内容:com.alibaba.druid.spring.boot3.autoconfigure.DruidDataSourceAutoConfigure
application.yaml
spring: datasource: type: com.alibaba.druid.pool.DruidDataSource druid: url: jdbc:mysql:///day01?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf8&useSSL=false username: root password: root driver-class-name: com.mysql.cj.jdbc.Driver filter: stat: enabled: true log-slow-sql: true slow-sql-millis: 1000 merge-sql: true filters: stat,wall,log4j2 mybatis: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
启动类
@MapperScan("com.atguigu.mapper") @SpringBootApplication public class MainApplication { public static void main (String[] args) { SpringApplication.run(MainApplication.class,args); } }
功能编码
编写实体类 User.java(此处使用了 Lombok 简化代码)
@Data public class User { private Long id; private String name; private Integer age; private String email; }
编写 Mapper 包下的 UserMapper接口
public interface UserMapper extends BaseMapper <User> {}
继承mybatis-plus提供的基础Mapper接口,自带crud方法!
测试和使用
添加测试类,进行功能测试:
@SpringBootTest @RunWith(SpringRunner.class) public class SampleTest { @Autowired private UserMapper userMapper; @Test public void testSelect () { System.out.println(("----- selectAll method test ------" )); List<User> userList = userMapper.selectList(null ); userList.forEach(System.out::println); } }
小结
通过以上几个简单的步骤,我们就实现了 User 表的 CRUD 功能,甚至连 XML 文件都不用编写!
从以上步骤中,我们可以看到集成MyBatis-Plus非常的简单,只需要引入 starter 工程,并配置 mapper 扫描路径即可。
核心功能 基于Mapper接口CRUD
通用 CRUD 封装BaseMapper 接口, Mybatis-Plus 启动时自动解析实体表关系映射转换为 Mybatis 内部对象注入容器! 内部包含常见的单表操作!
Insert方法
Delete方法 int delete (@Param(Constants.WRAPPER) Wrapper<T> wrapper) ;int deleteBatchIds (@Param(Constants.COLLECTION) Collection<? extends Serializable> idList) ;int deleteById (Serializable id) ;int deleteByMap (@Param(Constants.COLUMN_MAP) Map<String, Object> columnMap) ;
类型
参数名
描述
Wrapper
wrapper
实体对象封装操作类(可以为 null)
Collection<? extends Serializable>
idList
主键 ID 列表(不能为 null 以及 empty)
Serializable
id
主键 ID
Map
columnMap
表字段 map 对象
Update方法 int update (@Param(Constants.ENTITY) T updateEntity, @Param(Constants.WRAPPER) Wrapper<T> whereWrapper) ;int updateById (@Param(Constants.ENTITY) T entity) ;
类型
参数名
描述
T
entity
实体对象 (set 条件值,可为 null)
Wrapper
updateWrapper
实体对象封装操作类(可以为 null,里面的 entity 用于生成 where 语句)
Select方法 T selectById (Serializable id) ; T selectOne (@Param(Constants.WRAPPER) Wrapper<T> queryWrapper) ; List<T> selectBatchIds (@Param(Constants.COLLECTION) Collection<? extends Serializable> idList) ; List<T> selectList (@Param(Constants.WRAPPER) Wrapper<T> queryWrapper) ; List<T> selectByMap (@Param(Constants.COLUMN_MAP) Map<String, Object> columnMap) ; List<Map<String, Object>> selectMaps (@Param(Constants.WRAPPER) Wrapper<T> queryWrapper) ; List<Object> selectObjs (@Param(Constants.WRAPPER) Wrapper<T> queryWrapper) ; IPage<T> selectPage (IPage<T> page, @Param(Constants.WRAPPER) Wrapper<T> queryWrapper) ; IPage<Map<String, Object>> selectMapsPage (IPage<T> page, @Param(Constants.WRAPPER) Wrapper<T> queryWrapper) ; Integer selectCount (@Param(Constants.WRAPPER) Wrapper<T> queryWrapper) ;
参数说明
类型
参数名
描述
Serializable
id
主键 ID
Wrapper
queryWrapper
实体对象封装操作类(可以为 null)
Collection<? extends Serializable>
idList
主键 ID 列表(不能为 null 以及 empty)
Map
columnMap
表字段 map 对象
IPage
page
分页查询条件(可以为 RowBounds.DEFAULT)
自定义和多表映射 mybatis-plus的默认mapperxml位置
mybatis-plus: mapper-locations: classpath:/mapper/*.xml
自定义mapper方法:
public interface UserMapper extends BaseMapper <User> { List<User> queryAll () ; }
基于mapper.xml实现:
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "https://mybatis.org/dtd/mybatis-3-mapper.dtd" > <mapper namespace ="com.atguigu.mapper.UserMapper" > <select id ="queryAll" resultType ="user" > select * from user </select > </mapper >
基于Service接口CRUD 通用 Service CRUD 封装IService 接口,进一步封装 CRUD 采用 get 查询单行 remove 删除 list 查询集合 page 分页 前缀命名方式区分 Mapper 层避免混淆,
对比Mapper接口CRUD区别:
service添加了批量方法
service层的方法自动添加事务
使用Iservice接口方式 接口继承IService接口
public interface UserService extends IService <User> {}
类继承ServiceImpl实现类
@Service public class UserServiceImpl extends ServiceImpl <UserMapper,User> implements UserService {}
CRUD方法介绍 保存: boolean save (T entity) ;boolean saveBatch (Collection<T> entityList) ;boolean saveBatch (Collection<T> entityList, int batchSize) ;修改或者保存: boolean saveOrUpdate (T entity) ;boolean saveOrUpdate (T entity, Wrapper<T> updateWrapper) ;boolean saveOrUpdateBatch (Collection<T> entityList) ;boolean saveOrUpdateBatch (Collection<T> entityList, int batchSize) ;移除: boolean remove (Wrapper<T> queryWrapper) ;boolean removeById (Serializable id) ;boolean removeByMap (Map<String, Object> columnMap) ;boolean removeByIds (Collection<? extends Serializable> idList) ;更新: boolean update (Wrapper<T> updateWrapper) ;boolean update (T updateEntity, Wrapper<T> whereWrapper) ;boolean updateById (T entity) ;boolean updateBatchById (Collection<T> entityList) ;boolean updateBatchById (Collection<T> entityList, int batchSize) ;数量: int count () ;int count (Wrapper<T> queryWrapper) ;查询: T getById (Serializable id) ; T getOne (Wrapper<T> queryWrapper) ; T getOne (Wrapper<T> queryWrapper, boolean throwEx) ; Map<String, Object> getMap (Wrapper<T> queryWrapper) ; <V> V getObj (Wrapper<T> queryWrapper, Function<? super Object, V> mapper) ; 集合: List<T> list () ; List<T> list (Wrapper<T> queryWrapper) ; Collection<T> listByIds (Collection<? extends Serializable> idList) ; 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) ; List<Object> listObjs (Wrapper<T> queryWrapper) ; <V> List<V> listObjs (Wrapper<T> queryWrapper, Function<? super Object, V> mapper) ;
分页查询实现 首先明确一点,mybatis分页rowbounds确实实现了分页,但是是逻辑分页/内存分页,他先把数据全查出来,load到memory中,然后给你你想要的
Mp就提供了物理分页的插件,解决上述问题
导入分页插件
@Bean public MybatisPlusInterceptor mybatisPlusInterceptor () { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor (); PaginationInnerInterceptor pageInterceptor = new PaginationInnerInterceptor (); pageInterceptor.setOverflow(false ); pageInterceptor.setMaxLimit(500L ); pageInterceptor.setDbType(DbType.MYSQL); interceptor.addInnerInterceptor(pageInterceptor); return interceptor; }
使用分页查询
@Test public void testPageQuery () { Page<User> page = new Page <>(1 , 5 ); userMapper.selectPage(page, null ); List<User> list = page.getRecords(); list.forEach(System.out::println); System.out.println("当前页:" +page.getCurrent()); System.out.println("每页显示的条数:" +page.getSize()); System.out.println("总记录数:" +page.getTotal()); System.out.println("总页数:" +page.getPages()); System.out.println("是否有上一页:" +page.hasPrevious()); System.out.println("是否有下一页:" +page.hasNext()); }
自定义的mapper方法使用分页
方法
IPage<User> selectPageVo (IPage<?> page, Integer id) ;
接口实现
<select id="selectPageVo" resultType="xxx.xxx.xxx.User" > SELECT * FROM user WHERE id > #{id} </select>
测试
@Test public void testQuick () { IPage page = new Page (1 ,2 ); userMapper.selectPageVo(page,2 ); long current = page.getCurrent(); System.out.println("current = " + current); long pages = page.getPages(); System.out.println("pages = " + pages); long total = page.getTotal(); System.out.println("total = " + total); List records = page.getRecords(); System.out.println("records = " + records); }
条件构造器使用 条件构造器作用 实例代码:
QueryWrapper<User> queryWrapper = new QueryWrapper <>(); queryWrapper.eq("name" , "John" ); queryWrapper.ne("age" , 30 ); queryWrapper.like("email" , "@gmail.com" ); 等同于: delete from user where name = "John" and age != 30 and email like "%@gmail.com%" int delete (@Param(Constants.WRAPPER) Wrapper<T> wrapper) ;
使用MyBatis-Plus的条件构造器,可以构建灵活、高效的查询条件,而不需要手动编写复杂的 SQL 语句。它提供了许多方法来支持各种条件操作符,并且可以通过链式调用来组合多个条件。这样可以简化查询的编写过程,并提高开发效率。
条件构造器继承结构 条件构造器类结构:
Wrapper : 条件构造抽象类,最顶端父类
AbstractWrapper : 用于查询条件封装,生成 sql 的 where 条件
QueryWrapper : 查询/删除条件封装
UpdateWrapper : 修改条件封装
AbstractLambdaWrapper : 使用Lambda 语法
LambdaQueryWrapper :用于Lambda语法使用的查询Wrapper
LambdaUpdateWrapper : Lambda 更新封装Wrapper
基于QueryWrapper 组装条件
组装查询条件:
@Test public void test01 () { QueryWrapper<User> queryWrapper = new QueryWrapper <>(); queryWrapper.like("username" , "a" ) .between("age" , 20 , 30 ) .isNotNull("email" ); List<User> list = userMapper.selectList(queryWrapper); list.forEach(System.out::println);
组装排序条件:
@Test public void test02 () { QueryWrapper<User> queryWrapper = new QueryWrapper <>(); queryWrapper .orderByDesc("age" ) .orderByAsc("id" ); List<User> users = userMapper.selectList(queryWrapper); users.forEach(System.out::println); }
组装删除条件:
@Test public void test03 () { QueryWrapper<User> queryWrapper = new QueryWrapper <>(); queryWrapper.isNull("email" ); int result = userMapper.delete(queryWrapper); System.out.println("受影响的行数:" + result); }
and和or关键字使用(修改):
@Test public void test04 () { QueryWrapper<User> queryWrapper = new QueryWrapper <>(); queryWrapper .like("username" , "a" ) .gt("age" , 20 ) .or() .isNull("email" ); User user = new User (); user.setAge(18 ); user.setEmail("user@atguigu.com" ); int result = userMapper.update(user, queryWrapper); System.out.println("受影响的行数:" + result); }
指定列映射查询:
@Test public void test05 () { QueryWrapper<User> queryWrapper = new QueryWrapper <>(); queryWrapper.select("username" , "age" ); List<Map<String, Object>> maps = userMapper.selectMaps(queryWrapper); maps.forEach(System.out::println); }
condition判断组织条件:
@Test public void testQuick3 () { String name = "root" ; int age = 18 ; QueryWrapper<User> queryWrapper = new QueryWrapper <>(); if (!StringUtils.isEmpty(name)){ queryWrapper.eq("name" ,name); } if (age > 1 ){ queryWrapper.eq("age" ,age); } queryWrapper.eq(!StringUtils.isEmpty(name),"name" ,name) .eq(age>1 ,"age" ,age); }
基于 UpdateWrapper组装条件 使用queryWrapper:
@Test public void test04 () { QueryWrapper<User> queryWrapper = new QueryWrapper <>(); queryWrapper .like("username" , "a" ) .gt("age" , 20 ) .or() .isNull("email" ); User user = new User (); user.setAge(18 ); user.setEmail("user@atguigu.com" ); int result = userMapper.update(user, queryWrapper); System.out.println("受影响的行数:" + result); }
注意:使用queryWrapper + 实体类形式可以实现修改,但是无法将列值修改为null值!
使用updateWrapper:
@Test public void testQuick2 () { UpdateWrapper<User> updateWrapper = new UpdateWrapper <>(); updateWrapper.eq("id" ,3 ) .set("email" ,null ) .set("age" ,18 ); int result = userMapper.update(null , updateWrapper); System.out.println("result = " + result); }
使用updateWrapper可以随意设置列的值!!
基于LambdaQueryWrapper组装条件 1. **LambdaQueryWrapper对比QueryWrapper优势**
QueryWrapper 示例代码:
QueryWrapper<User> queryWrapper = new QueryWrapper <>(); queryWrapper.eq("name" , "John" ) .ge("age" , 18 ) .orderByDesc("create_time" ) .last("limit 10" ); List<User> userList = userMapper.selectList(queryWrapper);
LambdaQueryWrapper 示例代码:
LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper <>(); lambdaQueryWrapper.eq(User::getName, "John" ) .ge(User::getAge, 18 ) .orderByDesc(User::getCreateTime) .last("limit 10" ); List<User> userList = userMapper.selectList(lambdaQueryWrapper);
从上面的代码对比可以看出,相比于 QueryWrapper,LambdaQueryWrapper 使用了实体类的属性引用(例如 User::getName、User::getAge),而不是字符串来表示字段名,这提高了代码的可读性和可维护性。
2. **lambda表达式回顾**
Lambda 表达式是 Java 8 引入的一种函数式编程特性,它提供了一种更简洁、更直观的方式来表示匿名函数或函数式接口的实现。Lambda 表达式可以用于简化代码,提高代码的可读性和可维护性。
Lambda 表达式的语法可以分为以下几个部分:
参数列表: 参数列表用小括号 () 括起来,可以指定零个或多个参数。如果没有参数,可以省略小括号;如果只有一个参数,可以省略小括号。
示例:(a, b), x ->, () ->
箭头符号: 箭头符号 -> 分割参数列表和 Lambda 表达式的主体部分。
示例:->
Lambda 表达式的主体: Lambda 表达式的主体部分可以是一个表达式或一个代码块。如果是一个表达式,可以省略 return 关键字;如果是多条语句的代码块,需要使用大括号 {} 括起来,并且需要明确指定 return 关键字。
示例:
单个表达式:x -> x * x
代码块:(x, y) -> { int sum = x + y; return sum; }
Lambda 表达式的语法可以更具体地描述如下:
interface Greeting { void sayHello () ; } public class LambdaExample { public static void main (String[] args) { Greeting greeting = new Greeting () { @Override public void sayHello (int a) { System.out.println("Hello, world!" ); } }; a->System.out.println("Hello, world!" ) greeting = () -> System.out.println("Hello, world!" ); System.out::println; () -> 类.XXX(); -> 类::方法名 greeting.sayHello(); } }
3. 方法引用回顾:
方法引用是 Java 8 中引入的一种语法特性,它提供了一种简洁的方式来直接引用已有的方法或构造函数。方法引用可以替代 Lambda 表达式,使代码更简洁、更易读。
Java 8 支持以下几种方法引用的形式:
1. **静态方法引用:** 引用静态方法,语法为 `类名::静态方法名`。
2. **实例方法引用:** 引用实例方法,语法为 `实例对象::实例方法名`。
3. **对象方法引用:** 引用特定对象的实例方法,语法为 `类名::实例方法名`。
4. **构造函数引用:** 引用构造函数,语法为 `类名::new`。
演示代码:
import java.util.Arrays;import java.util.List;import java.util.function.Consumer;public class MethodReferenceExample { public static void main (String[] args) { List<String> names = Arrays.asList("John" , "Tom" , "Alice" ); names.forEach(name -> System.out.println(name)); names.forEach(System.out::println); } }
4. lambdaQueryWrapper使用案例:
@Test public void testQuick4 () { String name = "root" ; int age = 18 ; QueryWrapper<User> queryWrapper = new QueryWrapper <>(); queryWrapper.eq(!StringUtils.isEmpty(name),"name" ,name) .eq(age>1 ,"age" ,age); LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper <>(); lambdaQueryWrapper.eq(!StringUtils.isEmpty(name), User::getName,name); List<User> users= userMapper.selectList(lambdaQueryWrapper); System.out.println(users); }
基于LambdaUpdateWrapper组装条件 使用案例:
@Test public void testQuick2 () { UpdateWrapper<User> updateWrapper = new UpdateWrapper <>(); updateWrapper.eq("id" ,3 ) .set("email" ,null ) .set("age" ,18 ); LambdaUpdateWrapper<User> updateWrapper1 = new LambdaUpdateWrapper <>(); updateWrapper1.eq(User::getId,3 ) .set(User::getEmail,null ) .set(User::getAge,18 ); int result = userMapper.update(null , updateWrapper); System.out.println("result = " + result); }
核心注解使用
理解和介绍
MyBatis-Plus是一个基于MyBatis框架的增强工具,提供了一系列简化和增强的功能,用于加快开发人员在使用MyBatis进行数据库访问时的效率。
MyBatis-Plus提供了一种基于注解的方式来定义和映射数据库操作,其中的注解起到了重要作用。
理解:
public interface UserMapper extends BaseMapper <User> {}
此接口对应的方法为什么会自动触发 user表的crud呢?
默认情况下, 根据指定的<实体类>的名称对应数据库表名,属性名对应数据库的列名!
但是不是所有数据库的信息和实体类都完全映射!
例如: 表名 t_user → 实体类 User 这时候就不对应了!
自定义映射关系就可以使用mybatis-plus提供的注解即可!
2. @TableName注解
- 描述:表名注解,标识实体类对应的表
- 使用位置:实体类
@TableName("sys_user") public class User { private Long id; private String name; private Integer age; private String email; }
特殊情况:如果表名和实体类名相同(忽略大小写)可以省略该注解!
其他解决方案:全局设置前缀
mybatis-plus: global-config: db-config: table-prefix: sys_
3. @TableId 注解
- 描述:主键注解
- 使用位置:实体类主键字段
@TableName("sys_user") public class User { @TableId(value="主键列名",type=主键策略) private Long id; private String name; private Integer age; private String email; }
属性
类型
必须指定
默认值
描述
value
String
否
“”
主键字段名
type
Enum
否
IdType.NONE
指定主键类型
IdType 属性可选值:
值
介绍
AUTO
数据库ID自增
NONE
无状态,该类型为未设置主键类型(注解里等于跟随全局,全局里约等于 INPUT)
INPUT
insert前自行set主键值
ASSIGN_ID
分配ID(主键类型为Number(Long和Integer)或String)(since 3.3.0),使用接口IdentifierGenerator的方法nextId(默认实现类为 DefaultIdentifierGenerator雪花算法)
ASSIGN_UUID
分配UUID,主键类型为String(since 3.3.0),使用接口IdentifierGenerator的方法nextUUID(默认default方法)
ID_WORKER
分布式全局唯一ID 长整型类型 (please use ASSIGN_ID)
UUID
32位UUID字符串 (please use ASSIGN_UUID)
ID_WORKER_STR
分布式全局唯一ID 字符串类型 (please use ASSIGN_ID)
全局配置修改主键策略:
mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: table-prefix: t_ id-type: auto
在以下场景下,添加@TableId注解是必要的:
a.实体类的字段与数据库表的主键字段不同名:如果实体类中的字段与数据库表的主键字段不一致,需要使用@TableId注解来指定实体类中表示主键的字段。
b.主键生成策略不是默认策略:如果需要使用除了默认主键生成策略以外的策略,也需要添加@TableId注解,并通过value属性指定生成策略。
雪花算法使用场景
雪花算法(Snowflake Algorithm)是一种用于生成唯一ID的算法。它由Twitter公司提出,用于解决分布式系统中生成全局唯一ID的需求。
在传统的自增ID生成方式中,使用单点数据库生成ID会成为系统的瓶颈,而雪花算法通过在分布式系统中生成唯一ID,避免了单点故障和性能瓶颈的问题。
雪花算法生成的ID是一个64位的整数,由以下几个部分组成:
时间戳:41位,精确到毫秒级,可以使用69年。
节点ID:10位,用于标识分布式系统中的不同节点。
序列号:12位,表示在同一毫秒内生成的不同ID的序号。
通过将这三个部分组合在一起,雪花算法可以在分布式系统中生成全局唯一的ID,并保证ID的生成顺序性。
雪花算法的工作方式如下:
当前时间戳从某一固定的起始时间开始计算,可以用于计算ID的时间部分。
节点ID是分布式系统中每个节点的唯一标识,可以通过配置或自动分配的方式获得。
序列号用于记录在同一毫秒内生成的不同ID的序号,从0开始自增,最多支持4096个ID生成。
需要注意的是,雪花算法依赖于系统的时钟,需要确保系统时钟的准确性和单调性,否则可能会导致生成的ID不唯一或不符合预期的顺序。
雪花算法是一种简单但有效的生成唯一ID的算法,广泛应用于分布式系统中,如微服务架构、分布式数据库、分布式锁等场景,以满足全局唯一标识的需求。
你需要记住的: 雪花算法生成的数字,需要使用Long 或者 String类型主键!!
@TableField
描述:字段注解(非主键)
@TableName("sys_user") public class User { @TableId private Long id; @TableField("nickname") private String name; private Integer age; private String email; }
属性
类型
必须指定
默认值
描述
value
String
否
“”
数据库字段名
exist
boolean
否
true
是否为数据库表字段
MyBatis-Plus会自动开启驼峰命名风格映射!!!
高级扩展 逻辑删除实现 概念:
逻辑删除,可以方便地实现对数据库记录的逻辑删除而不是物理删除。逻辑删除是指通过更改记录的状态或添加标记字段来模拟删除操作,从而保留了删除前的数据,便于后续的数据分析和恢复。
物理删除:真实删除,将对应数据从数据库中删除,之后查询不到此条被删除的数据
逻辑删除:假删除,将对应数据中代表是否被删除字段的状态修改为“被删除状态”,之后在数据库中仍旧能看到此条数据记录
逻辑删除实现:
数据库和实体类添加逻辑删除字段
表添加逻辑删除字段
可以是一个布尔类型、整数类型或枚举类型。
ALTER TABLE USER ADD deleted INT DEFAULT 0 ; # int 类型 1 逻辑删除 0 未逻辑删除
实体类添加逻辑删除属性
@Data public class User { / / @TableId private Integer id; private String name; private Integer age; private String email; @TableLogic / / 逻辑删除字段 int mybatis- plus下,默认 逻辑删除值为1 未逻辑删除0 private Integer deleted; }
全局指定
mybatis-plus: global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0
演示逻辑删除操作
逻辑删除以后,没有真正的删除语句,删除改为修改语句!
删除代码:
@Test public void testQuick5 () { userMapper.deleteById(5 ); }
实际执行:
UPDATE user SET deleted= 1 WHERE id= ? AND deleted= 0
测试查询数据
@Test public void testQuick6 () { userMapper.selectList(null ); }
查询中排除逻辑删除字段以及注意事项
在实体类的对应字段上加上注解@TableField(select=false)
@TableLogic @TableField(select=false) private Integer deleted;
下次再查询的时候这个字段就不会出现
但是在自定义的sql中,逻辑删除的还是会查出来,得看你自己的sql了
乐观锁实现 悲观锁和乐观锁场景和介绍 并发问题场景演示:
乐观锁和悲观锁是在并发编程中用于处理并发访问和资源竞争的两种不同的锁机制!!
悲观锁(认为一定会发生冲突):
悲观锁的基本思想是,在整个数据访问过程中,将共享资源锁定,以确保其他线程或进程不能同时访问和修改该资源。悲观锁的核心思想是”先保护,再修改 “。在悲观锁的应用中,线程在访问共享资源之前会获取到锁,并在整个操作过程中保持锁的状态,阻塞其他线程的访问。只有当前线程完成操作后,才会释放锁,让其他线程继续操作资源。这种锁机制可以确保资源独占性和数据的一致性,但是在高并发环境下,悲观锁的效率相对较低。
乐观锁(任务不能发生冲突):
乐观锁的基本思想是,认为并发冲突的概率较低,因此不需要提前加锁,而是在数据更新阶段进行冲突检测和处理。乐观锁的核心思想是”先修改,后校验 “。在乐观锁的应用中,线程在读取共享资源时不会加锁,而是记录特定的版本信息。当线程准备更新资源时,会先检查该资源的版本信息是否与之前读取的版本信息一致,如果一致则执行更新操作,否则说明有其他线程修改了该资源,需要进行相应的冲突处理。乐观锁通过避免加锁操作,提高了系统的并发性能和吞吐量,但是在并发冲突较为频繁的情况下,乐观锁会导致较多的冲突处理和重试操作。
具体技术和方案:
乐观锁实现方案和技术:
版本号/时间戳:为数据添加一个版本号或时间戳字段,每次更新数据时,比较当前版本号或时间戳与期望值是否一致,若一致则更新成功,否则表示数据已被修改,需要进行冲突处理。
CAS(Compare-and-Swap):使用原子操作比较当前值与旧值是否一致,若一致则进行更新操作,否则重新尝试。
无锁数据结构:采用无锁数据结构,如无锁队列、无锁哈希表等,通过使用原子操作实现并发安全。
悲观锁实现方案和技术:
锁机制:使用传统的锁机制,如互斥锁(Mutex Lock)或读写锁(Read-Write Lock)来保证对共享资源的独占访问。
数据库锁:在数据库层面使用行级锁或表级锁来控制并发访问。
信号量(Semaphore):使用信号量来限制对资源的并发访问。
介绍版本号乐观锁技术的实现流程:
每条数据添加一个版本号字段version
取出记录时,获取当前 version
更新时,检查获取版本号是不是数据库当前最新版本号
如果是[证明没有人修改数据], 执行更新, set 数据更新 , version = version+ 1
如果 version 不对[证明有人已经修改了],我们现在的其他记录就是失效数据!就更新失败
使用mybatis-plus数据使用乐观锁
添加版本号更新插件
@Configuration @MapperScan("com.xxx.mapper") public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor () { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor (); interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor ()); return interceptor; } }
乐观锁字段添加@Version注解
注意: 数据库也需要添加version字段
ALTER TABLE USER ADD VERSION INT DEFAULT 1 ; # int 类型 乐观锁字段
支持的数据类型只有:int,Integer,long,Long,Date,Timestamp,LocalDateTime
仅支持 updateById(id) 与 update(entity, wrapper) 方法
@Version private Integer version;
正常更新使用即可
@Test public void testQuick7 () { User user = userMapper.selectById(5 ); User user1 = userMapper.selectById(5 ); user.setAge(20 ); user1.setAge(30 ); userMapper.updateById(user); userMapper.updateById(user1); }
自动填充 作用:插入自动填充 createTime;更新自动填充 updateTime,不用每次手动 set
步骤 1:数据库增加字段
alter table user_high add column create_time datetime;alter table user_high add column update_time datetime;
步骤 2:实体类注解标记填充策略
@TableField(fill = FieldFill.INSERT) private LocalDateTime createTime;@TableField(fill = FieldFill.INSERT_UPDATE) private LocalDateTime updateTime;
FieldFill 枚举:INSERT / UPDATE / INSERT_UPDATE / DEFAULT(不填充)
步骤 3:自定义填充处理器【核心代码】,交给 Spring 容器
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;import org.apache.ibatis.reflection.MetaObject;import org.springframework.stereotype.Component;import java.time.LocalDateTime;@Component public class MyMetaObjectHandler implements MetaObjectHandler { @Override public void insertFill (MetaObject metaObject) { this .strictInsertFill(metaObject, "createTime" , LocalDateTime::now, LocalDateTime.class); this .strictInsertFill(metaObject, "updateTime" , LocalDateTime::now, LocalDateTime.class); } @Override public void updateFill (MetaObject metaObject) { this .strictUpdateFill(metaObject, "updateTime" , LocalDateTime::now, LocalDateTime.class); } }
对比:setFieldValByName 会强制覆盖用户传入的值,生产优先用 strict 系列
注意点
只有 mybatis-plus 自带的 save、update 方法生效,xml 手写 sql 不填充
字段名必须和实体属性名完全一致,大小写不能错
类型要匹配(LocalDateTime/Date)
性能分析插件
重点:只用于开发测试!绝对不能上生产,性能损耗大
作用:把?占位符替换成真实参数,输出完整 SQL,打印执行耗时,用来排查慢 SQL
步骤 1:引入依赖
<dependency > <groupId > p6spy</groupId > <artifactId > p6spy</artifactId > <version > 3.9.1</version > </dependency >
步骤 2:修改 application.yml 数据源配置
原来 mysql 驱动地址改成 p6spy 驱动,url 前面加 p6spy:
spring: datasource: url: jdbc:p6spy:mysql://127.0.0.1:3306/db_test?useUnicode=true&characterEncoding=utf8 driver-class-name: com.p6spy.engine.spy.P6SpyDriver
步骤 3:resources 下面新建 spy.properties
driverlist =com.mysql.cj.jdbc.Driver appender =com.p6spy.engine.spy.appender.Slf4JLogger logMessageFormat =com.p6spy.engine.spy.appender.CustomLineFormat customLogMessageFormat =耗时:%(executionTime)ms | SQL: %(sqlSingleLine) excludecategories =info,debug,result,batch dateformat =yyyy-MM-dd HH:mm:ss
注意点
改完 url 和 driver,不要写错 p6spy 前缀
生产环境一定要移除依赖 + 恢复原生驱动
多租户插件 三种多租户方案对比
独立数据库:一租户一个库,隔离最高,成本最高
共享库独立 schema:一个库多个 schema
共享库共享表,tenant_id 字段隔离(本插件实现,企业最常用)
步骤 1:数据库加租户字段
alter table user_high add column tenant_id bigint comment '租户id' ;
步骤 2:写租户上下文(ThreadLocal,存当前请求租户 ID)
public class TenantContextHolder { private static final ThreadLocal<Long> TENANT_ID = new ThreadLocal <>(); public static void setTenantId (Long id) { TENANT_ID.set(id); } public static Long getTenantId () { return TENANT_ID.get(); } public static void clear () { TENANT_ID.remove(); } }
一般在拦截器 / 过滤器,从 token/header 解析 tenantId 放入这个 ThreadLocal
步骤 3:自定义租户处理器 TenantLineHandler
import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler; import net.sf.jsqlparser.expression.Expression; import net.sf.jsqlparser.expression.LongValue; import org.springframework.stereotype.Component; @Component public class MyTenantHandler implements TenantLineHandler { / / 获取当前租户ID表达式 @Override public Expression getTenantId() { Long tenantId = TenantContextHolder.getTenantId(); if(tenantId = = null ){ return null ; } return new LongValue(tenantId); } / / 数据库租户字段名 @Override public String getTenantIdColumn() { return "tenant_id"; } / / 返回true = 忽略这张表,不拼接租户条件(系统表、字典表写在这里) @Override public boolean ignoreTable(String tableName) { / / 示例:sys_config表不需要租户隔离 return "sys_config".equals (tableName); } }
步骤 4:配置类注册插件(放在插件列表最前面!)
@Bean public MybatisPlusInterceptor mybatisPlusInterceptor () { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor (); interceptor.addInnerInterceptor(new TenantLineInnerInterceptor (myTenantHandler)); return interceptor; }
知识点
xml 手写 SQL 不会自动追加 tenant_id!必须自己写条件
联表查询每张表最好都写别名,否则插件解析 SQL 容易出错
超级管理员需要跳过租户:在 ignoreTable 或者 getTenantId 返回 null
插入的时候,插件自动给 insert 语句填充 tenant_id,配合自动填充也可以
动态表名插件 场景:日志表按年月分表 log_202610、log_202611,代码里面只写逻辑表名 log
步骤 1:配置插件
@Bean public MybatisPlusInterceptor mybatisPlusInterceptor () { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor (); DynamicTableNameInnerInterceptor dynamicTableNameInnerInterceptor = new DynamicTableNameInnerInterceptor (); dynamicTableNameInnerInterceptor.setTableNameHandler((sql, tableName) -> { if ("log" .equals(tableName)){ String suffix = TableContextHolder.getMonthSuffix(); return tableName + "_" + suffix; } return tableName; }); interceptor.addInnerInterceptor(dynamicTableNameInnerInterceptor); return interceptor; }
使用示例
/ / 代码写逻辑表名 log,插件自动替换成 log_202610logMapper.selectList(null );
知识点
顺序:动态表名插件放在分页前面 ,否则分页 count 的表名替换失败
表后缀不要由前端直接传,后端 ThreadLocal 控制,防止 SQL 注入
只适合规则简单的后缀分表;复杂分表(hash)不推荐这个插件
SQL 注入器
作用:给 BaseMapper 新增全局通用方法,所有 Mapper 继承之后都能用,官方扩展包自带 3 个好用方法:
InsertBatchSomeColumn:MySQL 单 SQL 批量插入(mybatis-plus 自带 saveBatch 是循环单条 insert!性能差)
AlwaysUpdateSomeColumnById:根据 id 更新,固定更新某些字段,跳过指定字段
LogicDeleteByIdWithFill:逻辑删除的同时,自动填充字段
步骤 1:自定义 BaseMapper,扩展方法
import com.baomidou.mybatisplus.core.mapper.BaseMapper;import com.baomidou.mybatisplus.extension.injector.methods.AlwaysUpdateSomeColumnById;import com.baomidou.mybatisplus.extension.injector.methods.InsertBatchSomeColumn;import org.apache.ibatis.annotations.Param;import java.util.List;public interface MyBaseMapper <T> extends BaseMapper <T> { int insertBatchSomeColumn (@Param("list") List<T> entityList) ; int alwaysUpdateSomeColumnById (@Param("et") T entity) ; }
步骤 2:自定义 SqlInjector,注册内置扩展方法
import com.baomidou.mybatisplus.core.injector.DefaultSqlInjector;import com.baomidou.mybatisplus.core.metadata.TableInfo;import com.baomidou.mybatisplus.extension.injector.methods.AlwaysUpdateSomeColumnById;import com.baomidou.mybatisplus.extension.injector.methods.InsertBatchSomeColumn;import org.springframework.stereotype.Component;import java.util.List;@Component public class MySqlInjector extends DefaultSqlInjector { @Override public List<AbstractMethod> getMethodList (Class<?> mapperClass, TableInfo tableInfo) { List<AbstractMethod> methodList = super .getMethodList(mapperClass, tableInfo); methodList.add(new InsertBatchSomeColumn (i -> i.getFieldFill() != FieldFill.UPDATE)); methodList.add(new AlwaysUpdateSomeColumnById ()); return methodList; } }
步骤 3:业务 Mapper 继承 MyBaseMapper
public interface UserHighMapper extends MyBaseMapper <UserHigh> {}
步骤 4:调用
List<UserHigh> list = new ArrayList <>(); userMapper.insertBatchSomeColumn(list);
知识点
InsertBatchSomeColumn 只支持 MySQL ,语法是 values (),(),()
受数据库 max_allowed_packet 限制,一次不要塞几万条,建议分批
自己也可以继承 AbstractMethod 写完全自定义的全局 SQL 方法
防全表更新和删除实现 针对 update 和 delete 语句 作用: 阻止恶意的全表更新删除
添加防止全表更新和删除拦截器
@Bean public MybatisPlusInterceptor mybatisPlusInterceptor () { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor (); interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor ()); return interceptor; } }
测试全部更新或者删除
@Test public void testQuick8 () { User user = new User (); user.setName("custom_name" ); user.setEmail("xxx@mail.com" ); userService.saveOrUpdate(user,null ); }
Mybatisx插件 MyBatis-Plus为我们提供了强大的mapper和service模板,能够大大的提高开发效率
但是在真正开发过程中,MyBatis-Plus并不能为我们解决所有问题,例如一些复杂的SQL,多表联查,我们就需要自己去编写代码和SQL语句,我们该如何快速的解决这个问题呢,这个时候可以使用MyBatisX插件
MyBatisX一款基于 IDEA 的快速开发插件,为效率而生。