Mybatis - 应用篇 (二、进阶应用)

一、MyBatis实现分页

        1、关于逻辑分类和物理分页

        分页可分为逻辑分页物理分页

        逻辑分页:是一次性把全部数据查询加载进内存 ,然后再进行分页。这样优点是减少IO次数,适合频繁访问、数据量少的情况。缺点是不适合大数据量,容易造成内存溢出。

        物理分页:是利用limit语法在数据库中进行分页。他的优点是适合分页大数据量数据。缺点是频繁查询数据库,消耗性能。

        

        2、Mybatis实现分页功能

        mybatis实现分页有三种方式:

  1. 直接使用SQL语句,利用limit关键字分页(物理分页)
  2. RowBounds(逻辑分页)
  3. 第三方插件PageHelper(物理分页)

        2.1  SQL语句实现分页

limit语法 limit [offset,] rows

offset 偏移量,从第条数据开始,默认为0

rows 行数,要拿多少条数据

         mapper接口中:意思就是 从第几行开始拿多少条数据。

    @Select("select count(*) from emp   ")
    int countEmp();

    @Select("select * from emp  limit #{offset},#{rows} ")
    List<Emp> pageEmp(@Param("offset") int offset,@Param("rows") int rows);

         如果我们要实现分页逻辑,一般前端会传 pageNum(第几页) , pageSize(多少条数据)。我们需要自己计算分页逻辑。
        比如我们接受到的 pageNum = 3 ,pageSzie = 5:

        int pageNum = 3;  //查第3页
        int pageSize = 5;  //每页显示5条数据

        int count = empMapper.countEmp();  //总条数
        //计算共有多少页 +1是因为如果相除有余数 就等于多了一页,这里粗略计算为多一页
        int pagesNum = (count / pageSize) + 1;
        // 这页的偏移量offset是多少 , 也就是从第几条开始
        int offset = (pageNum - 1) * pageSize;  //偏移量

        List<Emp> roles = empMapper.pageEmp(offset,pagesNum);
        System.out.println(roles);
        //最后把分页数据封装到Page类中返回,这里不做演示

        这种方式缺点是比较麻烦,要自己计算分页数据和封装Page类。优点是自定义性强。

        2.2  RowBounds实现分页

        原理:通过RowBounds实现分页和通过数组方式分页原理差不多,都是一次获取所有符合条件的数据,然后在内存中对大数据进行操作,实现分页效果。只是数组分页需要我们自己去实现分页逻辑,这里更加简化而已。
        存在问题:一次性从数据库获取的数据可能会很多,对内存的消耗很大,可能导师性能变差,甚至引发内存溢出。
        适用场景:在数据量很大的情况下,建议还是适用拦截器实现分页效果。RowBounds建议在数据量相对较小的情况下使用。

 

       RowBounds分页是Mybatis提供的一种分页方式,其原理主要是在执行SQL查询后,将返回的所有结果集加载到内存中,然后在内存中根据指定的偏移量(offset)和限制数(limit)进行分页处理。
        具体来说,当我们在Mybatis的Mapper接口中调用查询方法时,可以传入一个RowBounds对象作为参数。这个RowBounds对象包含了分页所需的信息,比如当前页码、每页显示的记录数等。在执行查询时,Mybatis会首先执行完整的SQL查询语句,获取到所有满足条件的结果集。然后,Mybatis会根据RowBounds对象中指定的偏移量和限制数,在内存中对这些结果集进行截取,从而得到当前页需要展示的数据。
        需要注意的是,RowBounds分页方式是一种逻辑分页,即在内存中进行分页处理。当数据量非常大时,这种方式可能会导致内存溢出的问题。因此,对于大数据量的分页需求,建议使用物理分页方式,即在SQL查询语句中添加LIMIT和OFFSET子句,直接在数据库层面进行分页处理。
        

        

        在mapper接口中:

    @Select("select count(*) from emp")
    int countEmp();

    @Select("select * from emp")
    List<Emp> pageEmp(RowBounds rowBounds);

  

        int pageNum = 1;  //查第几页
        int pageSize = 3;  //每页几条数据

        //总条数
        int count = empMapper.countEmp();  
        //计算出:共有多少页、这页的偏移量offset是多少
        int pagesNum = (count / pageSize) + 1;  //共有多少页
        int offset = (pageNum - 1) * pageSize;  //偏移量

        RowBounds rowBounds = new RowBounds(offset,pageSize);
        List<Emp> emps = empMapper.pageEmp(rowBounds);
        System.out.println(emps);
        //最后把分页数据封装到Page类中返回,这里不做演示

        

        2.3 第三方插件PageHelper

        PageHelper的分页原理:主要基于MyBatis的插件机制。具体来说,PageHelper内部实现了一个PageInterceptor拦截器,这个拦截器会在MyBatis执行SQL查询之前进行拦截。
        当我们在代码中调用PageHelper的startPage方法时,它会在当前线程上下文中设置一个ThreadLocal变量,用于保存分页的参数,如当前页码、每页显示的数量等。
        随后,当MyBatis执行SQL查询时,PageInterceptor拦截器会拦截到这一操作。拦截器会从ThreadLocal中获取到分页参数,并根据这些参数来改写原始的SQL语句,添加LIMIT和OFFSET子句,以实现分页查询。
        改写后的SQL语句会被发送到数据库执行,数据库返回的结果集就是根据分页参数查询得到的结果。
        最后,PageInterceptor拦截器会将ThreadLocal中的分页参数清除,避免对后续操作产生影响。
        通过这种方式,PageHelper实现了对MyBatis查询结果的分页处理,而无需修改原有的SQL语句、Mapper接口和XML文件,因此具有无侵入性和易用性。同时,由于分页操作是在数据库层面进行的,因此也具有较高的性能。
        需要注意的是,PageHelper使用了ThreadLocal来保存分页参数,因此分页参数是与线程绑定的,这意味着不同的线程之间不会共享分页参数,从而保证了分页的准确性和独立性。
        总的来说,PageHelper通过拦截MyBatis的SQL查询操作,并在查询语句中添加LIMIT和OFFSET子句,实现了对查询结果的分页处理,从而简化了分页操作的实现过程,提高了开发效率。
PageHelper 是一个 MyBatis 的分页插件,可以简化分页操作。

         导入第三方依赖包:

        <dependency>
            <groupId>com.github.pagehelper</groupId>
            <artifactId>pagehelper</artifactId>
            <version>5.2.0</version>
        </dependency>

        代码如下:

        PageHelper.startPage(1, 3);
        PageInfo<Emp> pageInfo = new PageInfo<>(empMapper.allEmp());
        System.out.println(pageInfo.getNextPage());  //下一页
        System.out.println(pageInfo.getPrePage());  //上一页
        System.out.println(pageInfo.getPageNum());  //当前页
        System.out.println(pageInfo.getPageSize());  //每页多少条
        System.out.println(pageInfo.getSize());  //当前页的条数
        System.out.println(pageInfo.getTotal());  //总条数
        System.out.println(pageInfo.getPages());  //总页数
        System.out.println(pageInfo.getList());

        直接执行上面代码发现不行,还需要手动加上一个拦截器:

package com.test.demo.config;


import com.github.pagehelper.PageInterceptor;

import org.apache.ibatis.plugin.Interceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class PageHelperConfigure {

    @Bean
    public Interceptor[] plugins() {
        return new Interceptor[]{new PageInterceptor()};
    }
}

  如果分页失效可以参考下面的文章:
SpringBoot+MyBatis使用pagehelper分页插件及其注意事项(含解决分页不生效问题)_springboot pagehelper查询生效页面不显示-CSDN博客

        二、MapperScan和Mapper注解

        @MapperScan 扫描方式:

        @MapperScan 注解:会将指定目录下所有 DAO 类封装成 MyBatis 的 BaseMapper 类,然后注入 Spring 容器中,不需要额外的注解,就可以完成注入,常见的 DAO 定义如下

package xxx.xxx.xxx.dao;
public interface IXXXDao  extends BaseMapper<XXX>{}

        需要注意的是@MapperScan 扫描路径下除了 MyBatis 的 DAO 类,不应该有自定义的普通 DAO 接口及其实现类

        需要在工程启动类上使用扫描注解:

@MapperScan({"xx.xxmodule.dao"})

        项目启动时会报错:

        可见非继承自 BaseMapper 的普通接口及其实现类都被封装成 Mapper 类注入了,最终导致引用该 DAO 的 Service 类依赖注入失败。

        规避办法有两个:

1、自定义的普通接口类单独放置,区别 MyBatis 的 dao 和工程自定义 dao【推荐】。
2、为 Impl 实现类添加 @Primary 接口,弊端是无实际意义的接口定义也被当做 Mapper 封装了【不推荐】。

        @Mapper 注解方式:

        使用 @Mapper 注解时,debug 日志模式下,我们能看到 MyBatis 自动注入时会自动扫描启动类目录下所有的 @Mapper 注解,主要日志为:

2020-10-25 10:01:31,850 DEBUG (MybatisPlusAutoConfiguration.java:275)- 
Searching for mappers annotated with @Mapper
2020-10-25 10:33:08,200 DEBUG (MybatisPlusAutoConfiguration.java:275)- 
Using auto-configuration base package 'cn.xx.xx.xx'

        两种方式的比较:

        详细了解这俩的用法后,发现项目中用得很混乱,这是两种不同的注入 MyBatis DAO 实体的方法,选择其一即可,而项目中这两种注解都存在。

        实际上,使用 @MapperScan 后,对应包的 DAO 类就无需 @Mapper 了;如果使用 @Mapper ,就不需要 @MapperScan 。

        而工程中,基本上所有的 DAO 类都用了 @Mappper 注解,同时又为启动类设置了         @MapperScan,那么由于 @MapperScan 优先级高,@Maper 就失效了。

        究其根源可能是,代码都是拷贝过来的,没人深究不同配置有什么区别吧。而@MapperScan 的出现,就是为了解决每个类都要写 @Mapper 这个繁琐步骤的!

三、Mybatis中其他注解:

注解目标相对应的 XML描述
@CacheNamespace<cache>为给定的命名空间 (比如类) 配置缓存。 属性:implemetation,eviction, flushInterval,size 和 readWrite。
@CacheNamespaceRef<cacheRef>参照另外一个命名空间的缓存来使用。 属性:value,应该是一个名空间的字 符串值(也就是类的完全限定名) 。
@ConstructorArgsMethod<constructor>收集一组结果以传递给一个结果对象的构造方法
@Arg方法
  • <arg>
  • <idArg>
单 独 的 构 造 方 法 参 数 , 是 ConstructorArgs 集合的一部分。属性: id,column,javaType,typeHandler。 id 属性是布尔值, 来标识用于比较的属 性,和<idArg>XML 元素相似。
@TypeDiscriminator方法<discriminator>一组实例值被用来决定结果映射的表 现。 属性: column, javaType, jdbcType, typeHandler,cases。cases 属性就是实 例的数组。
@Case方法<case>单独实例的值和它对应的映射。属性: value,type,results。Results 属性是结 果数组,因此这个注解和实际的 ResultMap 很相似,由下面的 Results 注解指定。
@Results方法<resultMap>结果映射的列表, 包含了一个特别结果 列如何被映射到属性或字段的详情。 属 性:value, id。value 属性是 Result 注解的数组。 The id attribute is the name of the result mapping.
@Result方法
  • <result>
  • <id>
在列和属性或字段之间的单独结果映 射。属 性:id,column, property, javaType ,jdbcType ,type Handler, one,many。id 属性是一个布尔值,表 示了应该被用于比较(和在 XML 映射 中的<id>相似)的属性。one 属性是单 独 的 联 系, 和 <association> 相 似 , 而 many 属 性 是 对 集 合 而 言 的 , 和 <collection>相似。 它们这样命名是为了 避免名称冲突。
@One方法<association>复杂类型的单独属性值映射。属性: select,已映射语句(也就是映射器方 法)的完全限定名,它可以加载合适类 型的实例。注意:联合映射在注解 API 中是不支持的。这是因为 Java 注解的 限制,不允许循环引用。 fetchType, which supersedes the global configuration parameterlazyLoadingEnabled for this mapping.
@Many方法<collection>@Many是MyBatis中的注解之一,用于在一对多关联查询中指定查询结果的映射方式。该注解可以用于XML文件中,通常与和标签一起使用。使用@Many注解时,需要指定查询结果映射的Java对象类型和查询结果映射的属性
@MapKey方法复 杂 类 型 的 集合 属 性 映射 。 属 性 : select,是映射语句(也就是映射器方 法)的完全限定名,它可以加载合适类 型的一组实例。注意:联合映射在 Java 注解中是不支持的。这是因为 Java 注 解的限制,不允许循环引用。
@Options方法映射语句的属性mybatis的@Options注解能够设置缓存时间,能够为对象生成自增的key
  • @Insert
  • @Update
  • @Delete
  • @Select
方法
  • <insert>
  • <update>
  • <delete>
  • <select>
这些注解中的每一个代表了执行的真 实 SQL。 它们每一个都使用字符串数组 (或单独的字符串)。如果传递的是字 符串数组, 它们由每个分隔它们的单独 空间串联起来。这就当用 Java 代码构 建 SQL 时避免了“丢失空间”的问题。 然而,如果你喜欢,也欢迎你串联单独 的字符串。属性:value,这是字符串 数组用来组成单独的 SQL 语句。
  • @InsertProvider
  • @UpdateProvider
  • @DeleteProvider
  • @SelectProvider
方法
  • <insert>
  • <update>
  • <delete>
  • <select>
这些可选的 SQL 注解允许你指定一个 类名和一个方法在执行时来返回运行 允许创建动态 的 SQL。 基于执行的映射语句, MyBatis 会实例化这个类,然后执行由 provider 指定的方法. 该方法可以有选择地接受参数对象.(In MyBatis 3.4 or later, it’s allow multiple parameters) 属性: type,method。type 属性是类。method 属性是方法名。 注意: 这节之后是对 类的 讨论,它可以帮助你以干净,容于阅读 的方式来构建动态 SQL。
@ParamParameterN/A如果你的映射器的方法需要多个参数, 这个注解可以被应用于映射器的方法 参数来给每个参数一个名字。否则,多 参数将会以它们的顺序位置来被命名 (不包括任何 RowBounds 参数) 比如。 #{param1} , #{param2} 等 , 这 是 默 认 的 。 使 用 @Param(“person”),参数应该被命名为 #{person}。
@SelectKeyMethod<selectKey>返回非自增主键
如果我们数据库中的主键不是自增方式产生的,但是当我们插入新数据后,需要返回该条数据的主键,那么我们可以使用这个注解。
@ResultMapMethodN/A作用:用于指定查询结果集的映射关系。
@ResultTypeMethodN/A

作用:用于指定查询结果集的类型。

@ResultType是MyBatis中的注解之一,用于指定查询结果的类型。该注解可以用于接口方法或XML文件中,通常与@Select、@Insert、@Update、@Delete等注解一起使用。使用@ResultType注解时,需要指定查询结果的类型

@FlushMethodN/A

作用:用于在插入、更新或删除操作之后自动清空缓存。

@Flush是MyBatis中的注解之一,用于在Mapper接口中指定在执行方法前或方法后刷新缓存。该注解可以用于Mapper接口方法上,通常与@Select、@Insert、@Update、@Delete等注解一起使用。使用@Flush注解时,需要指定刷新缓存的时机

        上一篇说过的就不再说了,有些也不常用就大概说一下:
 

        3.1 @CacheNamespace

        这个注解涉及到mybatis中的二级缓存,所以先简单说说mybatis中的一级缓存和二级缓存。

        

        1. 一级缓存默认开启,属于SqlSession级别

        同一个SqlSession执行查询,结果存储在缓存中,下次查询直接从缓存中取,直到进行了插入、修改或删除等操作或手动清空缓存。

         2. 二级缓存,属于namespace级别的

         二级缓存是用来解决一级缓存不能跨会话共享的问题的,可以被多个SqlSession共享(只要是同一个Mapper中的同一个方法,都可以共享),生命周期和应用同步

         先简单了解一下一级缓存,二级缓存具体原理在原理篇写,主要是如何应用。

         SpringBoot集成MyBatis注解方式   :

         1、yml中开启二级缓存:

mybatis:
  configuration:
    cache-enabled: true

        2、Mapper层接口使用@CacheNamespace注解

@Mapper
@CacheNamespace(readWrite = false)   //全局注解
public interface EmpMapper {

    //查询所有数据
    @Select("select * from emp")
    List<Emp> findAll();
}

         基本上就是这样。这个简单语句的效果如下:

  • 映射语句文件中的所有 select 语句的结果将会被缓存。
  • 映射语句文件中的所有 insert、update 和 delete 语句会刷新缓存。
  • 缓存会使用最近最少使用算法(LRU, Least Recently Used)算法来清除不需要的缓存。
  • 缓存不会定时进行刷新(也就是说,没有刷新间隔)。
  • 缓存会保存列表或对象(无论查询方法返回哪种)的 1024 个引用。
  • 缓存会被视为读/写缓存,这意味着获取到的对象并不是共享的,可以安全地被调用者修改,而不干扰其他调用者或线程所做的潜在修改。

  @CacheNamespace注解源码:

package org.apache.ibatis.annotations;
 
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.apache.ibatis.cache.Cache;
import org.apache.ibatis.cache.decorators.LruCache;
import org.apache.ibatis.cache.impl.PerpetualCache;
 
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface CacheNamespace {
    // 缓存实现类,默认PerpetualCache, 可以自定义缓存实现类(需要实现Cache接口)
    Class<? extends Cache> implementation() default PerpetualCache.class;
 
    // 缓存回收策略
    Class<? extends Cache> eviction() default LruCache.class;  
 
    // 定时自动清空缓存间隔, 自动刷新时间,单位ms,未配置时默认不自动刷新
    long flushInterval() default 0L;
 
    // 最多缓存对象个数	默认1024
    int size() default 1024;
 
    // 是否只读	
    // true:只读缓存:会给所有调用者返回缓存对象的相同实例。因此这些对象不能被修改。这提供了很重要的性能优势。
    // false:读写缓存:会返回缓存对象的拷贝(通过序列化),不会共享。这会慢一些,但是安全,因此默认是false。对象必须支持序列化
    boolean readWrite() default true;
 
    // 是否使用可重入锁实现缓存的并发控制	设置为true会使用BlockingCache对Cache进行装饰。默认是false
    boolean blocking() default false;
 
    Property[] properties() default {};
}
flushInterval 属性缓存定时回收类作用
flushInterval = 5000ScheduledCache缓存超过指定时间则清空,单位ms,不设置该属性则不使用ScheduledCache不自动回收缓存
readWrite 属性支持序列化的缓存作用
readWrite = falseSerializedCache设置为true是规定缓存只读,设置为false时使用SerializedCache,相同的查询从缓存中得到结果对象的副本
blocking 属性阻塞缓存作用
blocking = trueBlockingCache设置为true时,基于java可重入锁,在缓存中get/set方法加锁,操作只有一个线程读写缓存
其他基本缓存实现类描述作用
LoggingCache缓存日志基本默认使用的缓存,输出对缓存的操作和缓存命中率等信息
SynchronizedCache同步缓存基于synchronized关键字实现,解决并发问题
TransactionCache事务缓存以事务的形式一次存入或移除多个缓存

 简单使用就是在mapper接口上面直接加上 CacheNamespace 注解就行,然后所有的属性都是默认值。

        如果想要第三方类库实现二级缓存,例如使用redis,简单使用步骤如下:

        1、引入相关依赖:

        <!-- 整合redis依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
            <version>2.6.14</version>
        </dependency>
        
        <!--logback相关依赖-->
        <dependency>
            <groupId>net.logstash.logback</groupId>
            <artifactId>logstash-logback-encoder</artifactId>
            <version>7.2</version>
        </dependency>
        
        <!-- hutool工具类相关依赖-->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.8.0</version>
        </dependency>

        2、增加yaml配置

  redis:
     host: 127.0.0.1
     port: 6379
     database: 13
     password: 123456

         3、编写 实现  Cache 接口的 MybatisRedisCache 实现类:

@Slf4j
public class MybatisRedisCache implements Cache {

    final static String NAME_SPACE ="mybatis-cache:";


    private static RedisTemplate<String, Object> redisTemplate;
    private static int cacheSec;
    private final String id;
    /**
     * The {@code ReadWriteLock}.
     */
    private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();

    public MybatisRedisCache(final String id) {
        if (id == null) {
            throw new IllegalArgumentException("Cache instances require an ID");
        }
        log.warn("MybatisRedisCache:id=" + id);
        this.id = id;
    }

    public static void setCacheSec(int cacheSec) {
        MybatisRedisCache.cacheSec = cacheSec;
    }

    public static void setRedisTemplate(RedisTemplate<String, Object> redisTemplate) {
        MybatisRedisCache.redisTemplate = redisTemplate;
    }

    @Override
    public ReadWriteLock getReadWriteLock() {
        return this.readWriteLock;
    }

    @Override
    public String getId() {
        return this.id;
    }

    @Override
    public void putObject(Object key, Object value) {
        try {
            log.debug(">>>>>>>>>>>>>>>>>>>>>>>>putObject: key=" + key + ",value=" + value);
            if (null != value) {
                if (cacheSec > 0) {
                    // 这里简单起见, 先固定好了缓存的时长
                    // 也可以尝试 结合<property name="cacheSec" value="600"/> 在不同的mapper中指定特殊的缓存时长
                    // 也可以根据实际业务情况,制定缓存策略
                    redisTemplate.opsForValue().set(NAME_SPACE + key.toString(), value, cacheSec, TimeUnit.SECONDS);
                } else {
                    redisTemplate.opsForValue().set(NAME_SPACE + key.toString(), value);
                }
            }

        } catch (Exception e) {
            e.printStackTrace();
            log.error("redis保存数据异常!");
        }
    }

    @Override
    public Object getObject(Object key) {
        try {
            log.debug(">>>>>>>>>>>>>>>>>>>>>>>>getObject: key=" + key);
            int size = this.getSize();

            if (null != key) {
                // 这里很坑, 如果选用的redis序列化反序列化的方式不合适,在返回结果后可能会报类转换异常
                return redisTemplate.opsForValue().get(NAME_SPACE + key.toString());
            }
        } catch (Exception e) {
            e.printStackTrace();
            log.error("redis获取数据异常!");
        }
        return null;
    }

    @Override
    public Object removeObject(Object key) {
        try {
            if (null != key)
                return redisTemplate.delete(NAME_SPACE + key.toString());
        } catch (Exception e) {
            e.printStackTrace();
            log.error("redis获取数据异常!");
        }
        return null;
    }

    @Override
    public void clear() {
        Set<String> keys = redisTemplate.keys(NAME_SPACE + "*");
        if (CollectionUtil.isNotEmpty(keys)) {
            redisTemplate.delete(keys);
        }
        log.debug(">>>>>>>>>>>>>>>>>>>>>>>>clear");
    }

    @Override
    public int getSize() {
        Set<String> keys = redisTemplate.keys(NAME_SPACE + "*");
        if (CollectionUtil.isNotEmpty(keys)) {
            return keys.size();
        }
        return 0;
    }
}

         4、添加redisTemplate 序列化配置类:

@Configuration
public class MybatisRedisCacheConfiguration {

    @Value("${mybatis.cache-sec:600}")
    private int cacheSec;


    @Autowired
    public void config(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        // 配置连接工厂
        template.setConnectionFactory(factory);

        // 序列化 这里也可根据实际情况,使用其他的序列化实现类,
        JdkSerializationRedisSerializer serializer = new JdkSerializationRedisSerializer();
        template.setValueSerializer(serializer);
        //使用StringRedisSerializer来序列化和反序列化redis的key值
        template.setKeySerializer(new StringRedisSerializer());

        // 设置hash key 和value序列化模式
        template.setHashKeySerializer(new StringRedisSerializer());
        template.setHashValueSerializer(serializer);
        template.afterPropertiesSet();

        MybatisRedisCache.setRedisTemplate(template);
        MybatisRedisCache.setCacheSec(cacheSec);
    }
}

     5、最后在需要开启二级缓存的 mapper接口上的 @CacheNamespace注解 中更改实现类即可:

@Mapper
@CacheNamespace(implementation = MybatisRedisCache.class)   
public interface EmpMapper {

    //查询所有数据
    @Select("select * from emp")
    List<Emp> findAll();
}

        再次调用发现数据已经存储在redis中。

        关于 @CacheNamespace 注解主要了解这么多,实际生产中一般不怎么直接用二级缓存,如果需要缓存一般会在特定方法上,如果需要,可以再另行深入研究。

        

        3.2 @CacheNamespaceRef

        @CacheNamespace和@CacheNamespaceRef这俩注解的区别:

        @CacheNamespace: 作用于mapper接口上面,是用来实现二级缓存的,点击查看源码可以发现,东西也不多,主要讲的是缓存的实现类,如果不指定实现类的话,默认就是PerpetualCache(实际上就是hashmap实现的),
        我们平常使用默认方式的的时候可以直接使用,如果使用自定义的方式,则需要指明实现类,例如上面的整合redis。
        这个存在的问题是xml里面走的SQL是可以被缓存的,但是你接口层的注解之类的SQL是不会被缓存,那有的人就说了,我把@CacheNamespace注解和cache标签一起使用不就行了嘛,但实际上是不行的。

        那么如何同时去满足这个问题呢:

        @CacheNamespaceRef :来解决这个问题,也就是在接口上使用这个注解,把接口上的@CacheNamespace注解替换成@CacheNamespaceRef ,同时的话xml文件里面使用cache标签,使用@CacheNamespaceRef注解要注意一点,要指明value或者name,如果不指明则会报错

        可以使用下面三种方式配置: 

        顺便说一下,如果某个SQL不想被缓存,可以单独处理一下:
1、SQL走的是xml文件查询:配置useCache=“false”
在这里插入图片描述
2、SQL走的是注解形式:@Options(useCache=false)
在这里插入图片描述
        如果你走的是xml,你在注解上使用这个注解,将不会起效 

        3.3 @ConstructorArgs 、@Arg

        简单来说@ConstructorArgs 是用来指定如何将查询结果映射到类的构造函数参数的注解。

        可能直接这么说,会有点不太了解,可以举个例子,我们之前使用 resultMap 标签来进行将sql查询出来的列名 跟类中的字段进行映射然后赋值。例如:

    <!--使用自定义结果集类型 -->
    <resultMap type="com.test.demo.model.EmpBO" id="empBOResult">
        <!-- property 是 com.test.demo.model.EmpBO 类中的属性 -->
        <!-- column是查询结果的列名,可以来自不同的表 -->
        <id property="empId" column="id" />
        <result property="empName" column="name" />
        <result property="empJob" column="job" />
        <result property="empSalary" column="salary" />
    </resultMap>

        实际上@ConstructorArgs 与 MyBatis 中的 <resultMap> 标签中的 <id><result> 标签都用于将 SQL 查询结果映射到 Java 对象中,但它们的用途和使用场景有所不同。

  • <resultMap> 、 <id><result> 配置通常用于有无参构造函数的类。它通过字段名(属性名)来直接映射查询结果到类的属性上。
  • @ConstructorArgs@Arg 注解是用于映射查询结果到构造函数参数的。特别适用于不可变类或者类中没有无参构造函数的情况。

        例如我们有下面这样的User类:

public class User {
    private final int id;
    private final String name;
    private final String email;

    public User(int id, String name, String email) {
        this.id = id;
        this.name = name;
        this.email = email;
    }
// Getters...
}

        这个User类我们强制只允许使用 有参构造创建对象,没有无参构造器,此时我们可以使用 @ConstructorArgs 注解来指定如何将查询结果映射到这个构造函数中:

@Results({
    @ConstructorArgs({
        @Arg(column = "id", javaType = int.class),
        @Arg(column = "name", javaType = String.class),
        @Arg(column = "email", javaType = String.class)
    })
})
@Select("SELECT id, name, email FROM users WHERE id = #{id}")
User getUserById(int id);
        3.4@TypeDiscriminator、@Case

        作用:用于指定类型鉴别器,用于根据查询结果集的不同类型映射到不同的Java对象。
        实际上这个注解用得比较少,网上能查到得用法也比较少,现实生产上其实用到得也不多,简单演示一下适用场景:        

        使用查询语句select * from user有如下需求:
        1、当用户年龄为18岁时查询结果显示生日信息。
        2、当用户年龄不为18岁时查询结果不显示生日信息。(显示为空或NULL即可)

        如果我们使用mybatis,第一个想到的解决办法可能是在Java程序里把用户信息查出来,然后再根据年龄做if判断。但是这样做有点繁琐。还好mybatis提供了一个标签(<discriminator/>)来解决如上的业务需求。discriminator也就是侦察器、也叫鉴别器
        也就是说discriminator可以控制我们返回值具体是什么。
  
mapper.xml配置内容如下:

<resultMap id="userMapForTestDiscriminator" type="user" autoMapping="false">

    <!--关闭自动映射,那么没有指定的列名不会出现在结果集中-->
    <id property="id" column="id"/>
    <result property="username" column="username"/>
    <result property="age" column="age"/>
    <discriminator javaType="int" column="age">
        <case value="18" resultType="user">
           <result property="birthday" column="birthday"/>
        </case>
    </discriminator>
</resultMap>

<select id="selectDiscriminator" resultMap="userMapForTestDiscriminator">
    select * from user limit 2
</select>

        select标签的id为selectDiscriminator,并且返回结果集使用resultMap来接收。重点就在resultMap里配置了discriminator。先来解释一下discriminator的作用:

        discriminator有个子标签是case,并且指定侦察器鉴别的属性为age。(它的用法很类似于Java中的switch)

        当查询的结果集中age列数据等于case指定的value值时(在这个例子里就是当结果集中age列为18时)。则把case标签中的result标签加入到外部的resultMap标签中。反之——如果结果集中的age列值不为18,则不做任何操作。

        换个更直观的说法来看。

  • 当结果集中的age列等于18时(本例中)实际上返回的resultMap标签相当于
    <resultMap id="userMapForTestDiscriminator" type="user" autoMapping="false">
    
        <!--关闭自动映射,那么没有指定的列名不会出现在结果集中-->
        <id property="id" column="id"/>
        <result property="username" column="username"/>
        <result property="age" column="age"/>
        <result property="birthday" column="birthday"/>
    
    </resultMap>
  • 当结果集中的age列不等于18时(本例中)实际上返回的resultMap标签相当于
    <resultMap id="userMapForTestDiscriminator" type="user" autoMapping="false">
    
        <!--关闭自动映射,那么没有指定的列名不会出现在结果集中-->
        <id property="id" column="id"/>
        <result property="username" column="username"/>
        <result property="age" column="age"/>
    
    </resultMap>

        和Java中的Switch用法一模一样,当满足条件时就执行case语句中的代码。

        上面是discriminator标签得用法,下面简单说一下@TypeDiscriminator注解得用法:

        有三个对象,Emp,Dmp,Dmp和Cmp继承于Emp,Cmp扩展salary属性,Dmp扩展 job 属性。

@Getter
@Setter
@Data
@Accessors(chain = true)
public class Emp implements Serializable {
    
    private Integer id;
    private String name;
    private String type;
    
}

@Data
public class Dmp  extends Emp{
    private String job;

}

@Data
public class Cmp  extends Emp{
    private Double salary;
}




@TypeDiscriminator(javaType = String.class, column = "type",
    cases = {
         @Case(value = "1", type = Cmp.class ),
         @Case(value = "2", type = Dmp.class)})
@Select("select * from emp where id = #{id}")
Emp selectById(@Param("id") Integer id);

        当type = 1 时返回Cmp,当type = 2时返回Dmp。

        因为用的比较少,如果用到别的用途可以自行搜索。

        3.5 @MapKey

        这是一个用在返回值为map的方法上的注解。它能够将存放对象的list转化为key值为对象的某一属性的map。属性有:value,填入的时对象的属性名,作为map的key值。

        举例:
        DAO层:

@MapKey("id")
Map getStudents();

      xml层:

<!--获取学生基本信息-->
<select id="getStudents" resultType="map">
    SELECT id, idCard, name FROM students
</select>

        使用前:

         使用后:

   

        注意事项:
        值得注意的是,通过@MapKey注解返回的Map,其key的类型和指定的字段类型是一致的。        
        例如:指定id作为Map的key,id为int类型,那么该Map的key也为integer类型,如果你通过String类型的key去获取value,则获取不到。

        3.6 @Options

        mybatis的@Options注解能够设置缓存时间,能够为对象生成自增的key。

        第一个使用场景:设置主键自增,并回填主键值。
        注意:@Options注解只能搭配Insert语句使用
        Options中的参数分别是:useGeneratedKeys = true 表明由Mybatis获取数据库自动生成的主键值;keyProperty = “id” 表明把获取到的主键值写入到实体类emp的id属性。

        第二个使用场景:设置缓存时间
        useCache = true 表示将会缓存本次查询结果,以提高下次查询速度;flushCache = Options.FlushCachePolicy.FALSE表示查询时不刷新缓存;timeout = 10000表示查询结果缓存10000秒。

        3.7 @SelectProvider、@UpdateProvider、@DeleteProvider、@SelectProvider

         这四个的用法一样,只是提供的sql类型不一样,其实主要就是可以自己写一个方法拼接sql语句。然后执行。

        例如:@SelectProvider:

public interface accountservice {

    @SelectProvider(method = "selectaccount", type = SqlContext.class)
    List<account> show(account account);

}

        type 是类文件名称,method是方法指定里面的SQL;

        SQL类的写法:

public class SqlContext {

    public String selectaccount(final account account){

     return new SQL(){
        { SELECT("*");
          FROM("account");
       if(account.getNum()!=null && account.getNum()!="0"){
            WHERE(" num=#{num } ");
        }
       if (account.getName()!=null && account.getName()!="") {
            WHERE(" name=#{name } ");
        }}
    }.toString();
     
    }
}

        

        3.8 @SelectKey

        @SelectKey 注解主要用于在插入数据后获取数据库生成的主键。这在很多数据库中是常见的做法,比如使用自增字段或序列生成主键。

        @SelectKey 注解通常与插入数据的 SQL 语句一起使用,它告诉 MyBatis 在执行插入操作后执行另一个 SQL 语句来获取主键。

        @SelectKey注解用在已经被 @Insert 或@InsertProvider或 @Update 或@UpdateProvider注解了的方法上。若在未被上述四个注解的方法上作 @SelectKey 注解则视为无效。

        

        注解的使用注意事项:
(1)自身无效的情况。需要前置注解才能生效:@Insert 或 @InsertProvider 或 @Update 或 @UpdateProvider,否则无效。
(2)他人无效的情况: 如果指定了 @SelectKey 注解,那么 MyBatis 就会忽略掉由 @Options 注解所设置的生成主键。

@Insert("INSERT INTO user (name, email) VALUES (#{name}, #{email})")  
@SelectKey(statement = "SELECT LAST_INSERT_ID()", keyProperty = "id", resultType = Long.class, before = false)  
Long insertUser(User user);

        在这个例子中,@Insert 注解用于定义插入数据的 SQL 语句。@SelectKey 注解则定义了另一个 SQL 语句 SELECT LAST_INSERT_ID(),用于在插入操作后获取生成的主键。keyProperty 指定了将获取到的主键值设置到哪个属性上(这里是 User 对象的 id 属性),resultType 指定了主键值的类型(这里是 Long),before 属性指定了 @SelectKey 注解的 SQL 语句是在插入操作之前执行还是之后执行(这里是 false,表示在之后执行)。

        3.9 @Flush

        用于在插入、更新或删除操作之后自动清空缓存。
        @Flush是MyBatis中的注解之一,用于在Mapper接口中指定在执行方法前或方法后刷新缓存。该注解可以用于Mapper接口方法上,通常与@Select、@Insert、@Update、@Delete等注解一起使用。使用@Flush注解时,需要指定刷新缓存的时机。
        用的情况很少,简单举个例子:

@Select("SELECT * FROM users WHERE id = #{id}")
@Flush(flushCache = FetchType.AFTER)
User getUserById(Long id);

四、mybatis中常用配置:

mybatis:
#  扫描mapper.xml文件路径
  mapper-locations: classpath:mapper/*.xml
  configuration:
    map-underscore-to-camel-case: true
#    打印sql日志
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值