Mybatis-Plus(进阶)

一、ActiveRecord模式

​ ActiveRecord也属于ORM(对象关系映射)层,由Rails最早提出,遵循标准的ORM模型:表映射到记录,记录映射到对象,字段映射到对象属性。配合遵循的命名和配置惯例,能够很大程度的快速实现模型的操作,而且简洁易懂。

ActiveRecord的主要思想是:

  • 每一个数据库表对应创建一个类,类的每一个对象实例对应于数据库中表的一行记录;通常表的每个字段在类中都有相应的Field;

  • ActiveRecord同时负责把自己持久化,在ActiveRecord中封装了对数据库的访问,即CURD;;

  • ActiveRecord是一种领域模型(Domain Model),封装了部分业务逻辑;

二、使用ActiveRecord

使用ActiveRecord需要再实体类上继承Model接口,同时也需要有一个Mapper接口实现BaseMapper接口,并指定泛型.

例如:

UserDemo实体类继承了Model

@Component
@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("user")
public class UserDemo extends Model<UserDemo> {
    
    
    @TableId(value = "id")
    private Long id;
    //select如果为false表示不从数据库查询该字段
    @TableField(select = true )
    private String name;
    private Integer age;
    private String email;
    //插入数据时自动填充数据,需要配置插件
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime insertTime;
    //修改数据时自动填充数据,需要配置插件
    @TableField(fill = FieldFill.UPDATE)
    private LocalDateTime updateTime;
}

同时需要有个Mapper接口继承BaseMapper并指定泛型为UserDemo

@Repository
public interface UserDemoMapper extends BaseMapper<UserDemo>{
    
    
}

测试

    @Test
    public void testActiveRecord(){
    
    
            List<UserDemo> users = userDemo.selectAll();
            users.forEach(System.out::println);
        }

结果如下:成功查询到数据

image-20201002204658559

但是如果只有实体类没有Mapper接口就会报错如下:

image-20201002204637904

Model抽象类里面也有通用的CRUD,可以直接使用,这里就不一一演示了

image-20201002204910403

三、Mybatis-Plus常用插件

3.1、插件简介

​ MyBatis 允许你在映射语句执行过程中的某一点进行拦截调用。默认情况下,MyBatis 允许使用插件来拦截的方法调用包括:

  • Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
  • ParameterHandler (getParameterObject, setParameters)
  • ResultSetHandler (handleResultSets, handleOutputParameters)
  • StatementHandler (prepare, parameterize, batch, update, query)

image-20201002210032812

这些类中方法的细节可以通过查看每个方法的签名来发现,或者直接查看 MyBatis 发行包中的源代码。 如果你想做的不仅仅是监控方法的调用,那么你最好相当了解要重写的方法的行为。 因为在试图修改或重写已有方法的行为时,很可能会破坏 MyBatis 的核心模块。 这些都是更底层的类和方法,所以使用插件的时候要特别当心。

插件的作用就是为了增强功能,它通过拦截器拦截需要增强的方法,通过动态代理,为被拦截的方法增强功能

3.2、如何使用插件

要想自定义插件必须要实现Interceptor接口,这个接口中有三个方法需要实现。

image-20201002210352760

方法 作用
intercept 这个方法是mybatis的核心方法,要实现自定义逻辑,基本都是改造这个方法,其中invocation参数可以通过反射要获取原始方法和对应参数信息
plugin 它的作用是用来生成一个拦截对方,也就是代理对象,使得被代理的对象一定会经过intercept方法,通常都会使用mybatis提供的工具类Plugin来获取代理对象,如果有自己独特需求,可以自定义
setProperties 这个方法就是用来设置插件的一些属性

@Intercepts注解就是用来标明拦截4个接口中的那个接口和接口中的哪些方法。如下自定义一个拦截器插件

@Intercepts({
    
    @Signature(type = Executor.class,method = "query",
        args = {
    
    MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}
), @Signature(type = Executor.class,method = "query",
        args = {
    
    MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}
)}
)

public class MyInteceptor implements Interceptor {
    
    
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
    
    

        System.out.println("");
        System.out.println("");
        System.out.println("");
        System.out.println("");
        System.out.println("成功拦截查询!!!");
        System.out.println("");
        System.out.println("");
        System.out.println("");
        System.out.println("");
        Object proceed = invocation.proceed();
        return proceed;
    }

    @Override
    public Object plugin(Object target) {
    
    
        return Plugin.wrap(target,this);
    }

    @Override
    public void setProperties(Properties properties) {
    
    

    }
}

  //将拦截器添加到容器中  
	@Bean
    public MyInteceptor myInteceptor(){
    
    
      return new MyInteceptor();
    }

对Executor接口的query方法进行了拦截,并通过动态代理生成代理类,此时,执行查询方法就会别拦截

测试:

  @Test
        public void testSelect(){
    
    
          userMapper.selectById(2);     
        }

如下图,成功对查询方法进行拦截,并在控制台打印语句

image-20201003133809846

3.3、执行分析插件

在MP中提供了对SQL执行的分析的插件,可用作阻断全表更新、删除的操作,注意:该插件仅适用于开发环境,不适用于生产环境

//注册SqlExplainInterceptor(高版本已经被标记过时)
	@Bean
    public SqlExplainInterceptor sqlExplainInterceptor(){
    
    
        SqlExplainInterceptor sqlExplainInterceptor = new SqlExplainInterceptor();
        List<ISqlParser> sqlParserList = new ArrayList<>();
        // 攻击 SQL 阻断解析器、加入解析链
        sqlParserList.add(new BlockAttackSqlParser());
        sqlExplainInterceptor.setSqlParserList(sqlParserList);
        return sqlExplainInterceptor;
    }
   //删除表中所有的数据  
	@Test
    public void sqlExplainTest(){
    
    
            userMapper.delete(null);
        }

结果如下,控制台抛出异常,禁止全表删除

image-20201003134713407

3.4、性能分析插件

性能分析拦截器,用于输出每条 SQL 语句及其执行时间,可以设置最大执行时间,超过时间会抛出异常。同样该插件只适合开发环境。

导入p6spy的pom依赖

	<!--SQL分析插件依赖-->
        <dependency>
            <groupId>p6spy</groupId>
            <artifactId>p6spy</artifactId>
            <version>3.9.0</version>
        </dependency>

使用p6spy需要修改jdbc的URL和Dirver

spring:
  datasource:
    username: 'root'
    password: 'root'
    url: jdbc:p6spy:mysql://localhost:3306/mybatis_plus?useSSL=false&serverTimezone=Asia/Shanghai&useLegacyDatetimeCode=false
    driver-class-name: com.p6spy.engine.spy.P6SpyDriver

同时在resources目录下创建spy.properties文件(MP官网文档复制的)

#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

测试:

 @Test
        public void testSelect(){
    
    
          userMapper.selectBatchIds(Arrays.asList(1,2,3));
        }

结果如图:

image-20201003140406746

可以通过logMessageFormat和customLogMessageFormat自定义日志的输出格式如下:

logMessageFormat=com.p6spy.engine.spy.appender.CustomLineFormat
customLogMessageFormat=%(currentTime) \n SQL耗时: %(executionTime) ms \n 连接信息: %(category)-%(connectionId) \n 执行语句: %(sqlSingleLine)

image-20201003142006822

可以选择的变量如下

  • %(connectionId)connection id
  • %(currentTime):当前时间
  • %(executionTime):执行耗时
  • %(category):执行分组
  • %(effectiveSql):提交的 SQL 换行
  • %(effectiveSqlSingleLine):提交的 SQL 不换行显示
  • %(sql):执行的真实 SQL 语句,已替换占位
  • %(sqlSingleLine):执行的真实 SQL 语句,已替换占位不换行显示

3.5、乐观锁插件

使用乐观锁的意图是当要更新一条记录的时候,希望这条记录没有被别人更新。

乐观锁实现方式:

  • 取出记录时,获取当前version

  • 更新时,带上这个version

  • 执行更新时, set version = newVersion where version = oldVersion

  • 如果version不对,就更新失败

乐观锁配置需要2步 记得两步

  1. 插件配置
//高版本中已经被标记过时
@Bean
public OptimisticLockerInterceptor optimisticLockerInterceptor() {
    
    
    return new OptimisticLockerInterceptor();
}
  1. 注解实体字段@Version,必需要!!
@Version
private Integer version;

特别说明:

  • 支持的数据类型只有:int,Integer,long,Long,Date,Timestamp,LocalDateTime
  • 整数类型下 newVersion = oldVersion + 1
  • newVersion 会回写到 entity
  • 仅支持 updateById(id)update(entity, wrapper) 方法
  • update(entity, wrapper) 方法下, wrapper 不能复用!!!

实体类如下:

@Component
@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("tb_user")
public class User extends Model<User> {
    
    
    private Long id;
    private String userName;
    private String password;
    private String name;
    private Integer age;
    private String email;
    @Version
    private Integer version;
}

表结构如下:

image-20201003143326848

测试:

@Test
    public void testOptimisticLock(){
    
    
          User user1 = userMapper.selectById(1l);
          User user2 = userMapper.selectById(2l);
          user1.setAge(80);
          user2.setAge(90);
          int result = this.userMapper.updateById(user1);
          int result2 = this.userMapper.updateById(user2);
        }

结果如下:

image-20201003144856156

image-20201003144905029

第一条修改语句成功,第二条修改语句失败,因为修改数据的时候要判断取出数据时的version和修改数据时数据库的version是否一致。第一条语句修改过后,数据库的version变为了1,而user2的version是0,此时再去修改数据,就与数据库version不一致,更新失败.

3.6、逻辑删除插件

所谓逻辑删除就是将数据标记为删除,而并非真正的物理删除(非DELETE操作),查询时需要携带状态条件,确保被标记的数据不被查询到。这样做的目的就是避免数据被真正的删除。

使用逻辑删除还是需要两个步骤:

  1. application.yaml添加配置(步骤一)
mybatis-plus:
  global-config:
    db-config:
      logic-delete-field: deleted  # 全局逻辑删除的实体字段名(since 3.3.0,配置后可以忽略不配置步骤2)
      logic-delete-value: 1 # 逻辑已删除值(默认为 1)
      logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
  1. 实体类加上@TableLogic注解(步骤二)

    @Component
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    @TableName("tb_user")
    public class User extends Model<User> {
          
          
        private Long id;
        private String userName;
        private String password;
        private String name;
        private Integer age;
        private String email;
        @Version
        private Integer version;
        @TableLogic
        private Integer deleted;
    }
    

表字段:

image-20201003150808525

测试:

   @Test
    public void testLogicDelete(){
    
    
            QueryWrapper<User> wrapper = new QueryWrapper<>();
            wrapper.eq("id",1l);
            userMapper.delete(wrapper);
            userMapper.selectById(1l);
        }

image-20201003151458947

image-20201003151557838

删除操作只是将deleted置为1,查询时将deleted作为条件查询,数据库中数据并没有被真正删除。

注意在向有逻辑删除的表插入数据:

  1. 字段在数据库定义默认值(推荐)
  2. insert 前自己 set 值
  3. 使用自动填充功能

四、Sql注入器

全局配置 sqlInjector 用于注入 ISqlInjector 接口的子类,实现自定义方法注入。

public interface ISqlInjector {
    
    

    /**
     * <p>
     * 检查SQL是否注入(已经注入过不再注入)
     * </p>
     *
     * @param builderAssistant mapper 信息
     * @param mapperClass      mapper 接口的 class 对象
     */
    void inspectInject(MapperBuilderAssistant builderAssistant, Class<?> mapperClass);
}

自定义自己的通用方法可以实现接口 ISqlInjector 也可以继承抽象类 AbstractSqlInjector 注入通用方法 SQL 语句 然后继承 BaseMapper 添加自定义方法,全局配置 sqlInjector 注入 MP 会自动将类所有方法注入到 mybatis 容器中。

4.1、编写MyBaseMapper

@Repository
public interface MyBaseMapper extends BaseMapper<User> {
    
    

    /**
     * 自定义查询所有方法
     * @return
     */
    List findAll();

}

4.2、自定义Sql注入器

如果直接继承AbstractSqlInjector的话,原有的BaseMapper中的方法将失效,所以我们选择继承DefaultSqlInjector进行扩展。

public class MySqlInjector extends DefaultSqlInjector {
    @Override
    public List<AbstractMethod> getMethodList(Class<?> mapperClass) {
         List<AbstractMethod> methodList = super.getMethodList(mapperClass);
         //扩充自定义的方法
        methodList.add(new FindAll());
        return methodList;
    }
}

4.3、编写FindAll实体类

public class FindAll extends AbstractMethod {
    
    
    @Override
    public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
    
    

        String sql = String.format("<script>%s SELECT %s FROM %s %s %s\n</script>", sqlFirst(), sqlSelectColumns(tableInfo, true), tableInfo.getTableName(),
                sqlWhereEntityWrapper(true, tableInfo), sqlComment());
        SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, modelClass);
        return this.addSelectMappedStatementForTable(mapperClass, "findAll", sqlSource, tableInfo);
    }
}

4.4、注入到容器中

  @Bean
    public MySqlInjector mySqlInjector(){
    
    
        return new MySqlInjector();
    }

4.5、测试

 @Autowired
        MyBaseMapper MyBaseMapper;

        @Test
    public void testFindAll(){
    
    
            List<User> all = MyBaseMapper.findAll();
            System.out.println(all);
        }

结果如下:成功查询到数据

image-20201003160405009

五、自动填充功能

原理:

  • 实现元对象处理器接口:com.baomidou.mybatisplus.core.handlers.MetaObjectHandler
  • 注解填充字段 @TableField(.. fill = FieldFill.INSERT) 生成器策略部分也可以配置!
  1. 实现MetaObjectHandler接口
@Component
public class DateHandler implements MetaObjectHandler {
    
    
    /**
    *插入时给insertTime自动填充
    */
    @Override
    public void insertFill(MetaObject metaObject) {
    
    
        setFieldValByName("insertTime", LocalDateTime.now(),metaObject);
    }

    /**
    *修改时给updateTime自动填充
    */
    @Override
    public void updateFill(MetaObject metaObject) {
    
    
        setFieldValByName("updateTime", LocalDateTime.now(),metaObject);
    }
}

实体字段:

@Component
@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("user")
public class UserDemo extends Model<UserDemo> {
    
    
    private Long id;
    private String name;
    private Integer age;
    private String email;
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime insertTime;
    @TableField(fill = FieldFill.UPDATE)
    private LocalDateTime updateTime;

}

FieldFill可选

  • DEFAULT :默认不处理
  • INSERT:插入时填充
  • UPDATE:更新时填充
  • INSERT_UPDATE:插入和更新时填充

数据库表

image-20201003161406980

image-20201003161505605

插入测试:

   @Test
    public void testFillInsert(){
    
    
            UserDemo userDemo = new UserDemo();
            userDemo.setEmail("[email protected]");
            userDemo.setAge(100);
            userDemo.setName("马化腾");
            userDemoMapper.insert(userDemo);
        }

结果:插入数据成功,insert_time字段自动填充了当前时间

image-20201003161902111

image-20201003161915000

更新测试:

    @Test
    public void testFillInsert(){
    
    
            UserDemo userDemo2 = new UserDemo();
            userDemo2.setEmail("[email protected]");
            userDemo2.setAge(100);
            userDemo2.setName("强子");
            QueryWrapper<UserDemo> wrapper = new QueryWrapper<>();
            wrapper.eq("id",14);
            userDemoMapper.update(userDemo2,wrapper);
        }

结果:修改成功,update_time自动填充当前时间

image-20201003162234375

image-20201003162250535

Mybatis-Plus还支持通用枚举,以及代码生成器,多数据源等等一系列的功能,个位如果需要可以前往官方文档https://baomidou.com/查看,中国人写的文档,看起来很轻松的。最后给大家推荐一个IDEA的插件MybatisX,可以实现Java接口与XML文件的跳转,可以为Mapper方法自动生成XML

猜你喜欢

转载自blog.csdn.net/qq_44134480/article/details/108911433