zoukankan      html  css  js  c++  java
  • mybatis从入门到精通(其他章节)

    8. mybatis缓存配置

    1.一级缓存

    springboot是在开启事务的情况下是开启一级缓存的,不开启事务的情况下不开启一级缓存。
    一级缓存是指,在对查询的方法和结果存储到hashMap中,key值由方法名和参数列表决定的,value值是查询到的结果
    如果再次进行相同的查询那么会取hashMap中的值而不是数据库中的值,这样做会残生一些问题。

    测试

    @Transactional
    @Test
    void test1Cache(){
        SysUser sysUser = userMapper.selectByPrimaryKey(1L);
        sysUser.setUserName("new name");
        SysUser sysUser1 = userMapper.selectByPrimaryKey(1L);
        Assert.assertEquals(sysUser,sysUser1);
        log.info(sysUser1.getUserName());
    }
    

    这里的断言是通过的,也就是说sysUser和sysUser1指向的是相同的内存地址,要小心这种情况,可能你以为sysUser1是数据库中的值

    关闭一级缓存可以通过添加select标签中的flushCache="true"那么执行完查询后会清除当前的sqlSession所有的缓存,在进行增,删,改操作并成功提交的情况下会清空一级缓存。

    2.二级缓存

    mybatis开启二级缓存,先要全局性的打开缓存配置

    <!--全局性地开启或关闭所有映射器配置文件中已配置的任何缓存。默认值为true-->
    <setting name="cacheEnabled" value="true"/>
    

    在mapper.xml文件中添加配置,也可以在mapper接口文件中添加

    <!--开启二级缓存,
    该文件下的select语句都会被缓存,
    所有的delete update insert都会刷新缓存
    缓存默认使用LRU算法收回~eviction
    缓存不会按照时间顺序自动刷新~flushInterval 单位毫秒
    缓存中会存储集合或者对象1024个引用~size
    缓存会被视为read/ write(可读/可写)的,意味着对象检索不是共享的,而且可以安全地被调用者修改,而不干扰其他调用者或线程所做的潜在修改。
    readOnly="true" 是说返回的对象是相同的实例,这提高了性能(但是只要修改了对象的属性,会导致数据库与缓存中的数据不一致会产生脏读),
    默认为false,那么mybatis会通过序列化返回对象的拷贝,这就需要查询出来的对象实现可序列化接口
    -->
    <cache
     eviction="FIFO"
     flushInterval="60000"
     size = "512"
     readOnly="true"
    />
    

    测试

    @Test
    void test1Cache(){
        SysUser sysUser = userMapper.selectByPrimaryKey(1L);
        sysUser.setUserName("new name");
        SysUser sysUser1 = userMapper.selectByPrimaryKey(1L);
        Assert.assertEquals(sysUser,sysUser1);
        log.info(sysUser1.getUserName());
    }
    

    image-20201031111314924

    由于第一次select语句未执行所以命中率为0,这里是让 readOnly="true"所以修改对象以后,第二条读到的数据在缓存和数据库中的数据不一致是脏读

    改为false并且令SysUser继承Serializable接口可以解决这个问题

    什么是序列化?

    3.二级缓存使用的情况

    1. 已查询为主的表,该表很少进行增,删,改操作
    2. 绝大多数都是单表查询时,很少出现与其他的表相关联
      1. 如果要通过两张表查询数据,可能出现脏读的情况,可以使用参照缓存来解决。但是如果几十张表都已不同的关联关系存在时,显然参照缓存也不起作用了。当某几个表可以作为一个业务整体时,通常是让几个会关联的ER表同时使用同一个二级缓存,这样就能解决脏数据问题。
    3. 可以按照业务划分对表进行分组,如果关联的表比较少可以使用参照缓存

    9. Mybatis插件开发

    Mybatis允许在已映射语句执行过程中的某一点进行拦截调用。默认情况下, Mybatis允许使用插件来拦截的接口包括以下几个。

    Executor
    ParameterHandler
    ResultSetHandler
    StatementHandler
    

    想要自定义插件就得自定义拦截器,通过继承interceptor接口,在该类上配置拦截器注解

    1. interceptor

    public interface Interceptor {
    
      Object intercept(Invocation invocation) throws Throwable;
    
      default Object plugin(Object target) {
        return Plugin.wrap(target, this);
      }
    
      default void setProperties(Properties properties) {
        // NOP
      }
    
    }
    

    setProperties设置插件的参数用来改变插件的行为,插件的参数在mybatis.config中配置的时候设置

    <plugins>
        <plugin interceptor="com.yogurt.plugin.MybatisInterceptor">
            <property name="pro1" value="value1"/>
            <property name="pro2" value="value2"/>
        </plugin>
    </plugins>
    

    plugin(target:被拦截的对象)该方法会在创建被拦截接口对象时调用

    Plugin.wrap(target, this)会自动判断拦截器的签名被拦截对象的接口是否匹配,只有匹配的情况下才会动态代理拦截对象???

    intercept:mybatis运行时要执行的拦截方法,通过invocation参数可以获取以下信息

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        //获取当前被拦截的对象
        Object target = invocation.getTarget();
        //获取当前被拦截的方法
        Method method = invocation.getMethod();
        //获取当前被拦截的方法的参数
        Object[] args = invocation.getArgs();
        //真正的执行被拦截的方法就是 method.invoke(target,args);
        Object proceed = invocation.proceed();
        return null;
    }
    

    NOTE:

    当配置多个拦截器时, Mybatis会遍历所有拦截器,按顺序执行拦截器的plugin方法,被拦截的对象就会被层层代理。在执行拦截对象的方法时,会一层层地调用拦截器,拦截器通过 Invocation.proceed()调用下一层的方法,直到真正的方法被执行。方法执行的结果会从最里面开始向外一层层返回,所以如果存在按顺序配置的A、B、C三个签名相同的拦截器,Mybaits会按照C>B>A> target.proceed()>A>B>C的顺序执行。如果A、B、C签名不同,就会按照 Mybatis 拦截对象的逻辑执行。

    2. 拦截器签名

    拦截器签名通过两个注解实现,@Intercepts和@Signature

    @Intercepts注解中的属性是一个@Signature数组。用来拦截多个方法

    type :需要拦截的类,从上面提到的四个中间选

    method和args可以唯一定位到一个方法

    @Intercepts(
            @Signature(
                    type = ResultSetHandler.class,
                    method = "handleResultSets",
                    args = Statement.class
            )
    )
    

    note: 可以被拦截的四个接口中的方法并不是都可以被拦截,详情请看《mybatis从入门到精通》

    3.开发一个下划线键值转驼峰式插件

    需求:我们在处理mybatis查询结果的时候为了方便扩展,有时候会使用Map作为返回值,那么返回回来的Map的key就是数据库中的列名,是下划线形式的,使用起来特别不方便,所以需要在结果返回回来之后进行处理,需要开发一个下划线键值转驼峰式插件,这个插件可以通过拦截ResultSetHandler中的 handleResultSets(Statement stmt) 方法来实现

    ResultSetHandler中的 List handleResultSets(Statement stmt) throws SQLException;

    该方法是在存储过程返回值不为Cursor时调用,拦截处理Mybatis的查询结果特别有效,并且这个接口被调用的位置在处理二级缓存之前,因此通过这种方式处理的结果可以在二级缓存中体现。

    /**
     * 下滑线转驼峰插件
     */
    @Intercepts(
            @Signature(
                    type = ResultSetHandler.class,
                    method = "handleResultSets",
                    args = {Statement.class}
            )
    )
    @SuppressWarnings({"unchecked", "rawTypes"})
    public class UnderscoreToCamelCaseInterceptor implements Interceptor {
    
        @Override
        public Object intercept(Invocation invocation) throws Throwable {
            //拿到该方法的返回值,这可不是乱强转的,是因为被拦截的方法返回值是List<E>才能这么转
            List<Object> list = (List<Object>) invocation.proceed();
            for (Object object : list) {
                if (object instanceof Map) {
                    processMap((Map<String, Object>) object);
                } else {
                    break;
                }
            }
            return list;
        }
    
        /**
         * 处理map类型
         */
        private void processMap(Map<String, Object> map) {
            Set<String> set = map.keySet();//set里面没有实现Iterator,所以转一下
            HashSet<String> keySet = new HashSet<>(set);
            for (String key : keySet) {
                //将大写开头的字母转为小写,如果包含下划线则转换为驼峰
                if (key.charAt(0) >= 'A' && key.charAt(0) <= 'Z' || key.indexOf("_") > 0) {
                    Object value = map.get(key);
                    map.remove(key);
                    map.put(underscoreToCamelCaseKey(key), value);
                }
            }
        }
    
        /**
         * 将下划线分格转换成驼峰分格
         */
        private String underscoreToCamelCaseKey(String key) {
            StringBuilder sb = new StringBuilder();
            //设置一个flag表示一个单词的结束,下一个单词的开始
            boolean nextUpperCase = false;
            //遍历key的每个字母
            for (int i = 0; i < key.length(); i++) {
                char c = key.charAt(i);
                if (c == '_') {
                    if (sb.length() > 0) {
                        //如果这个当前字符是下划线,且不是sb的开头(有时候列名是_role_name的)那么下一个字母就是大写的
                        nextUpperCase = true;
                    }
                } else {
                    if (nextUpperCase) {
                        sb.append(Character.toUpperCase(c));
                        nextUpperCase = false;//重置默认标记
                    } else {
                        sb.append(Character.toLowerCase(c));
                    }
                }
            }
            return sb.toString();
        }
    }
    

    最后在mybatis-config.xml文件中配置一下就能实现需求了。

    <plugins>
        <plugin interceptor="com.yogurt.plugin.UnderscoreToCamelCaseInterceptor"/>
    </plugins>
    

    4.实现分页插件

    在实现分页查询的时候需要添加分页条件offset、limit,且由于每个数据库的分页实现都不一样,还要databaseId判断。如果要查询总数还要增加一个count并手动添加另外一条sql语句,所以显得sql十分臃肿,这时候可以通过拦截器来实现分页功能。

    分页插件需要拦截的方法:

    Executor 中的 query方法,这里注意是四个参数的,还有跟多参数的query方法由于Mybatis的内部实现无法被拦截

    <E> List<E> query(MappedStatement ms//执行sql用的
        , Object parameter//参数
        , RowBounds rowBounds//包含了offset limit,后面还有PageRowBounds继承了RowBounds
        , ResultHandler resultHandler//实现对结果的处理
                     ) throws SQLException;
    

    实现分页插件需要两个关键的类:PageInterceptor和Dialect

    PageInterceptor负责实现分页和查询总数的逻辑,Dialect是一个接口,不同的数据库有不同的实现方式,主要是实现了查询总数的Sql,和分页sql的拼接等。

    1. PageInterceptor

    @Intercepts(
       @Signature(
          type = Executor.class, 
          method = "query", 
          args = {MappedStatement.class, Object.class, 
                RowBounds.class, ResultHandler.class}
       )
    )
    public class PageInterceptor implements Interceptor {
        private static final List<ResultMapping> EMPTY_RESULTMAPPING
              = new ArrayList<ResultMapping>(0);
        private Dialect dialect;
        private Field additionalParametersField;
    
       @Override
        public Object intercept(Invocation invocation) throws Throwable {
            //获取拦截方法的参数
            Object[] args = invocation.getArgs();
            MappedStatement ms = (MappedStatement) args[0];
            Object parameterObject = args[1];
            RowBounds rowBounds = (RowBounds) args[2];
            //使用rowBounds参数判断是否需要进行分页,如果不需要,直接返回结果
            if (!dialect.skip(ms.getId(), parameterObject, rowBounds)) {
               ResultHandler resultHandler = (ResultHandler) args[3];
                //当前的目标对象~被拦截的对象
                Executor executor = (Executor) invocation.getTarget();
                //获取执行的select ~ sql语句 例如 select * from xxx
                BoundSql boundSql = ms.getBoundSql(parameterObject);
    
                //反射获取动态参数
                Map<String, Object> additionalParameters = 
                      (Map<String, Object>) additionalParametersField.get(boundSql);
                //根据当前rowBounds的类型是不是PageRowBounds来判断是否需要进行 count 查询
                if (dialect.beforeCount(ms.getId(), parameterObject, rowBounds)){
                   //根据当前的 ms 创建一个返回值为 Long 类型的 ms
                    MappedStatement countMs = newMappedStatement(ms, Long.class);
                    //创建 count 查询的缓存 key
                    CacheKey countKey = executor.createCacheKey(
                          countMs, 
                          parameterObject, 
                          RowBounds.DEFAULT, 
                          boundSql);
                    //调用方言获取 count sql
                    String countSql = dialect.getCountSql(
                          boundSql, 
                          parameterObject, 
                          rowBounds, 
                          countKey);
                    //根据countSql获取到BoundSql对象
                    BoundSql countBoundSql = new BoundSql(
                          ms.getConfiguration(), 
                          countSql, 
                          boundSql.getParameterMappings(), 
                          parameterObject);
                    //当使用动态 SQL 时,可能会产生临时的参数,这些参数需要手动设置到新的 BoundSql 中
                    for (String key : additionalParameters.keySet()) {
                        countBoundSql.setAdditionalParameter(
                              key, additionalParameters.get(key));
                    }
                    //执行 count 查询
                    Object countResultList = executor.query(
                          countMs, 
                          parameterObject, 
                          RowBounds.DEFAULT, 
                          resultHandler, 
                          countKey, 
                          countBoundSql);
                    Long count = (Long) ((List) countResultList).get(0);
                    //处理查询总数,把数据库中的总条数放进PageRowBounds的total中
                    dialect.afterCount(count, parameterObject, rowBounds);
                    //没有数据不执行之后的流程
                    if(count == 0L){
                       //当查询总数为 0 时,直接返回空的结果
                       return dialect.afterPage(
                             new ArrayList(), 
                             parameterObject, 
                             rowBounds); 
                    }
                }
                //根据有没有rowBounds参数来判断是否需要进行分页查询
                if (dialect.beforePage(ms.getId(), parameterObject, rowBounds)){
                   //生成分页的缓存 key
                    CacheKey pageKey = executor.createCacheKey(
                          ms, 
                          parameterObject, 
                          rowBounds, 
                          boundSql);
                    //调用方言获取分页 sql,每个数据库的分页查询都不一样这里获取的是Mysql的分页查询
                    String pageSql = dialect.getPageSql(
                          boundSql, 
                          parameterObject, 
                          rowBounds, 
                          pageKey);
                    BoundSql pageBoundSql = new BoundSql(
                          ms.getConfiguration(), 
                          pageSql, 
                          boundSql.getParameterMappings(), 
                          parameterObject);
                    //设置动态参数
                    for (String key : additionalParameters.keySet()) {
                        pageBoundSql.setAdditionalParameter(
                              key, additionalParameters.get(key));
                    }
                    //执行分页查询
                    List resultList = executor.query(
                          ms, 
                          parameterObject, 
                          RowBounds.DEFAULT, 
                          resultHandler, 
                          pageKey, 
                          pageBoundSql);
                    //返回resultList
                    return dialect.afterPage(resultList, parameterObject, rowBounds);
                }
            }
            //返回默认查询
            return invocation.proceed();
        }
    
        /**
         * 根据现有的 ms 创建一个新的,使用新的返回值类型
         *
         * @param ms
         * @param resultType
         * @return
         */
        public MappedStatement newMappedStatement(
              MappedStatement ms, Class<?> resultType) {
            MappedStatement.Builder builder = new MappedStatement.Builder(
                  ms.getConfiguration(), 
                  ms.getId() + "_Count", 
                  ms.getSqlSource(), 
                  ms.getSqlCommandType()
            );
            builder.resource(ms.getResource());
            builder.fetchSize(ms.getFetchSize());
            builder.statementType(ms.getStatementType());
            builder.keyGenerator(ms.getKeyGenerator());
            if (ms.getKeyProperties() != null 
                  && ms.getKeyProperties().length != 0) {
                StringBuilder keyProperties = new StringBuilder();
                for (String keyProperty : ms.getKeyProperties()) {
                    keyProperties.append(keyProperty).append(",");
                }
                keyProperties.delete(
                      keyProperties.length() - 1, keyProperties.length());
                builder.keyProperty(keyProperties.toString());
            }
            builder.timeout(ms.getTimeout());
            builder.parameterMap(ms.getParameterMap());
            //count查询返回值int
            List<ResultMap> resultMaps = new ArrayList<ResultMap>();
            ResultMap resultMap = new ResultMap.Builder(
                  ms.getConfiguration(), 
                  ms.getId(), 
                  resultType, 
                  EMPTY_RESULTMAPPING).build();
            resultMaps.add(resultMap);
            builder.resultMaps(resultMaps);
            builder.resultSetType(ms.getResultSetType());
            builder.cache(ms.getCache());
            builder.flushCacheRequired(ms.isFlushCacheRequired());
            builder.useCache(ms.isUseCache());
            return builder.build();
        }
    
        @Override
        public Object plugin(Object target) {
            return Plugin.wrap(target, this);
        }
    
        @Override
        public void setProperties(Properties properties) {
            String dialectClass = properties.getProperty("dialect");
            try {
                //这里是初始化了MySqlDialect它实现了Dialect接口
                dialect = (Dialect) Class.forName(dialectClass).newInstance();
            } catch (Exception e) {
                throw new RuntimeException(
                      "使用 PageInterceptor 分页插件时,必须设置 dialect 属性");
            }
            //这里时也没干
            dialect.setProperties(properties);
            try {
                //反射获取 BoundSql 中的 additionalParameters 属性
                //additionalParameters是一个map对象。
                additionalParametersField = BoundSql.class.getDeclaredField(
                      "additionalParameters");
                //使得private也能用,可以使用该对象获取键和值
                additionalParametersField.setAccessible(true);
            } catch (NoSuchFieldException e) {
                throw new RuntimeException(e);
            }
        }
    
    }
    

    该类的逻辑是:拿到dialect的实例,这个实例是配置插件的时候就通过property设置好的。先判断是否进行分页查询,再判断是否要查询数据库的总条数,如果进行总条数查询则查询到数据库的总条数,如果数据库的总条数是0就直接返回一个空的ArrayList。再判断是否执行分页查询(这一层其实我觉得多余了)。。

    在controller层传过来的所有的参数除了rowBounds都会保存到query的parameterObject中,如果你还是用了动态sql(if where..)那么在动态sql中使用的参数都会保存到additionalParameters中,需要让BoundSql添加这些动态sql需要的参数

    2. dialect

    public interface Dialect {
       /**
        * 跳过 count 和 分页查询
        * 
        * @param msId 执行的  MyBatis 方法全名
        * @param parameterObject 方法参数
        * @param rowBounds 分页参数
        * @return true 跳过,返回默认查询结果,false 执行分页查询
        */
       boolean skip(String msId, Object parameterObject, RowBounds rowBounds);
       
       /**
        * 执行分页前,返回 true 会进行 count 查询,false 会继续下面的 beforePage 判断
        * 
        * @param msId 执行的  MyBatis 方法全名
        * @param parameterObject 方法参数
        * @param rowBounds 分页参数
        * @return
        */
       boolean beforeCount(String msId, Object parameterObject, RowBounds rowBounds);
       
       /**
        * 生成 count 查询 sql
        * 
        * @param boundSql 绑定 SQL 对象
        * @param parameterObject 方法参数
        * @param rowBounds 分页参数
        * @param countKey count 缓存 key
        * @return
        */
       String getCountSql(BoundSql boundSql, Object parameterObject, RowBounds rowBounds, CacheKey countKey);
       
       /**
        * 执行完 count 查询后
        * 
        * @param count 查询结果总数
        * @param parameterObject 接口参数
        * @param rowBounds 分页参数
        */
       void afterCount(long count, Object parameterObject, RowBounds rowBounds);
       
       /**
        * 执行分页前,返回 true 会进行分页查询,false 会返回默认查询结果
        * 
        * @param msId 执行的 MyBatis 方法全名
        * @param parameterObject 方法参数
        * @param rowBounds 分页参数
        * @return
        */
       boolean beforePage(String msId, Object parameterObject, RowBounds rowBounds);
       
       /**
        * 生成分页查询 sql
        * 
        * @param boundSql 绑定 SQL 对象
        * @param parameterObject 方法参数
        * @param rowBounds 分页参数
        * @param pageKey 分页缓存 key
        * @return
        */
       String getPageSql(BoundSql boundSql, Object parameterObject, RowBounds rowBounds, CacheKey pageKey);
       
       /**
        * 分页查询后,处理分页结果,拦截器中直接 return 该方法的返回值
        * 
        * @param pageList 分页查询结果
        * @param parameterObject 方法参数
        * @param rowBounds 分页参数
        * @return
        */
       Object afterPage(List pageList, Object parameterObject, RowBounds rowBounds);
       
       /**
        * 设置参数
        * 
        * @param properties 插件属性
        */
       void setProperties(Properties properties);
    }
    

    3.MysqlDialect和PageRowBounds

    MysqlDialect实现了在mysql数据库中的总条数查询sql语句和分页语句

    PageRowBounds继承了rowBounds除了offset、limit还添加了一个参数total,如果用户设置了PageRowBounds那么会多进行一次sql查询,查询到的总条数保存到total变量中

    public class MySqlDialect implements Dialect {
    
       @Override
       public boolean skip(String msId, Object parameterObject, RowBounds rowBounds) {
          //这里使用 RowBounds 分页,默认没有 RowBounds 参数时,会使用 RowBounds.DEFAULT 作为默认值
          return rowBounds == RowBounds.DEFAULT;
       }
       //返回false执行 beforePage 返回true执行 countSql
       @Override
       public boolean beforeCount(String msId, Object parameterObject, RowBounds rowBounds) {
          //只有使用 PageRowBounds 才能记录总数,否则查询了总数也没用
          return rowBounds instanceof PageRowBounds;
       }
       
       @Override
       public String getCountSql(BoundSql boundSql, Object parameterObject, RowBounds rowBounds, CacheKey countKey) {
          //简单嵌套实现 MySql count 查询,count(*)是 boundSql.getSql() 返回的条数
          log.info(boundSql.getSql());
          //这里是使用派生表查询到数据库的总条数,执行了两次sql查询猜得到count(*),实际上可以优化
          return "select count(*) from (" + boundSql.getSql() + ") temp";
       }
       
        @Override
        public void afterCount(long count, Object parameterObject, RowBounds rowBounds) {
           //记录总数,按照 beforeCount 逻辑,只有 PageRowBounds 时才会查询 count,所以这里直接强制转换
           ((PageRowBounds)rowBounds).setTotal(count);
        }
    
        @Override
       public boolean beforePage(String msId, Object parameterObject, RowBounds rowBounds) {
          //执行分页前,返回 true 会进行分页查询,false 会返回默认查询结果
          return rowBounds != RowBounds.DEFAULT;
       }
       
       @Override
       public String getPageSql(BoundSql boundSql, Object parameterObject, RowBounds rowBounds, CacheKey pageKey) {
          //pageKey 会影响缓存,通过固定的 RowBounds 可以保证二级缓存有效
          pageKey.update("RowBounds");
          return boundSql.getSql() + " limit " + rowBounds.getOffset() + "," + rowBounds.getLimit();
       }
    
       @Override
       public Object afterPage(List pageList, Object parameterObject, RowBounds rowBounds) {
          return pageList;
       }
       
       @Override
       public void setProperties(Properties properties) {
          
       }
    }
    
    public class PageRowBounds extends RowBounds{
       private long total;
    
       public PageRowBounds() {
          super();
       }
    
       public PageRowBounds(int offset, int limit) {
          super(offset, limit);
       }
    
       public long getTotal() {
          return total;
       }
    
       public void setTotal(long total) {
          this.total = total;
       }
    }
    

    5.总结

    如果要根据自己的需求开发插件,必须要学好Mybatis中的源码,任重而道远!!!

  • 相关阅读:
    [不断更新中]模板
    Noip 2018 游记
    [luogu3067 USACO12OPEN] 平衡的奶牛群
    [luogu4127 AHOI2009] 同类分布 (数位dp)
    [luogu4571 JSOI2009] 瓶子和燃料 (数论)
    [luogu4056 JSOI2009] 火星藏宝图 (贪心 dp)
    [luogu3573 POI2014] RAJ-Rally (拓扑排序 权值线段树)
    常见的狄利克雷卷积(一篇很好的博客上看到的)
    cz_xuyixuan
    [bzoj1951] [Sdoi2010]古代猪文 费马小定理+Lucas定理+CRT
  • 原文地址:https://www.cnblogs.com/iandf/p/13905711.html
Copyright © 2011-2022 走看看