当前位置: 首页 > 图灵资讯 > 技术篇> 2023最新MybatisPlus实战教程(五)拓展篇

2023最新MybatisPlus实战教程(五)拓展篇

来源:图灵教育
时间:2023-04-20 16:50:26

  5 5.1【扩展篇】 逻辑删除

  我们已经完成了基本的增加、删除、更改和检查操作,但对于删除操作,我们考虑了一个问题。我们真的会在实际开发中从数据库中删除数据吗?

  当然不是。这里举个例子:在电商网站上,我们会上架很多商品。这些商品下架后,如果我们从数据库中删除这些商品,我们应该在年底统计商品数据信息,所以我们不能删除这些商品信息。

  如果商场里的商品下架了,我们会从数据库中删除商品

  到了年终总结的时候,我们要总结一下今年的销售额,发现少了2万,这绝对不合理。因此,我们不能真正删除数据。在这里,我们将采用逻辑删除方案。逻辑删除的操作是添加一个字段来表示数据的状态。如果需要删除一个数据,我们可以通过改变数据的状态来实现它,这不仅可以表示数据被删除,而且可以保留数据以便将来进行统计。让我们来实现这种效果。[1]首先在表中添加一列字段,以表示是否删除。我们在这里使用的字段类型是int类型。1表示该数据可用,0表示该数据不可用

  [2]实体类为Integer添加了一个字段,用于对应表中的字段@Data@AllArgsConstructor@NoArgsConstructorpublic class User extends Model { private Long id; private String name; private Integer age; private String email; @TableLogic(value = "1",delval = "0") private Integer status;}

  [3]测试逻辑删除效果 @Testvoid logicDelete(){ userMapper.deleteById(7L);}

  查看拼接的SQL语句,我们发现在执行删除操作时,语句变成了修改,将数据状态从1变为0,表示数据为删除状态

  逻辑删除的效果也可以通过全局配置来实现5.2 通用枚举

  首先,让我们回顾一下枚举。什么是枚举?当我们想表达一组信息时,这组信息只能从一些固定值中选择,不能随意编写。在这种情况下,枚举非常合适。

  例如,如果我们想表达性别,性别只有两个值,要么是男性,要么是女性,那么我们可以用枚举来描述性别。[1]我们首先在表中添加一个字段来表示性别,这里我们通常使用int来描述,因为int类型可以通过0和1来表示两个不同的性别

  【2】编写枚举类 public enum GenderEnum { _MAN_(0,“男”) _WOMAN_(1,“女”; private Integer gender; private String genderName; GenderEnum(Integer gender, String genderName) { this.gender = gender; this.genderName = genderName; }}

  [3]实体类添加相关字段 @Data@AllArgsConstructor@NoArgsConstructorpublic class User extends Model { private Long id; private String name; private Integer age; private String email; private GenderEnum gender; private Integer status;}

  [4]添加数据 @Testvoid enumTest(){ User user = new User(); user.setName("liu"); user.setAge(29); user.setEmail("liu@powernode.com"); user.setGenderEnum(GenderEnum._MAN_); user.setStatus(1); userMapper.insert(user);}

  此时,当我们检查控制台时,我们会发现添加失败

  原因是我们不能将枚举类型作为int数字插入数据库。然而,我们给出了相应的int值的枚举类型,所以在这里我们只需要一个配置,我们可以将枚举类型作为数字插入数据库,添加@enumvalue注释 public enum GenderEnum { _MAN_(0,“男”) _WOMAN_(1,"女"); @EnumValue private Integer gender; private String genderName; GenderEnum(Integer gender, String genderName) { this.gender = gender; this.genderName = genderName; }}

  此时,我们再次执行添加操作,发现数据可以成功添加,数据库中插入了枚举类型的值5.3 字段类型处理器

  在某些情况下,我们使用Map集合作为属性来接收前端传输的数据,但当这些数据存储在数据库中时,我们使用json格式的数据来存储,json本质上是一个字符串,即varchar类型。那么如何实现实体Map类型和数据库Varchar类型的相互转换,这里需要使用字段类型处理器来完成。[1]我们首先在实体类中添加一个字段,Map类型 @Data@AllArgsConstructor@NoArgsConstructorpublic class User extends Model { private Long id; private String name; private Integer age; private String email; private GenderEnum gender; private Integer status; private Map contact;**//联系方式**}

  [2]我们在数据库中添加一个字段,为varchar类型

  [3]在实体类中添加相应的注释,实现不同类型的数据转换,使用字段类型处理器 @Data@AllArgsConstructor@NoArgsConstructor@TableName(autoResultMap = true)**///查询时将json字符串封装为Map集合**public class User extends Model { private Long id; private String name; private Integer age; private String email; private GenderEnum gender; private Integer status; @TableField(typeHandler = FastjsonTypeHandler.class)**///指定字段类型处理器 **private Map contact;**//联系方式**}

  [4]字段类型处理器依赖于Json处理器Fastjson,因此我们需要引入相应的依赖 com.alibaba fastjson 1.2.76

  [5]测试添加操作 @Testvoid typeHandler(){ User user = new User(); user.setName("zhang"); user.setAge(28); user.setEmail("zhang@powernode.com"); user.setGender(GenderEnum._MAN_); user.setStatus(1); HashMap contact = new HashMap<>(); contact.put("phone","010-1234567"); contact.put("tel","13388889999"); user.setContact(contact); userMapper.insert(user);}

  SQL语句如下 ==> Preparing: INSERT INTO powershop_user ( id, name, age, email, gender, status, contact ) VALUES ( ?, ?, ?, ?, ?, ?, ? )==> Parameters: 1617915941843202049(Long), zhang(String), 28(Integer), zhang@powernode.com(String), 0(Integer), 1(Integer), {"phone":"010-1234567","tel":"13388889999"}(String)<== Updates: 1

  通过观察SQL语句,我们发现当插入Map类型的字段时,字段将转换为String类型

  查看数据库中的信息,发现添加成功

  [6]测试查询操作,发现从数据库中查询的数据已转移到Map集合 @Testvoid typeHandlerSelect(){ List users = userMapper.selectList(null); System._out_.println(users);} 5.4 自动填充功能

  项目中有一些属性。如果不想每次都填,可以设置为自动填充,如常见时间、创建时间、更新时间等。[1]在数据库表中添加两个字段

  请注意,只有设置下划线和小驼峰映射,这种mysql写法才能与实体类完成映射

  [2]在实体类中添加相应的字段,并指定需要自动填充的属性的填充时间

  @Data@NoArgsConstructor@AllArgsConstructor@TableName(autoResultMap = true)public class User extends Model {@TableIdprivate Long id;private String name;private Integer age;private String email;private Integer status;private GenderEnum gender;@TableField(typeHandler = FastjsonTypeHandler.class)private Map contact;** **@TableField(fill = FieldFill.INSERT)private Date createTime;@TableField(fill = FieldFill.INSERT_UPDATE)private Date updateTime;}

  [3]编制自动填充处理器,指定填充策略 @Componentpublic class MyMetaHandler implements MetaObjectHandler { @Override public void insertFill(MetaObject metaObject) { setFieldValByName("createTime",new Date(),metaObject); setFieldValByName("updateTime",new Date(),metaObject); } @Override public void updateFill(MetaObject metaObject) { setFieldValByName("updateTime",new Date(),metaObject); }}

  [4]插入前在这里设置mysql时区

  通过检查发现,当前时间时间正常

  [5]然后将配置文件的时区修改为servertimezone=Asia/Shanghai

  [6]测试插入操作 @Testvoid testFill(){ User user = new User(); user.setName("wang"); user.setAge(35); user.setEmail("wang@powernode.com"); user.setGender(GenderEnum._MAN_); user.setStatus(1); HashMap contact = new HashMap<>(); contact.put("phone","010-1234567"); contact.put("tel","13388889999"); userMapper.insert(user);}

  [7]测试更新操作 @Testvoid testfilll2(){ User user = new User(); user.setId(6L); user.setName("wang"); user.setAge(35); user.setEmail("wang@powernode.com"); user.setGender(GenderEnum._MAN_); user.setStatus(1); HashMap contact = new HashMap<>(); contact.put("phone","010-1234567"); contact.put("tel","13388889999"); userMapper.updateById(user);} 5.5 防全表更新和删除插件

  在实际开发中,全表更新和删除是一个非常危险的操作。在Mybatisplus中,提供插件并防止此类危险操作,首先演示全表更新的场景 @Testpublic void testUpdateAll(){ User user = new User(); user.setGender(GenderEnum._MAN_); userService.saveOrUpdate(user,null);}

  这很危险

  如何解决?注入Mybatisplusinterceptor类,配置blockattackinerinterceptor拦截器 @Configurationpublic class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType._MYSQL_)); interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor()); return interceptor; }}

  测试全表更新时,会出现抛出异常,防止全表更新 @SpringBootTestpublic class QueryTest { @Autowired private UserService userService;@Testvoid allUpdate(){ User user = new User(); user.setId(999L); user.setName("wang"); user.setEmail("wang@powernode.com"); userService.saveOrUpdate(user,null);}} 5.6 5.6.MybatisX快速开发插件.1 安装

  Mybatisx是IDEA提供的插件,旨在简化Mybatis和Mybatisplus框架。让我们来看看如何在IDEA中安装插件[1],首先选择File -> Settings

  [2]选择Pluginss

  [3]搜索MybatisX,点击安装

  [4]这种效果是安装完成的

  在已安装的目录下,我们可以看到这个IDEA插件

  [6]这时,我们勾选他,表示启用

  【7】重启IDEA,使插件生效。到目前为止,MybatisX插件已经安装好了5.6.2 功能

  安装插件后,让我们来看看插件的功能1、Mapper接口和映射文件的跳转功能

  2、逆向工程逆向工程是通过数据库表结构逆向生成Java工程的结构,包括以下几点:(1)实体类(2)Mapper接口

  (3)Mapper映射文件(4)Service接口(5)Service实现类在这里创建一个测试逆向工程功能的模块

  引入依赖,编制相应的配置文件信息 org.springframework.boot spring-boot-starter org.springframework.boot spring-boot-starter-test test mysql mysql-connector-java 8.0.31 com.alibaba druid 1.2.8 com.baomidou mybatis-plus-boot-starter 3.5.3 org.projectlombok lombok org.springframework.boot spring-boot-starter-web spring: datasource: username: root password: root url: jdbc:mysql://localhost:3306/mybatisplus?serverTimezone=UTC&characterEncoding=utf8&useUnicode=true&useSSL=false driver-class-name: com.mysql.cj.jdbc.Driver

  首先,使用IDEA连接mysqlll

  [2]填写连接信息,测试连接通过

  [3]找到表右键,选择插件的逆向工程选项

  [4]编制逆向工程配置信息

  [5]编写生成信息

  [6]观察生成结构,发现以下内容已生成(1)实体类(2)Mapper接口(3)Mapper映射文件(4)Service接口(5)Service映射文件。

  接下来,我们将在Mapper界面上添加@Mapper注释 @Mapperpublic interface UserMapper extends BaseMapper {}

  [8]测试代码环境 生成常见的需求代码

  虽然Mapper界面提供了一些常见的方法,我们可以直接使用这些常见的方法来完成sql操作,但对于实际场景中各种复杂的操作需求,仍然不够,所以mybatisx提供了更多的方法,并可以直接生成相应的sql句子,使开发更加简单。根据名称联想的常见操作 @Mapperpublic interface UserMapper extends BaseMapper { **///添加操作 **int insertSelective(User user); **///删除操作 **int deleteByNameAndAge(@Param("name") String name, @Param("age") Integer age); **///修改操作 **int updateNameByAge(@Param("name") String name, @Param("age") Integer age); **///查询操作 **List selectAllByAgeBetween(@Param("beginAge") Integer beginAge, @Param("endAge") Integer endAge);}

  在映射配置文件中,我们不需要编写相应的sql insert into powershop_user id, name, age, email, values #{id,jdbcType=BIGINT}, #{name,jdbcType=VARCHAR}, #{age,jdbcType=INTEGER}, #{email,jdbcType=VARCHAR}, delete from powershop_user where name = #{name,jdbcType=VARCHAR} AND age = #{age,jdbcType=NUMERIC} update powershop_user set name = #{name,jdbcType=VARCHAR} where age = #{age,jdbcType=NUMERIC} 5.7 乐观锁

  首先要了解开发中的一个常见场景,叫做并发请求。并发请求是同时有多个请求,同时请求服务器资源。如果获取信息,没有问题,但如果修改信息,就会出现问题。这里举一个具体的例子,例如,目前只剩下一个库存,这次有很多用户想购买商品,发起了购买商品的请求,所以可以让很多用户购买,当然不是,因为很多用户购买商品,所以会出现超卖问题,库存不足不能交付。因此,在开发中有必要解决这种超卖的问题

  除了超卖场景,还有很多并发访问场景。这类场景的核心问题是,在执行请求的过程中,其他请求不能更改数据。如果是一个完整的请求,其他请求在请求过程中没有修改数据,则该请求可以正常修改数据。如果在变更数据的过程中有其他变更数据的请求,则该请求不会变更该数据。

  要解决这类问题,最常见的想法就是加锁,锁可以用来验证在执行要求的过程中,是否有数据发生变化。有两种常见的数据库锁类型,悲观锁和乐观锁。完成的修改操作是先查询数据,然后修改数据。悲观锁:悲观锁在查询时锁定数据,在此请求完成前不会释放锁。这个请求完成后,释放锁,释放锁后,其他请求可以读写这个数据

  这样做可以确保读取的信息是当前的信息,确保信息的正确性,但并发效率很低,在实际开发中很少使用悲观锁,因为我们需要确保并发时的效率。

  乐观锁:乐观锁是通过表字段设计的。他的核心思想是在阅读时不添加锁。其他请求仍然可以读取此数据。修改时,判断数据是否已修改。如果已修改,请求的修改操作将无效。

  Updatete是通过sql实现的 表 set 字段 = 新值,version = version + 1 where version = 1

  这样的操作不会影响数据读取,并发效率更高。然而,目前可能看到的数据不是真实的信息数据,而是在修改之前,但在许多情况下是可以容忍的,影响不大。例如,我们经常看到库存,或者我们都加入了购物车,但点击后库存消失了。

  接下来,让我们来看看乐观锁的使用[1]在数据库表中添加字段version,表示版本,默认值为1

  产生后的效果

  [2]找到实体类,添加相应的属性,并使用@Version标记为乐观的锁定字段信息 @Data@NoArgsConstructor@AllArgsConstructorpublic class User { private Long id; private String name; private Integer age; private String email; @Version private Integer version;}

  [3]由于需要增强每个修改语句的完成语句,这里我们通过拦截器的配置,在执行每个修改的SQL语句时添加版本控制的功能 @Configurationpublic class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor(){ MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor()); return interceptor; }}

  [4]测试效果,这里我们模拟先查询,然后修改 @Testvoid updateTest(){ User user = userMapper.selectById(6L); user.setName("li"); userMapper.updateById(user);}

  通过查看拼接好的SQL语句,我们发现USER的数据是在查询过程中查询出来的,包含version版本的信息

  当我们完成修改时,他会修改版本号 + 1

  此时查看数据发现,更改姓名后,version已为2

  接下来,让我们模拟多个修改请求是否能达到乐观锁的效果。乐观锁的效果是,一个请求允许另一个请求在修改过程中查询,但修改将通过版本号是否更改来决定是否修改。如果版本号更改,则证明已要求修改数据,则此修改不生效。如果版本号没有更改,则完成修改。 @Testvoid updatetest2(){ **////模拟操作1查询操作 **User user1 = userMapper.selectById(6L); **////模拟操作2查询操作 **User user2 = userMapper.selectById(6L); **////模拟操作2修改操作 **user2.setName("lisi"); userMapper.updateById(user2); **////模拟操作1修改操作 **user1.setName("zhangsan"); userMapper.updateById(user1);}

  让我们来看看这个代码的执行过程。这个代码实际上是两个操作,但操作1在执行过程中完成了数据修改。此时,操作1无法再次修改操作1查询:此时版本为2

  操作2查询:此时版本为2

  操作2修改:此时检查版本,版本没有变化,所以修改完成,版本改为3

  操作1修改:此时检查版本,版本已经改变了原始版本信息,所以结束修改5.8 代码生成器

  代码生成器和逆向工程的区别在于,代码生成器可以生成更多的结构和内容,并允许我们配置更多的选项。在这里,我们将展示代码生成器的使用。[1]参考官方网站,使用代码生成器需要引入两个依赖 **** com.baomidou mybatis-plus-generator 3.5.3**** org.freemarker freemarker 2.3.31

  [2]编写代码生成器代码 @SpringBootTestclass GeneratorApplicationTests { public static void main(String[] args) { FastAutoGenerator._create_("jdbc:mysql://localhost:3306/mybatisplus?serverTimezone=UTC&characterEncoding=utf8&useUnicode=true&useSSL=false", "root", "root") .globalConfig(builder -> { builder.author("powernode") **// 设置作者 //.enableSwagger() // 开启 swagger 模式 **.fileOverride() **// 覆盖已生成的文件 **.outputDir("D://"); **// 指定输出目录 **}) .packageConfig(builder -> { builder.parent("com.powernode") **// 设置父包名 **.moduleName("mybatisplus") **// 设置父包模块名 **.pathInfo(Collections._singletonMap_(OutputFile._xml_, "D://")); **// 设置mapperXml生成路径 **}) .strategyConfig(builder -> { builder.addInclude("powershop_user") **// 设置需要生成的表名 **.addTablePrefix("powershop"); **// 设置滤表前缀 **}) .templateEngine(new FreemarkerTemplateEngine()) **// 使用Freemarker引擎模板,Velocity引擎模板默认 **.execute(); }}

  [3]执行,检查生成效果5.9 进行SQL分析打印

  在我们的日常开发工作中,我们不可避免地要检查当前程序执行的SQL语句,并了解其执行时间,以便于分析是否存在慢SQL问题。我们可以使用Mybatisplus提供的SQL分析打印功能,以获得SQL语句执行时间。[1]由于该功能依赖于p6spy组件,因此需要在pom中执行.首先将组件引入xml p6spy p6spy 3.9.1

  [2]application.在yml中配置驱动和url修改 spring: datasource: driver-class-name: com.p6spy.engine.spy.P6SpyDriver url: jdbc:p6spy:mysql

  [3]在resources下创建 spy.properties配置文件3.2.1以上使用

  modulelist=com.baomidou.mybatisplus.extension.p6spy.MybatisPlusLogFactory,com.p6spy.engine.outage.P6OutageFactory打印自定义日志

  logMessageFormat=com.baomidou.mybatisplus.extension.p6spy.P6SpyLogger#控制台appender输出=com.baomidou.mybatisplus.extension.p6spy.StdoutLogger使用日志系统记录 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,commit,resultset日期格式

  dateformat=yyyy-MM-dd HH:mm:ss可以有多个实际驱动

  #driverlist=org.h2.Driver是否打开慢SQL记录

  outagedetection=true慢SQL记录标准 2 秒

  outagedetectioninterval=2

  [4]测试执行查询所有操作,可以看到sql语句的执行时间5.10 多数据源

  在学习多数据源之前,让我们了解一下分库表。当一个项目的数据库数据非常大时,在完成SQL操作时,需要检索更多的数据。我们将遇到性能问题和SQL执行效率低下的问题。为了解决这个问题,我们的解决方案是将一个数据库中的数据拆分到多个数据库中,以减少单个数据库的数据量,并从分享访问请求的压力和减少单个数据库的数据量两个方面提高效率。让我们演示一下如何在Mybatisplus中演示数据源切换的效果[1]首先创建一个新的模块,并复制以前模块中的内容如下

  【2】引入依赖 com.baomidou dynamic-datasource-spring-boot-starter 3.1.0

  [3]创建新的数据库,提供多数据源环境

  数据库

  表数据

  [4]编制配置文件,指定多数据源信息 spring: datasource: dynamic: primary: master strict: false datasource: master: username: root password: root url: jdbc:mysql://localhost:3306/mybatisplus?serverTimezone=UTC&characterEncoding=utf8&useUnicode=true&useSSL=false driver-class-name: com.mysql.cj.jdbc.Driver slave_1: username: root password: root url: jdbc:mysql://localhost:3306/mybatisplus2serverTimezone=UTC&characterEncoding=utf8&useUnicode=true&useSSL=false driver-class-name: com.mysql.cj.jdbc.Driver

  [5]创建多个Service,用@DS注释描述不同的数据源信息 @Service@DS("master")public class UserServiceImpl extends ServiceImpl implements UserService {} @Service@DS("slave_1")public class Userserviceimpl2 extends ServiceImpl implements UserService{}

  [6]测试service多数据源环境执行结果 @SpringBootTestclass Mp03ApplicationTestst { @Autowired private UserServiceImpl userServiceImpl; @Autowired private Userserviceimpl2 userserviceimpl2; @Test public void select(){ User user = userServiceImpl.getById(1L); System._out_.println(user); } @Test public void select2(){ User user = userserviceimpl2.getById(1L); System._out_.println(user); }}

  [7]观察测试结果,发现结果可以从两个数据源中获得