一、MyBatis实现分页
1、关于逻辑分类和物理分页
分页可分为逻辑分页和物理分页:
逻辑分页:是一次性把全部数据查询加载进内存 ,然后再进行分页。这样优点是减少IO次数,适合频繁访问、数据量少的情况。缺点是不适合大数据量,容易造成内存溢出。
物理分页:是利用limit语法在数据库中进行分页。他的优点是适合分页大数据量数据。缺点是频繁查询数据库,消耗性能。
2、Mybatis实现分页功能
mybatis实现分页有三种方式:
- 直接使用SQL语句,利用limit关键字分页(物理分页)
- RowBounds(逻辑分页)
- 第三方插件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,应该是一个名空间的字 符串值(也就是类的完全限定名) 。 |
@ConstructorArgs | Method | <constructor> | 收集一组结果以传递给一个结果对象的构造方法 |
@Arg | 方法 |
| 单 独 的 构 造 方 法 参 数 , 是 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 | 方法 |
| 在列和属性或字段之间的单独结果映 射。属 性: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 |
| 方法 |
| 这些注解中的每一个代表了执行的真 实 SQL。 它们每一个都使用字符串数组 (或单独的字符串)。如果传递的是字 符串数组, 它们由每个分隔它们的单独 空间串联起来。这就当用 Java 代码构 建 SQL 时避免了“丢失空间”的问题。 然而,如果你喜欢,也欢迎你串联单独 的字符串。属性:value,这是字符串 数组用来组成单独的 SQL 语句。 |
| 方法 |
| 这些可选的 SQL 注解允许你指定一个 类名和一个方法在执行时来返回运行 允许创建动态 的 SQL。 基于执行的映射语句, MyBatis 会实例化这个类,然后执行由 provider 指定的方法. 该方法可以有选择地接受参数对象.(In MyBatis 3.4 or later, it’s allow multiple parameters) 属性: type,method。type 属性是类。method 属性是方法名。 注意: 这节之后是对 类的 讨论,它可以帮助你以干净,容于阅读 的方式来构建动态 SQL。 |
@Param | Parameter | N/A | 如果你的映射器的方法需要多个参数, 这个注解可以被应用于映射器的方法 参数来给每个参数一个名字。否则,多 参数将会以它们的顺序位置来被命名 (不包括任何 RowBounds 参数) 比如。 #{param1} , #{param2} 等 , 这 是 默 认 的 。 使 用 @Param(“person”),参数应该被命名为 #{person}。 |
@SelectKey | Method | <selectKey> | 返回非自增主键 如果我们数据库中的主键不是自增方式产生的,但是当我们插入新数据后,需要返回该条数据的主键,那么我们可以使用这个注解。 |
@ResultMap | Method | N/A | 作用:用于指定查询结果集的映射关系。 |
@ResultType | Method | N/A | 作用:用于指定查询结果集的类型。 @ResultType是MyBatis中的注解之一,用于指定查询结果的类型。该注解可以用于接口方法或XML文件中,通常与@Select、@Insert、@Update、@Delete等注解一起使用。使用@ResultType注解时,需要指定查询结果的类型 |
@Flush | Method | N/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 = 5000 | ScheduledCache | 缓存超过指定时间则清空,单位ms,不设置该属性则不使用ScheduledCache不自动回收缓存 |
readWrite 属性 | 支持序列化的缓存 | 作用 |
---|---|---|
readWrite = false | SerializedCache | 设置为true是规定缓存只读,设置为false时使用SerializedCache,相同的查询从缓存中得到结果对象的副本 |
blocking 属性 | 阻塞缓存 | 作用 |
---|---|---|
blocking = true | BlockingCache | 设置为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