文章目录
1. mybatis注解
上一章介绍mybatis的使用时,主要介绍的是通过XML方式进行SQL相关配置,其实我们还可以通过注解来减少编写Mapper映射文件,本章就主要讲解相关的注解。
1.1 常用的CRUD注解
- @Insert:实现新增
- @Update:实现更新
- @Delete:实现删除
- @Select:实现查询
通过上面的注解,可以替代mapper.xml里面的相关SQL标签,例如通过
@Select("select * from user")
public List<User> findAll();
可以替代
<select id="findAll" resultMap="userMap">
select * from user
</select>
1.2 结果集注解
在注解模式中,怎么指定返回的结果集中数据库字段与实体属性名的对应关系呢?
主要是通过@Result和@Results
来实现,具体示例如下:
@Select("select * from user")
@Results({
@Result(property = "id",column = "id"),
@Result(property = "name",column = "name")
})
public List<User> findAll();
其中的属性的解释:
- property:对应xml中resultMap的property属性,代表类属性名;
- column:对象xml中resultMap的column属性,代表表字段名;
- javaType:对应的java数据类型
1.3 复杂映射注解
复杂映射主要是分为一对一、一对多、多对多,通过上一章我们知道多对多就是两个一对多,所以此处只演示一对一和一对多两种情况。
1.3.1 一对一
一对一的注解是@One
,它代替了<association/>
标签,来指定子查询返回的对象信息。其中的属性讲解:
- select:指定关联的查询方法的全限定方法名
同时,column
属性对应的pid
值就是要传入子查询的参数。
通过上一章的用户和省信息的一对一例子来演示,通过注解实现时,等于是将查询拆分成两个,首先是查询用户、其次是查询省份信息。
所以我们需要在ProvinceMapp.java
里定义查询findById()
,提供给用户查询时调用,具体如下:
@Results({
@Result(property = "id",column = "id"),
@Result(property = "name",column = "name"),
@Result(property = "province",column = "pid",javaType = Province.class,
one=@One(select = "com.jfl.test.mapper.ProvinceMapper.findById"))
})
@Select("select * from user")
public List<User> findUserAndProvince();
同时,在ProvinceMapper.java
中提供如下方法:
@Select({"select * from province where pid = #{pid}"})
public Province findById(Integer pid);
当进行用户查询时,同时将用户信息中的pid
传入指定的省信息查询方法,最终返回用户和省信息的综合信息,完成一对一的查询。
1.3.2 一对多
一对多的注解是@Many
,它代替了<collection/>
标签,来指定子查询返回的对象信息。其中的属性讲解:
- select:指定关联的查询方法的全限定方法名
具体使用如下:
@Select("select * from province")
@Results({
@Result(property = "pid",column = "pid"),
@Result(property = "pname",column = "pname"),
@Result(property = "userList",column = "pid",javaType = List.class,
many=@Many(select = "com.jfl.test.mapper.UserMapper.findUserByPid"))
})
public List<Province> findAll();
同时,用户那变需要提供对应的查询方法:
@Select("select * from user where pid = #{pid}")
public List<Order> findUserByPid(Integer pid);
最终就会返回一对多的结果。
在使用上,一对多和一对一基本一样,只有注解以及返回结果类型的差异。
2. 延迟加载
延迟加载就是在需要⽤到数据时才进⾏加载,不需要⽤到数据时就不加载数据。
延迟加载也称懒加载。
延迟加载是基于嵌套查询来实现的。
Mybatis是支持延迟加载的,只不过默认是关闭状态。
在开发过程中,很多时候我们并不需要在加载省份信息时就⼀定要加载关联的用户信息。此时就是我们所说的延迟加载。
延迟加载分为局部延迟加载和全局延迟加载,主要是根据配置的位置不同,影响的范围不同。
延迟加载是基于嵌套查询来实现的,因为它是将关联的查询进行延迟查询来实现延迟加载,如果使用的是left join
或者其他非嵌套的查询,是没法进行延迟加载。
2.1 局部延迟加载
在<association/>
和<collection/>
标签中都有⼀个fetchType
属性,通过修改它的值lazy
还是eager
,可以修改局部的加载策略。
<resultMap id="userMap" type="com.jfl.test.User">
<result property="id" column="id"></result>
<result property="name" column="name"></result>
<!--
fetchType="lazy" : 懒加载策略
fetchType="eager" : ⽴即加载策略
-->
<association property="province" ofType="province" column="pid" select="com.jfl.test.mapper.ProvinceMapper.findById" fetchType="lazy">
<result column="pid" property="pid"></result>
<result column="pname" property="pname"></result>
</association>
</resultMap>
这种情况下,只有这一个查询会进行延迟加载。
2.2 全局延迟加载
想要全局的嵌套查询都进行延迟加载的话,在Mybatis的核⼼配置⽂件中可以使⽤setting
标签修改全局的加载策略。
<settings>
<!--开启全局延迟加载功能-->
<setting name="lazyLoadingEnabled" value="true"/>
</settings>
注意:加载策略的优先级是局部高于全局,也就类似于就近原则,如果sql上配置了立即加载,就算配了全局延迟的策略,但是在这个sql执行时也不会延迟加载。
2.3 延迟加载触发
当配置了延迟加载策略后,会发现即使没有调⽤关联对象的任何⽅法,但是在调⽤当前对象的equals、clone、hashCode、toString⽅法时也会触发关联对象的查询。
查看源码,可以看到在org.apache.ibatis.session.Configuration
里默认触发中有这几个方法:
public class Configuration {
.......
/**
* 指定对象的哪个方法会触发延迟加载。
*/
protected Set<String> lazyLoadTriggerMethods = new HashSet<>(Arrays.asList("equals", "clone", "hashCode", "toString"));
.......
}
解决这个问题,可以通过配置覆盖掉上诉的方法,具体配置如下:
<settings>
<!--开启全局延迟加载功能-->
<setting name="lazyLoadingEnabled" value="true"/>
<!--所有⽅法都会延迟加载-->
<setting name="lazyLoadTriggerMethods" value=""/>
</settings>
2.4 延迟加载原理
其实底层是通过动态代理(默认使⽤Javassist代理⼯⼚)来实现的延迟加载
当查询用户时,发现如果有懒加载的配置,如:fetchType="lazy"
,则将User生成一个代理对象进行返回,并把懒加载相关对象放到ResultLoaderMap
中存起来,当调用到懒加载相关方法时,根据代理类的invoke
进行具体的SQL查询,最终得到结果。
通过org.apache.ibatis.session.Configuration
里的setProxyFactory
方法可以看到默认情况下使用的是JavassistProxyFactory
,部分源码如下:
/**
* 默认使⽤Javassist代理⼯⼚
* @param proxyFactory
*/
public void setProxyFactory(ProxyFactory proxyFactory) {
if (proxyFactory == null) {
proxyFactory = new JavassistProxyFactory();
}
this.proxyFactory = proxyFactory;
}
通过追踪代码,可以看到Mybatis的查询结果是由ResultSetHandler
接⼝的handleResultSets()
⽅法处理的,所以可以进入它唯一的实现类DefaultResultSetHandler
来查看具体逻辑,主要方法是createResultObject
,源码如下:
// 创建映射后的结果对象
private Object createResultObject(ResultSetWrapper rsw, ResultMap resultMap, ResultLoaderMap lazyLoader, String columnPrefix) throws SQLException {
// useConstructorMappings ,表示是否使用构造方法创建该结果对象。此处将其重置
this.useConstructorMappings = false; // reset previous mapping result
final List<Class<?>> constructorArgTypes = new ArrayList<>(); // 记录使用的构造方法的参数类型的数组
final List<Object> constructorArgs = new ArrayList<>(); // 记录使用的构造方法的参数值的数组
// 创建映射后的结果对象
Object resultObject = createResultObject(rsw, resultMap, constructorArgTypes, constructorArgs, columnPrefix);
if (resultObject != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) {
// 如果有内嵌的查询,并且开启延迟加载,则创建结果对象的代理对象
final List<ResultMapping> propertyMappings = resultMap.getPropertyResultMappings();
for (ResultMapping propertyMapping : propertyMappings) {
// issue gcode #109 && issue #149
if (propertyMapping.getNestedQueryId() != null && propertyMapping.isLazy()) {
resultObject = configuration.getProxyFactory().createProxy(resultObject, lazyLoader, configuration, objectFactory, constructorArgTypes, constructorArgs);
break;
}
}
}
// 判断是否使用构造方法创建该结果对象
this.useConstructorMappings = resultObject != null && !constructorArgTypes.isEmpty(); // set current mapping result
return resultObject;
}
通过源码会看到在发现有延迟加载的属性时,会去调用configuration.getProxyFactory().createProxy()
产生一个代理对象并返回,configuration.getProxyFactory()
返回的就是上面说的默认代理对象工厂JavassistProxyFactory
,然后调用工厂的createProxy()
方法,查看源码如下:
@Override
public Object createProxy(Object target, ResultLoaderMap lazyLoader, Configuration configuration, ObjectFactory objectFactory, List<Class<?>> constructorArgTypes, List<Object> constructorArgs) {
return EnhancedResultObjectProxyImpl.createProxy(target, lazyLoader, configuration, objectFactory, constructorArgTypes, constructorArgs);
}
看到最终返回的是EnhancedResultObjectProxyImpl
的相关返回的结果,看到这个名字可以得到使用的应该就是cglib
的动态代理。进入该类中,可以看到具体的cglib
的创建逻辑以及调用时会执行的invoke
方法,源码如下:
static Object crateProxy(Class<?> type, MethodHandler callback, List<Class<?>> constructorArgTypes, List<Object> constructorArgs) {
// 创建 javassist ProxyFactory 对象
ProxyFactory enhancer = new ProxyFactory();
// 设置父类
enhancer.setSuperclass(type);
// 根据情况,设置接口为 WriteReplaceInterface 。和序列化相关,可以无视
try {
type.getDeclaredMethod(WRITE_REPLACE_METHOD); // 如果已经存在 writeReplace 方法,则不用设置接口为 WriteReplaceInterface
// ObjectOutputStream will call writeReplace of objects returned by writeReplace
if (log.isDebugEnabled()) {
log.debug(WRITE_REPLACE_METHOD + " method was found on bean " + type + ", make sure it returns this");
}
} catch (NoSuchMethodException e) {
enhancer.setInterfaces(new Class[]{WriteReplaceInterface.class}); // 如果不存在 writeReplace 方法,则设置接口为 WriteReplaceInterface
} catch (SecurityException e) {
// nothing to do here
}
// 创建代理对象
Object enhanced;
Class<?>[] typesArray = constructorArgTypes.toArray(new Class[constructorArgTypes.size()]);
Object[] valuesArray = constructorArgs.toArray(new Object[constructorArgs.size()]);
try {
enhanced = enhancer.create(typesArray, valuesArray);
} catch (Exception e) {
throw new ExecutorException("Error creating lazy proxy. Cause: " + e, e);
}
// 设置代理对象的执行器
((Proxy) enhanced).setHandler(callback);
return enhanced;
}
可以看到最终使用的是enhancer.create
,所以可以确定是cglib
动态代理来产生代理对象。
再查看下invoke
方法,看具体是怎么执行:
private static final String FINALIZE_METHOD = "finalize";
private static final String WRITE_REPLACE_METHOD = "writeReplace";
@Override
public Object invoke(Object enhanced, Method method, Method methodProxy, Object[] args) throws Throwable {
// 得到执行的方法名
final String methodName = method.getName();
try {
synchronized (lazyLoader) {
// 不匹配,直接进入else
if (WRITE_REPLACE_METHOD.equals(methodName)) {
Object original;
// 判断构造函数是否无参
if (constructorArgTypes.isEmpty()) {
original = objectFactory.create(type);
} else {
original = objectFactory.create(type, constructorArgTypes, constructorArgs);
}
PropertyCopier.copyBeanProperties(type, enhanced, original);
if (lazyLoader.size() > 0) {
return new JavassistSerialStateHolder(original, lazyLoader.getProperties(), objectFactory, constructorArgTypes, constructorArgs);
} else {
return original;
}
} else {
// 是否有延迟加载
if (lazyLoader.size() > 0 && !FINALIZE_METHOD.equals(methodName)) {
// 加载所有延迟加载的属性
if (aggressive || lazyLoadTriggerMethods.contains(methodName)) {
lazyLoader.loadAll();
// 如果调用了 setting 方法,则不在使用延迟加载
} else if (PropertyNamer.isSetter(methodName)) {
final String property = PropertyNamer.methodToProperty(methodName);
lazyLoader.remove(property); // 移除
// 如果调用了 getting 方法,则执行延迟加载
} else if (PropertyNamer.isGetter(methodName)) {
final String property = PropertyNamer.methodToProperty(methodName);
if (lazyLoader.hasLoader(property)) {
// 延迟加载单个属性
lazyLoader.load(property);
}
}
}
}
}
// 继续执行原方法
return methodProxy.invoke(enhanced, args);
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
通过以上源码分析,可以印证最开始所讲的,通过动态代理来实现的延迟加载。
3. 缓存机制
mybatis的缓存分为一级缓存和二级缓存;
一级缓存默认开启,二级缓存默认关闭
二级缓存会引起脏读,所以不建议使用,常用redis做数据缓存
3.1 一级缓存
一级缓存的有效范围是同一个sqlSession;同一个sqlSession中执行相同的查询,第二次查询不会访问数据库,直接从一级缓存获取,返回给调用者。
但是如果两次查询中间有增删改操作,则会刷新一级缓存,这时第二次查询依然会去查询数据库。
通过分析源码,可以知道一级缓存的数据结构就是一个HashMap
:
,而这个map的key就是mapperstatement(保存了要执行的SQL的信息)、参数、分页信息、最终执行的SQL来组成的。
//为本次查询创建缓存的Key
CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
所以每一个SqISession都会存放一个map对象的引用,那什么时候会创建一级缓存的Map呢,通过源码分析得知是通过BaseExecutor
来创建:
protected BaseExecutor(Configuration configuration, Transaction transaction) {
this.transaction = transaction;
this.deferredLoads = new ConcurrentLinkedQueue<>();
// 创建本地缓存
this.localCache = new PerpetualCache("LocalCache");
this.localOutputParameterCache = new PerpetualCache("LocalOutputParameterCache");
this.closed = false;
this.configuration = configuration;
this.wrapper = this; // 自己
}
当执行查询的时候,就会先去缓存中取,如果取不到再走查询,源码如下:
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
// 已经关闭,则抛出 ExecutorException 异常
if (closed) {
throw new ExecutorException("Executor was closed.");
}
// 清空本地缓存,如果 queryStack 为零,并且要求清空本地缓存。
if (queryStack == 0 && ms.isFlushCacheRequired()) {
clearLocalCache();
}
List<E> list;
try {
// queryStack + 1
queryStack++;
// 从一级缓存中,获取查询结果
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
// 获取到,则进行处理
if (list != null) {
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
// 获得不到,则从数据库中查询
} else {
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
} finally {
// queryStack - 1
queryStack--;
}
if (queryStack == 0) {
// 执行延迟加载
for (DeferredLoad deferredLoad : deferredLoads) {
deferredLoad.load();
}
// issue #601
// 清空 deferredLoads
deferredLoads.clear();
// 如果缓存级别是 LocalCacheScope.STATEMENT ,则进行清理
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// issue #482
clearLocalCache();
}
}
return list;
}
缓存中如果查不到的话,就从数据库查,在queryFromDatabase
方法中,会将查询结果写入localcache,内部调用的是Map的put方法,最终交给Map进行存放。
3.2 二级缓存
二级缓存的原理和数据结构,和一级缓存是一样的,不过二级缓存的作用范围是基于mapper文件的namespace的,也就是说多个sqlSession可以共享一个mapper中的二级缓存区域,并且如果多个mapper的namespace相同,即使是两个mapper,那么这两个mapper中执行sql查询到的数据也将存在相同的二级缓存区域中。
由于二级缓存默认是关闭,如果打开需要进行配置:
<settings>
<setting name="cacheEnabled" value="true"/>
</settings>
其次在Mapper.xml文件中开启缓存:
<cache></cache>
空标签说明使用mybatis自身的二级缓存,也可以进行指定:
<cache type="org.mybatis.caches.redis.RedisCache" />
这个就是指定使用redis作为二级缓存。
使用二级缓存需要pojo实现Serializable接口,为了将缓存数据取出执行反序列化操作,因为二级缓存数据存储介质多种多样,不一定只存在内存中,有可能存在硬盘中,如果我们要再取这个缓存的话,就需要反序列化了。
上面描述的是全局的配置,如果单独开启,则在sql标签上使用userCache
和flushCache
等配置项,useCache为true则使用二级缓存,否则不使用。
<select id="selectUserByUserId" useCache="false"
resultType="com.jfl.test.pojo.User" parameterType="int">
select * from user where id=#{id}
</select>
设置flushCache="true”
则会在增删改之后刷新缓存,默认是true,如果不刷新,则会出现脏读等问题。
3.3 redis做为二级缓存
使用mybatis自带的二级缓存存在以下问题:
- 自带的二级缓存是单服务器工作,无法实现分布式缓存;
- 当进行嵌套查询时,A表关联的B表数据,通过A查询时,最终结果是存在A的二级缓存中,当B发生改变,A的二级缓存是不会进行刷新的,会导致脏读等问题;
所以尽可能的不使用自带的二级缓存,而redis就是一个很好的分布式缓存,所以一般都是使用redis来做缓存。
主要的原理是redis提供的缓存,实现了mybatis的Cache接口,在配置时可以指定具体实现类,来使用redis的缓存。
mybatis-redis在存储数据的时候,是使用的hash结构,把cache的id作为这个hash的key (cache的id在mybatis中就是mapper的namespace);这个mapper中的查询缓存数据作为 hash的field,需要缓存的内容直接使用SerializeUtil存储,SerializeUtil和其他的序列化类差不多,负责对象的序列化和反序列化;