Skip to content

高级功能的演示需要用到一张新的表

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\>

本站总访问 — 次