Appearance
高级功能的演示需要用到一张新的表
user2 DROPTABLEIF EXISTSuser2; CREATETABLEuser2 ( id BIGINT(20) PRIMARYKEY NOTNULLCOMMENT ' 主键
- id',
- name VARCHAR(30) DEFAULTNULLCOMMENT '
姓名
- ',
- age INT(11) DEFAULTNULLCOMMENT '
年龄
- ',
- email VARCHAR(50) DEFAULTNULLCOMMENT '
邮箱
- ',
- manager_id BIGINT(20) DEFAULTNULLCOMMENT '
直属上级
- id',
- create_time DATETIME DEFAULTNULLCOMMENT '
创建时间
- ',
- update_time DATETIME DEFAULTNULLCOMMENT '
修改时间
- ',
- version INT(11) DEFAULT'1'COMMENT '
版本
- ',
- deleted INT(1) DEFAULT'0'COMMENT '
逻辑删除标识,0-未删除,1-已删除
', CONSTRAINTmanager_fk FOREIGNKEY(manager_id) REFERENCESuser2(id) ) ENGINE =INNODB CHARSET=UTF8; INSERTINTOuser2(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')
并创建对应的实体类
User2 packagecom.example.mp.po; importlombok.Data; importjava.time.LocalDateTime; @DatapublicclassUser2{ privateLong id; privateString name; privateInteger age; privateString email; privateLong managerId; privateLocalDateTime createTime; privateLocalDateTime updateTime; privateInteger version; privateInteger deleted; } 以及Mapper接口
packagecom.example.mp.mappers; importcom.baomidou.mybatisplus.core.mapper.BaseMapper; importcom.example.mp.po.User2; publicinterfaceUser2MapperextendsBaseMapper<User2> { } 逻辑删除 首先,为什么要有逻辑删除呢?直接删掉不行吗?当然可以,但日后若想要恢复,或者需要查看这些数据,就做不到了。逻辑删除是为了方便数据恢复,和保护数据本身价值的一种方案。 日常中,我们在电脑中删除一个文件后,也仅仅是把该文件放入了回收站,日后若有需要还能进行查看或恢复。当我们确定不再需要某个文件,可以将其从回收站中彻底删除。这也是类似的道理。 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项复制代码 测试代码
packagecom.example.mp; importcom.example.mp.mappers.User2Mapper; importcom.example.mp.po.User2; importorg.junit.Test; importorg.junit.runner.RunWith; importorg.springframework.beans.factory.annotation.Autowired; importorg.springframework.boot.test.context.SpringBootTest; importorg.springframework.test.context.junit4.SpringRunner; importjava.util.List; @RunWith(SpringRunner.class)@SpringBootTestpublicclassLogicDeleteTest{ @AutowiredprivateUser2Mapper mapper; @TestpublicvoidtestLogicDel(){ inti=mapper.deleteById(6); System.out.println("rowAffected = "+ i); } } 结果
可以看到,发出的SQL不再是DELETE,而是UPDATE 此时我们再执行一次
SELECT @TestpublicvoidtestSelect(){ List<User2> users = mapper.selectList(null); } 可以看到,发出的SQL语句,会自动在WHERE后面拼接逻辑未删除的条件。查询出来的结果中,没有了id为6的王软蛋。 若想要SELECT的列,不包括逻辑删除的那一列,则可以在实体类中通过@TableField进行配置 @TableField(select = false)privateInteger deleted; 复制代码 可以看到下图的执行结果中,SELECT中已经不包含deleted这一列了
前面在application.yml中做的配置,是全局的。通常来说,对于多个表,我们也会统一逻辑删除字段的名称,统一逻辑已删除和未删除的值,所以全局配置即可。当然,若要对某些表进行单独配置,在实体类的对应字段上使用@TableLogic即可 @TableLogic(value = "0", delval = "1")privateInteger deleted; 复制代码 小结 开启mp的逻辑删除后,会对SQL产生如下的影响 • INSERT语句:没有影响 • SELECT语句:追加WHERE条件,过滤掉已删除的数据 • UPDATE语句:追加WHERE条件,防止更新到已删除的数据 • DELETE语句:转变为UPDATE语句 **注意,上述的影响,只针对mp自动注入的SQL生效。**如果是自己手动添加的自定义SQL,则不会生效。比如
publicinterfaceUser2MapperextendsBaseMapper<User2> { @Select("select * from user2")List<User2> selectRaw(); } 复制代码 调用这个selectRaw,则mp的逻辑删除不会生效。 另,逻辑删除可在application.yml中进行全局配置,也可在实体类中用@TableLogic进行局部配置。 自动填充 表中常常会有“新增时间”,“修改时间”,“操作人” 等字段。比较原始的方式,是每次插入或更新时,手动进行设置。mp可以通过配置,对某些字段进行自动填充,食用示例如下 1. 在实体类中的某些字段上,通过@TableField设置自动填充
publicclassUser2{ privateLong id; privateString name; privateInteger age; privateString email; privateLong managerId; @TableField(fill = FieldFill.INSERT)// 插入时自动填充
privateLocalDateTime createTime; @TableField(fill = FieldFill.UPDATE)// 更新时自动填充
privateLocalDateTime updateTime; privateInteger version; privateInteger deleted; } 复制代码 2. 实现自动填充处理器
packagecom.example.mp.component; importcom.baomidou.mybatisplus.core.handlers.MetaObjectHandler; importorg.apache.ibatis.reflection.MetaObject; importorg.springframework.stereotype.Component; importjava.time.LocalDateTime;
@Component//需要注册到Spring容器中publicclassMyMetaObjectHandlerimplementsMetaObjectHandler{
@OverridepublicvoidinsertFill(MetaObject metaObject){ // 插入时自动填充// 注意第二个参数要填写实体类中的字段名称,而不是表的列名称
strictFillStrategy(metaObject, "createTime", LocalDateTime::now); }
@OverridepublicvoidupdateFill(MetaObject metaObject){ // 更新时自动填充
strictFillStrategy(metaObject, "updateTime", LocalDateTime::now); } } 复制代码 测试
@Testpublicvoidtest(){ User2user=newUser2(); user.setId(8L); user.setName(" 王一蛋
"); user.setAge(29); user.setEmail("yd@baomidou.com"); user.setManagerId(2L); mapper.insert(user); } 复制代码 根据下图结果,可以看到对createTime进行了自动填充
注意,自动填充仅在该字段为空时会生效,若该字段不为空,则直接使用已有的值。如下
@Testpublicvoidtest(){ User2user=newUser2(); user.setId(8L); user.setName(" 王一蛋
"); user.setAge(29); user.setEmail("yd@baomidou.com"); user.setManagerId(2L); user.setCreateTime(LocalDateTime.of(2000,1,1,8,0,0)); mapper.insert(user); } 复制代码
更新时的自动填充,测试如下
@Testpublicvoidtest(){ User2user=newUser2(); user.setId(8L); user.setName(" 王一蛋
"); user.setAge(99); mapper.updateById(user); } 复制代码
乐观锁插件 当出现并发操作时,需要确保各个用户对数据的操作不产生冲突,此时需要一种并发控制手段。悲观锁的方法是,在对数据库的一条记录进行修改时,先直接加锁(数据库的锁机制),锁定这条数据,然后再进行操作;而乐观锁,正如其名,它先假设不存在冲突情况,而在实际进行数据操作时,再检查是否冲突。乐观锁的一种通常实现是版本号,在MySQL中也有名为MVCC的基于版本号的并发事务控制。 在读多写少的场景下,乐观锁比较适用,能够减少加锁操作导致的性能开销,提高系统吞吐量。 在写多读少的场景下,悲观锁比较使用,否则会因为乐观锁不断失败重试,反而导致性能下降。 乐观锁的实现如下: 1. 取出记录时,获取当前
- version
更新时,带上这个
- version
执行更新时,
set version = newVersion where version = oldVersion 4. 如果oldVersion与数据库中的version不一致,就更新失败 这种思想和CAS(Compare And Swap)非常相似。 乐观锁的实现步骤如下 1. 配置乐观锁插件 packagecom.example.mp.config;
importcom.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor; importorg.springframework.context.annotation.Bean; importorg.springframework.context.annotation.Configuration;
@ConfigurationpublicclassMybatisPlusConfig{ /** 3.4.0 以后的mp版本,推荐用如下的配置方式
/@BeanpublicMybatisPlusInterceptor mybatisPlusInterceptor(){ MybatisPlusInterceptorinterceptor=newMybatisPlusInterceptor(); interceptor.addInnerInterceptor(newOptimisticLockerInnerInterceptor()); returninterceptor; } / 旧版mp可以采用如下方式。注意新旧版本中,新版的类,名称带有Inner, 旧版的不带, 不要配错了
*// @Bean public OptimisticLockerInterceptor opLocker() { return new OptimisticLockerInterceptor(); } */} 复制代码 2. 在实体类中表示版本的字段上添加注解
@Version @DatapublicclassUser2{ privateLong id; privateString name; privateInteger age; privateString email; privateLong managerId; privateLocalDateTime createTime; privateLocalDateTime updateTime; @VersionprivateInteger version; privateInteger deleted; } 复制代码 测试代码
@TestpublicvoidtestOpLocker(){ intversion=1; // 假设这个version是先前查询时获得的
User2user=newUser2(); user.setId(8L); user.setEmail("version@baomidou.com"); user.setVersion(version); inti=mapper.updateById(user); } 复制代码 执行之前先看一下数据库的情况
根据下图执行结果,可以看到SQL语句中添加了version相关的操作
当UPDATE返回了1,表示影响行数为1,则更新成功。反之,由于WHERE后面的version与数据库中的不一致,匹配不到任何记录,则影响行数为0,表示更新失败。更新成功后,新的version会被封装回实体对象中。 实体类中version字段,类型只支持int,long,Date,Timestamp,LocalDateTime 注意,乐观锁插件仅支持updateById(id)与update(entity, wrapper)方法 **注意:如果使用wrapper,则wrapper不能复用!**示例如下
@TestpublicvoidtestOpLocker(){ User2user=newUser2(); user.setId(8L); user.setVersion(1); user.setAge(2); // 第一次使用
LambdaQueryWrapper<User2> wrapper = newLambdaQueryWrapper<>(); wrapper.eq(User2::getName, " 王一蛋
"); mapper.update(user, wrapper); // 第二次复用
user.setAge(3); mapper.update(user, wrapper); } 复制代码
可以看到在第二次复用wrapper时,拼接出的SQL中,后面WHERE语句中出现了2次version,是有问题的。 性能分析插件 该插件会输出SQL语句的执行时间,以便做SQL语句的性能分析和调优。 注:3.2.0版本之后,mp自带的性能分析插件被官方移除了,而推荐食用第三方性能分析插件 食用步骤 1. 引入maven依赖
- <dependency>
- <groupId>p6spy</groupId>
- <artifactId>p6spy</artifactId>
- <version>3.9.1</version>
- </dependency>
复制代码 2. 修改
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:rootpassword:root复制代码 3. 在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的执行时长被记录了下来
多租户SQL解析器 多租户的概念:多个用户共用一套系统,但他们的数据有需要相对的独立,保持一定的隔离性。 多租户的数据隔离一般有如下的方式: • 不同租户使用不同的数据库服务器 优点是:不同租户有不同的独立数据库,有助于扩展,以及对不同租户提供更好的个性化,出现故障时恢复数据较为简单。 缺点是:增加了数据库数量,购置成本,维护成本更高 • 不同租户使用相同的数据库服务器,但使用不同的数据库(不同的schema) 优点是购置和维护成本低了一些,缺点是数据恢复较为困难,因为不同租户的数据都放在了一起 • 不同租户使用相同的数据库服务器,使用相同的数据库,共享数据表,在表中增加租户id来做区分 优点是,购置和维护成本最低,支持用户最多,缺点是隔离性最低,安全性最低 食用实例如下 添加多租户拦截器配置。添加配置后,在执行CRUD的时候,会自动在SQL语句最后拼接租户id的条件
packagecom.example.mp.config; importcom.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; importcom.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler; importcom.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor; importnet.sf.jsqlparser.expression.Expression; importnet.sf.jsqlparser.expression.LongValue; importorg.springframework.context.annotation.Bean; importorg.springframework.context.annotation.Configuration; @ConfigurationpublicclassMybatisPlusConfig{ @BeanpublicMybatisPlusInterceptor mybatisPlusInterceptor(){ MybatisPlusInterceptorinterceptor=newMybatisPlusInterceptor(); interceptor.addInnerInterceptor(newTenantLineInnerInterceptor(newTenantLineHandler() { @OverridepublicExpression getTenantId(){ // 返回租户id的值, 这里固定写死为1// 一般是从当前上下文中取出一个 租户
idreturnnewLongValue(1); } /** ** 通常会将表示租户id的列名,需要排除租户id的表等信息,封装到一个配置类中(如TenantConfig)
**/@OverridepublicString getTenantIdColumn()\{
// 返回表中的表示租户id的列名
return"manager_id"; } @OverridepublicbooleanignoreTable(String tableName){ // 表名不为 user2 的表, 不拼接多租户条件
return!"user2".equals(tableName); } }));
//
如果用了分页插件注意先 add TenantLineInnerInterceptor 再 add PaginationInnerInterceptor// 用了分页插件必须设置
MybatisConfiguration#useDeprecatedExecutor = falsereturninterceptor; } } 复制代码 测试代码
@TestpublicvoidtestTenant(){ LambdaQueryWrapper<User2> wrapper = newLambdaQueryWrapper<>(); wrapper.likeRight(User2::getName, " 王
") .select(User2::getName, User2::getAge, User2::getEmail, User2::getManagerId); user2Mapper.selectList(wrapper); } 复制代码
动态表名SQL解析器 当数据量特别大的时候,我们通常会采用分库分表。这时,可能就会有多张表,其表结构相同,但表名不同。例如order_1,order_2,order_3,查询时,我们可能需要动态设置要查的表名。mp提供了动态表名SQL解析器,食用示例如下 先在mysql中拷贝一下user2表
配置动态表名拦截器
packagecom.example.mp.config; importcom.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; importcom.baomidou.mybatisplus.extension.plugins.handler.TableNameHandler; importcom.baomidou.mybatisplus.extension.plugins.inner.DynamicTableNameInnerInterceptor; importorg.springframework.context.annotation.Bean; importorg.springframework.context.annotation.Configuration; importjava.util.HashMap; importjava.util.Random; @ConfigurationpublicclassMybatisPlusConfig{ @BeanpublicMybatisPlusInterceptor mybatisPlusInterceptor(){ MybatisPlusInterceptorinterceptor=newMybatisPlusInterceptor(); DynamicTableNameInnerInterceptordynamicTableNameInnerInterceptor=newDynamicTableNameInnerInterceptor(); HashMap<String, TableNameHandler> map = newHashMap<>(); // 对于user2表,进行动态表名设置
map.put("user2", (sql, tableName) -> { String_="_"; intrandom=newRandom().nextInt(2) + 1; returntableName + _ + random; // 若返回null, 则不会进行动态表名替换, 还是会使用
user2}); dynamicTableNameInnerInterceptor.setTableNameHandlerMap(map); interceptor.addInnerInterceptor(dynamicTableNameInnerInterceptor); returninterceptor; } } 复制代码 测试
@TestpublicvoidtestDynamicTable(){ user2Mapper.selectList(null); } 复制代码
总结 • 条件构造器AbstractWrapper中提供了多个方法用于构造SQL语句中的WHERE条件,而其子类QueryWrapper额外提供了select方法,可以只选取特定的列,子类UpdateWrapper额外提供了set方法,用于设置SQL中的SET语句。除了普通的Wrapper,还有基于lambda表达式的Wrapper,如LambdaQueryWrapper,LambdaUpdateWrapper,它们在构造WHERE条件时,直接以方法引用来指定WHERE条件中的列,比普通Wrapper通过字符串来指定要更加优雅。另,还有链式Wrapper,如LambdaQueryChainWrapper,它封装了BaseMapper,可以更方便地获取结果。 • 条件构造器采用链式调用来拼接多个条件,条件之间默认以AND连接 • 当AND或OR后面的条件需要被括号包裹时,将括号中的条件以lambda表达式形式,作为参数传入and()或or() 特别的,当()需要放在WHERE语句的最开头时,可以使用nested()方法 • 条件表达式时当需要传入自定义的SQL语句,或者需要调用数据库函数时,可用apply()方法进行SQL拼接 • 条件构造器中的各个方法可以通过一个boolean类型的变量condition,来根据需要灵活拼接WHERE条件(仅当condition为true时会拼接SQL语句) • 使用lambda条件构造器,可以通过lambda表达式,直接使用实体类中的属性进行条件构造,比普通的条件构造器更加优雅 • 若mp提供的方法不够用,可以通过自定义SQL(原生mybatis)的形式进行扩展开发 • 使用mp进行分页查询时,需要创建一个分页拦截器(Interceptor),注册到Spring容器中,随后查询时,通过传入一个分页对象(Page对象)进行查询即可。单表查询时,可以使用BaseMapper提供的selectPage或selectMapsPage方法。复杂场景下(如多表联查),使用自定义SQL。 • AR模式可以直接通过操作实体类来操作数据库。让实体类继承自Model即可 (完)
来自
<https://juejin.cn/post/6961721367846715428\>
