• 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) 
     SQL耗时: %(executionTime) ms 
     连接信息: %(category)-%(connectionId) 
     执行语句: %(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
    </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("163@qq.com");
                userDemo.setAge(100);
                userDemo.setName("马化腾");
                userDemoMapper.insert(userDemo);
            }
    

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

    image-20201003161902111

    image-20201003161915000

    更新测试:

        @Test
        public void testFillInsert(){
                UserDemo userDemo2 = new UserDemo();
                userDemo2.setEmail("jd@qq.com");
                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

  • 相关阅读:
    【二分图匹配/匈牙利算法】飞行员配对方案问题
    【模板/学习】匈牙利算法
    【tarjan缩点+分层图】草鉴定Grass Cownoisseur
    【微笑】
    【质因数分解】SAC E#1 一道中档题 Factorial
    【dfs+dp】砝码称重
    【背包dp】自然数拆分Lunatic版
    【单调队列】最大子序和
    【单调队列】滑动窗口
    bzoj 2834: 回家的路
  • 原文地址:https://www.cnblogs.com/myblogstart/p/13764679.html
Copyright © 2020-2023  润新知