- 一、Mybaits 基本框架设计
- 二、Mybatis源码结构
- 三、Mybatis 的主要构件及其相互关系
- 四、Mybatis核心模块梳理
- 第一步:
- 第二步
- 第三步
- 3.3 二级缓存
- 4.事物机制
- 五、Mybatis 源码模块
- 2.第二步:执行SQL语句(创建SqlSession并使用完成执行)
- SqlSession 的工作过程分析:
- 1. 开启一个数据库访问回话—-创建sqlSession 对象
- 2.为SqlSession传递一个配置的SQL语句的statement Id 和参数,然后返回结果
- 3. Mybatis执行器Executor根据SqlSession传递的参数执行query()方法。
- 4. StatementHandler对象负责设置Statement对象中的查询参数、处理JDBC返回的resultSet,将resultSet加工为List 集合返回:
- 5. StatementHandler 的parameterize(statement) 方法的实现:
- 6. StatementHandler 的List
query(Statement statement, ResultHandler resultHandler)方法的实现: - 7. ResultSetHandler 映射结果集
- SqlSession 的工作过程分析:
- 六、Mybatis常见面试题
- {} 和 ${} 的区别是什么
Mybatis: MyBatis 是一款优秀的持久层框架,它支持自定义 SQL、存储过程以及高级映射。MyBatis 免除了几乎所有的 JDBC 代码以及设置参数和获取结果集的工作。MyBatis 可以通过简单的 XML 或注解来配置和映射原始类型、接口和 Java POJO(Plain Old Java Objects,普通老式 Java 对象)为数据库中的记录。
一、Mybaits 基本框架设计
![image.png](https://cdn.nlark.com/yuque/0/2021/png/12747730/1636422341279-b34a1029-59f8-4892-935f-56f04a87d652.png#clientId=u5961e936-6898-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=402&id=u76e918f2&margin=%5Bobject%20Object%5D&name=image.png&originHeight=659&originWidth=831&originalType=binary&ratio=1&rotation=0&showTitle=false&size=60046&status=done&style=none&taskId=ufc27550f-d6b0-48a9-805f-5cdb08d6a9b&title=&width=506.5)
1.接口层 —- 和数据库交互的方式
Mybaits 和数据库交互的方式有两种方式:
- 使用传统的Mybaits提供的API
-
1.1 使用传统API
传统的API方式:就是传递 Statement Id 和查询参数给SqlSession 对象,使用SqlSession 对象完成和数据库的交互;Mybaits 提供非常方便和简单的API,供用户实现对数据库的CRUD
1.2 使用Mapper 接口
Mybaits将配置文件中的每一个
节点抽象为一个Mapper 接口,而这个接口中声明的方法和 节点中的 2.数据处理层
数据处理层可以说是Mybaits 的核心,从大的方面上讲,它主要完成的有两个功能
通过传入参数构建动态SQL语句
- SQL语句的执行以及封装查询结果集为List
2.1 参数映射和动态SQL语句生成
动态SQL语句生成可以说是Mybatis框架非常优雅的一个设计,Mybatis 通过传入的参数值,核心类BoudSql,使得Mybaits 有很强的灵活性和扩展性。
参数映射指的是java 数据类型和 jdbc 数据类型之间的转换,有个核心类TypeHadler主要负责类型转换,在Type包下每种类型都是一个XXXTypeHandler。类型之间转换主要包括两个过程:查询阶段:我们要将java类型的参数,转换成jdbc类型的数据,通过preparedStatement.setXXX() 来设值;另一个就是对resultset查询结果集的jdbcType 数据转换成java 数据类型。2.2 SQL语句的执行以及封装查询结果集成List
动态SQL语句完成后,Mybatis 将通过动态代理执行SQL语句,并将可能返回的结果集转换成List列表。 3.框架支撑层
3.1 事物管理机制
Mybatis 事物管理主要有两种实现:第一种交给Jdbc进行管理的事务,通过数据库事物的三个重要阶段完成事物管理;第二种 是Manange模式托管事物,交给容器来管理事务,它从来不提交和回滚一个链接,而他会让容器来管理事务的整个生命周期(Spring容器)。具体源码细节看第四部分模块的解析事物机制。3.2 数据源管理机制
Mybatis 作为一个ORM框架,数据源应该是最重要的。使用Mybatis 操作数据库时,每次都需要先和数据库建立连接,然后创建会话进行CRUD进行操作。每次操作完链接会关闭,如何这样频繁创建链接会特别耗性能,浪费资源,所以Mybatis提供了数据库链接池来管理数据源。
其实池管理技术不光在Mybatis中使用,好多地方都在使用池的技术,基本原理和思想一致。Mybatis 主要通过有链接池的数据(PooledDadaSource )和无池数据源来管理数据源(UnPooledDataSource)3.3 缓存机制
Mybatis 提高查询性能,避免频繁查询和高并发查询数据库设计了缓存机制,来提高性能,减少数据库问。Mybaits 设计缓存主要将缓存分为两级来管理,默认开启回话级别(session)的一级缓存,需要手动开启的应用级别(nameSpace)二级缓存。
在学习使用缓存过程中,需要了解每种缓存的使用场景和利弊,以及存在的问题。- 读过Mybatis 源码的应该知道:java.sql.Connection对象的创建一直延迟到执行SQL语句的时候。
- 整个框架支撑层模块真正开始起作用是在创建SqlSession对象成功执行SQL语句的时候,在创建sqlSession 对象的时候会根据是否开启缓存创建CacheingExecutor对象,sqlSession 执行SQL语句的时创建数据源和事物对象,存入到Configuration 对象中的 Environment 对象中,最后执行通过statement执行Sql 语句的使用configuration对象属性操作数据库。
/**
* 环境
* 决定加载哪种环境(开发环境/生产环境)
*/
public final class Environment {
//环境id
private final String id;
//事务工厂
private final TransactionFactory transactionFactory;
//数据源
private final DataSource dataSource;
}
二、Mybatis源码结构
1. 源码基本架构
Mybatis 源码主要分为18个包,根据功能模块进行了合理的分层设计,利用单一职责原则每个模块只负责处理特定的功能,大致包如下图所示:
2. 基本模块
annotations
annotation包下面全部都是Mybatis定义的注解,没有其他逻辑处理。
binding
binding模块主要负责mapper和xml映射绑定。
这个包下面四个核心类的主要功能和流程在下面有简单陈述:
一、每个类基本功能
MapperRegisty: 映射器注册机,扫描mapper接口存入一个map中,名为knownMapper; key = 接口全路经,value 为 代理空厂生产的一个代理对象 MapperProxyFactory: 代理工厂类,使用工厂模式创建MapperProxyFactory MapperProxy: 动态代理对象,实际上和Java代码中的Mapper接口对象,此处Mybatis的动态代理和实际的动态代理有些差距,并没有创建n 多个Java对象的具体实现类 MapperMethod: 所有Java mapper接口动态执行execute()方法
二、四个类之间的功能调用流程
- 在Mybatis 初始化加载过程中,扫描项目中所有Mybatis的xml配置文件和对应的mapper接口,加载构建Configuration对象时将所有的mapper接口通过MapperRegistry注册机加入到knownMappers Map中
- 在初始化完Configuration对象后,根据Configuration 创建一个SqlSession 对象,然后通过这个对象去获取mapper接口执行具体接口方法,功能代码主要体现在 sqlSession.getMapper()
- sqlSession.getMapper()方法的基本执行过程
- SqlSession.getMapper()
- DefaultSqlSession.getMapper()
- Configuration.getMapper()
- MapperRegistry.getMapper()
- MapperProxyFactory
mapperProxyFactory = knownMapper.get(type) - MapperProxy
mapperProxy = MapperProxyFactory.newInstance() - 最后返回一个真正的Java mapper接口对象
- 通过这个真正的接口动态代理调用 mapperProxy类的 invoke()方法
- MapperProxy 调用真正的实现方法 mapperMethod.execute()
- mapperMethod execute() 方法根据传入的sqlSession 调用SqlSession中的select()方法
- 后面全是SqlSession 的执行流程,看SqlSession的执行流程即可
上述流程具体代码流程如下: ```java // 1. 创建一个SqlSession 会话 SqlSession sqlSession = sqlSessionFactory.openSession(); // 2. 通过类获取mapper UserDao mapper = sqlSession.getMapper(UserDao.class); // 2.1 调用sqlSession getMappr() public interface SqlSession extends Closeable { /**
- Retrieves a mapper.
- 得到映射器
- 这个巧妙的使用了泛型,使得类型安全
- 到了MyBatis 3,还可以用注解,这样xml都不用写了
*/
T getMapper(Class type); } // 2.2 sqlSession 具体实现类执行getMapper方法 public class DefaultSqlSession implements SqlSession {
private Configuration configuration; private Executor executor; @Override public
T getMapper(Class type) { //最后会去调用MapperRegistry.getMapper return configuration. getMapper(type, this); } } // 2.3 去初始化完成的configuration 对象中获取已经加载的mapper接口 public class Configuration { //环境 protected Environment environment; public
T getMapper(Class type, SqlSession sqlSession) { return mapperRegistry.getMapper(type, sqlSession); } } // 2.4 获取 Configuration 对象中的 MapperRegistry 属性中的值 public class MapperRegistry { private Configuration config; //将已经添加的映射都放入HashMap private final Map
, MapperProxyFactory<?>> knownMappers = new HashMap , MapperProxyFactory<?>>(); public MapperRegistry(Configuration config) { this.config = config; }
@SuppressWarnings(“unchecked”) //返回代理类 public
T getMapper(Class type, SqlSession sqlSession) { final MapperProxyFactory mapperProxyFactory = (MapperProxyFactory ) knownMappers.get(type); if (mapperProxyFactory == null) { throw new BindingException(“Type “ + type + “ is not known to the MapperRegistry.”); } try { return mapperProxyFactory.newInstance(sqlSession); } catch (Exception e) { throw new BindingException(“Error getting mapper instance. Cause: “ + e, e); } } } // 2.5 创建映射器代理工厂创建对象,最后返回Java mapper接口的代理对象 /** - 映射器代理工厂
*/
public class MapperProxyFactory
{
private final Class
mapperInterface; private Map methodCache = new ConcurrentHashMap (); @SuppressWarnings(“unchecked”) protected T newInstance(MapperProxy mapperProxy) { //用JDK自带的动态代理生成映射器 return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy); } public T newInstance(SqlSession sqlSession) { final MapperProxy
mapperProxy = new MapperProxy (sqlSession, mapperInterface, methodCache); return newInstance(mapperProxy); }
} //3. Java 接口对象调用方法执行 User user1 = mapper.selectUser(1L);
3.1 动态代理执行invoke()方法 /**
映射器代理,代理模式 / public class MapperProxy
implements InvocationHandler, Serializable { private static final long serialVersionUID = -6424540398559729838L; private final SqlSession sqlSession; private final Class
mapperInterface; private final Map methodCache; public MapperProxy(SqlSession sqlSession, Class
mapperInterface, Map methodCache) { this.sqlSession = sqlSession; this.mapperInterface = mapperInterface; this.methodCache = methodCache; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { //代理以后,所有Mapper的方法调用时,都会调用这个invoke方法 //并不是任何一个方法都需要执行调用代理对象进行执行,如果这个方法是Object中通用的方法(toString、hashCode等)无需执行 if (Object.class.equals(method.getDeclaringClass())) { try {
return method.invoke(this, args);
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
} } //这里优化了,去缓存中找MapperMethod final MapperMethod mapperMethod = cachedMapperMethod(method); //执行 return mapperMethod.execute(sqlSession, args); } } 3.2 invoke() 方法 调用 mapperMethod.execute public class MapperMethod {
private final SqlCommand command; private final MethodSignature method;
public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) { this.command = new SqlCommand(config, mapperInterface, method); this.method = new MethodSignature(config, method); }
//执行 public Object execute(SqlSession sqlSession, Object[] args) { Object result; //可以看到执行时就是4种情况,insert|update|delete|select,分别调用SqlSession的4大类方法 if (SqlCommandType.INSERT == command.getType()) { Object param = method.convertArgsToSqlCommandParam(args); result = rowCountResult(sqlSession.insert(command.getName(), param)); } else if (SqlCommandType.UPDATE == command.getType()) { Object param = method.convertArgsToSqlCommandParam(args); result = rowCountResult(sqlSession.update(command.getName(), param)); } else if (SqlCommandType.DELETE == command.getType()) { Object param = method.convertArgsToSqlCommandParam(args); result = rowCountResult(sqlSession.delete(command.getName(), param)); } else if (SqlCommandType.SELECT == command.getType()) { if (method.returnsVoid() && method.hasResultHandler()) {
//如果有结果处理器
executeWithResultHandler(sqlSession, args);
result = null;
} else if (method.returnsMany()) {
//如果结果有多条记录
result = executeForMany(sqlSession, args);
} else if (method.returnsMap()) {
//如果结果是map
result = executeForMap(sqlSession, args);
} else {
//否则就是一条记录
Object param = method.convertArgsToSqlCommandParam(args);
result = sqlSession.selectOne(command.getName(), param);
} } else { throw new BindingException(“Unknown execution method for: “ + command.getName()); } if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) { throw new BindingException(“Mapper method ‘“ + command.getName()
+ " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
} return result; } } 3.3 mapperMethod.execute()方法调用最后的会话sqlSession.selectOne进行数据库实际操作 public interface SqlSession extends Closeable { /**
- Retrieve a single row mapped from the statement key and parameter.
- 根据指定的SqlID获取一条记录的封装对象,只不过这个方法容许我们可以给sql传递一些参数
- 一般在实际使用中,这个参数传递的是pojo,或者Map或者ImmutableMap
- @param
the returned object type - @param statement Unique identifier matching the statement to use.
- @param parameter A parameter object to pass to the statement.
- @return Mapped object
*/
T selectOne(String statement, Object parameter); }
> **根据上面的代码流程分分析,整个流程从 SqlSessionFactory.openSession()开始,从SqlSession.getMapper()从SqlSession 类开始各种内部调用,兜兜转转最后回到SqlSession去操作数据库**
<a name="AKj8P"></a>
#### 三、流程分解
1. 加载mapper接口到 knownMappers 中的流程
1. SqlFactoryBuilder builder() 读取配置文件进行解析,生成SqlSessionFactory对象
```java
//第4种方法是最常用的,它使用了一个参照了XML文档或更特定的SqlMapConfig.xml文件的Reader实例。
//可选的参数是environment和properties。Environment决定加载哪种环境(开发环境/生产环境),包括数据源和事务管理器。
//如果使用properties,那么就会加载那些properties(属性配置文件),那些属性可以用${propName}语法形式多次用在配置文件中。和Spring很像,一个思想?
public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
try {
//委托XMLConfigBuilder来解析xml文件,并构建
XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
return build(parser.parse());
} catch (Exception e) {
//这里是捕获异常,包装成自己的异常并抛出的idiom?,最后还要reset ErrorContext
throw ExceptionFactory.wrapException("Error building SqlSession.", e);
} finally {
ErrorContext.instance().reset();
try {
reader.close();
} catch (IOException e) {
// Intentionally ignore. Prefer previous error.
}
}
}
XMLConfigBuilder parse() 委托XMLConfigBuilder 解析XML文件进行构建Configuration对象 ```java //解析配置 public Configuration parse() { //如果已经解析过了,报错 if (parsed) { throw new BuilderException(“Each XMLConfigBuilder can only be used once.”); } parsed = true;
//根节点是configuration parseConfiguration(parser.evalNode(“/configuration”)); return configuration; }
//解析配置 private void parseConfiguration(XNode root) { try { //分步骤解析 //issue #117 read properties first //1.properties propertiesElement(root.evalNode(“properties”)); //2.类型别名 typeAliasesElement(root.evalNode(“typeAliases”)); //3.插件 pluginElement(root.evalNode(“plugins”)); //4.对象工厂 objectFactoryElement(root.evalNode(“objectFactory”)); //5.对象包装工厂 objectWrapperFactoryElement(root.evalNode(“objectWrapperFactory”)); //6.设置 settingsElement(root.evalNode(“settings”)); // read it after objectFactory and objectWrapperFactory issue #631 //7.环境 environmentsElement(root.evalNode(“environments”)); //8.databaseIdProvider databaseIdProviderElement(root.evalNode(“databaseIdProvider”)); //9.类型处理器 typeHandlerElement(root.evalNode(“typeHandlers”)); //10.映射器 mapperElement(root.evalNode(“mappers”)); } catch (Exception e) { throw new BuilderException(“Error parsing SQL Mapper Configuration. Cause: “ + e, e); } }
3. parseConfiguration 中 mapperElement() 方法解析 加载mapper 接口
1. configuration.addMappers(mapperPackage);
```java
private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
if ("package".equals(child.getName())) {
//10.4自动扫描包下所有映射器
String mapperPackage = child.getStringAttribute("name");
// TODO
configuration.addMappers(mapperPackage);
}
}
调用最后真正工作的类 MapperRegistry, mapperRegisrty.addMappers()
/**
* @since 3.2.2
*/
public void addMappers(String packageName, Class<?> superType) {
//查找包下所有是superType的类
ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<Class<?>>();
resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
Set<Class<? extends Class<?>>> mapperSet = resolverUtil.getClasses();
for (Class<?> mapperClass : mapperSet) {
addMapper(mapperClass);
}
}
/**
* @since 3.2.2
*/
//查找包下所有类
public void addMappers(String packageName) {
addMappers(packageName, Object.class);
}
三、Mybatis 的主要构件及其相互关系
从Mybatis 代码实现的角度来看,Mybatis的主要核心部分有以下几个:()
● Configuration mybatis所有的配置信息都维护在这个对象中
● SqlSession 完成承上启下的作用,承接Configuration完成创建,然后和数据库进行会话,完成CRUD操作
● Executor SQL代码执行器,负责SQL语句的生成和查询缓存的维护
● StatementHandler 封装了JDBC Statement操作,负责对JDBC statement的操作,如设置参数,将Statement结果集转换成List 集合
● ParameterHandler 负责对用户传递的参数转换成JDBC Statement 所需要的参数
● ResultSetHandler 负责将JDBC返回的ResultSet 结果集对象转化成List类型的集合
● TypeHandler 负责java类型和Jdbc类型的转换
● MapperStatement 是一个Map, key = namespace + id, value :mapperStatement对象,
● SqlSource 负责根据用户传入的parameterObject,动态生成SQL语句,将信息封装到BoundSql对 象中,并返回
● BoudSql 表示动态生成的SQL语句和相应的参数信息
● MapperProxy 所有Mapper的方法调用时,都会调用这个invoke方法
�Mybatis简易流程
Mybatis从大的角度去看,主要的核心构组件是:configuration、sqlSession、executor
基本流程:Mybaits 加载核心配置文件mapper接口,通过构造初始化为configuration对象
- sqlSessionBuild 根据Configuration 创建数据库会话 sqlSession
- sqlSession 委托给 executor 执行器去完成查询请求
其他的一些参数映射、实体映射、缓存、事物等都是从这三大核心构建中细化。
Mybatis 层次结构
![image.png](https://cdn.nlark.com/yuque/0/2021/png/12747730/1636595179881-2b2ce8b7-f31c-4ae2-92d7-2e2b61154c1e.png#clientId=uf627a5c1-46e0-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=584&id=ufd97ab99&margin=%5Bobject%20Object%5D&name=image.png&originHeight=1167&originWidth=905&originalType=binary&ratio=1&rotation=0&showTitle=false&size=111397&status=done&style=none&taskId=u6e118512-6b01-4fd0-aeea-88d86e36c26&title=&width=452.5)
四、Mybatis核心模块梳理
1.日志模块
2.数据源模块
3.缓存模块
3.1 缓存机制的设计与实现
Mybatis 将数据缓存设计为两级结构:一级缓存(Session)、二级缓存(namespace)
一级缓存是Session会话级别的缓存,位于表示一次会话的SqlSession对象之中,又称为本地缓存。一级缓存是Mybatis内部设置的特性,用户不能配置,Mybatis 默认是自动支持缓存的。用户没有设置它的权利(但不是绝对的,可以开发插件对它进行修改->(大的思路,实现plugin包下面的Interceptor接口,实现具体方法,InterceptorChain 拦截器链会进行解析自定义的插件生效�))
二级缓存是Application应用级别的缓存,它的生命周期很长,和Application生命周期一样,也是就说它的作用范围是整个Application应用。
一级缓存的工作机制:
一级缓存是Session会话级别的缓存,一般而言,一个SqlSession 对象会使用一个Executor 对象来完成会话操作,Executor对象会维护一个Cache 缓存,从而提高查询性能。
二级缓存的工作机制:
一个SqlSession对象会使用一个Executor对象来完成会话操作,MyBatis的二级缓存机制的关键就是对这个Executor对象做文章。如果用户配置了“cacheEnabled=true”,那么MyBatis在为SqlSession对象创建Executor对象时,会对Executor对象加上一个装饰者:CachingExecutor,这时SqlSession使用CachingExecutor对象来完成操作请求。CachingExecutor对于查询请求,会先判断该查询请求在Application级别的二级缓存中是否有缓存结果,如果有查询结果,则直接返回缓存结果;如果缓存中没有,再交给真正的Executor对象来完成查询操作,之后CachingExecutor会将真正Executor返回的查询结果放置到缓存中,然后在返回给用户。
使用两级缓存的基本示意图
两级缓存的使用顺序
一级和二级缓存的选择
对一级缓存和二级缓存做个简单总结:
一级缓存
- 一级缓存是Mybatis默认开启的sqlSession级别的缓存,底层主要使用HashMap实现,在使用hashMap实现的时候没有考虑扩容等情况;由于sqlSession 中缓存对象会被频繁修改,所以一般不用担心缓存内存大多导致JVM,也不用考虑缓存存储多长,什么时间过期等情况。
- 使用一级缓存的时候注意:
- 对于数据变化频率很大,并且需要高时效准确性的数据要求,我们使用SqlSession查询的时候,要控制好SqlSession的生存时间,SqlSession的生存时间越长,它其中缓存的数据有可能就越旧,从而造成和真实数据库的误差;同时对于这种情况,用户也可以手动地适时清空SqlSession中的缓存;
- 对于只执行、并且频繁执行大范围的select操作的SqlSession对象,SqlSession对象的生存时间不应过长。
二级缓存
- 二级缓存是namespace级别的,是sqlSession共享的,容易出现脏读,避免使用二级缓存
二级缓不适用分布式场景中,容易产生脏读,如果要满足需要自己去开发插件实现,成本比较高。
3.2 一级缓存
1.什么是一级缓存,为什么要使用一级缓存
每当使用Mybatis开启一次和数据库点会话,Mybatis就会创建出一个sqlSession对象表示一次数据库会话。
在对数据库的一次会话中,我们有可能会反复地执行完全相同的查询语句,如果不采取一些措施的话,每一次查询都会查询一次数据库,而我们在极短的时间内做了完全相同的查询,那么它们的结果极有可能完全相同,由于查询一次数据库的代价很大,这有可能造成很大的资源浪费。
为了解决这一问题,减少资源的浪费,MyBatis会在表示会话的SqlSession对象中建立一个简单的缓存,将每次查询到的结果结果缓存起来,当下次查询的时候,如果判断先前有个完全一样的查询,会直接从缓存中直接将结果取出,返回给用户,不需要再进行一次数据库查询了。
如下图所示,MyBatis会在一次会话的表示----一个SqlSession对象中创建一个本地缓存(local cache),对于每一次查询,都会尝试根据查询的条件去本地缓存中查找是否在缓存中,如果在缓存中,就直接从缓存中取出,然后返回给用户;否则,从数据库读取数据,将查询结果存入缓存并返回给用户。<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/12747730/1636508922829-d6656a05-1f04-46cb-83db-6db79c410986.png#clientId=u39de18c9-8404-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=212&id=ua670e2aa&margin=%5Bobject%20Object%5D&name=image.png&originHeight=423&originWidth=840&originalType=binary&ratio=1&rotation=0&showTitle=false&size=40742&status=done&style=none&taskId=ua50615b1-0cb2-45ed-8532-27792e983ef&title=&width=420)
2.Mybatis 中的一级缓存是怎样组织的
由于Mybatis 使用SqlSession 表示一次数据库会话,所以Mybatis的一级缓存由SqlSession 来控制。
实际上,SqlSession 只是一个Mybatis 对外的接口,SqlSession 将它的工作交接给Executor执行器这个角色来完成,负责完成对数据库的各种操作。当创建一个SqlSession对象时,同时会为SqlSession创建一个Executor执行器,而缓存信息就维护在这Executor执行器中,Mybatis 将缓存和对缓存相关的操作封装在Cache接口中。
SqlSession、Executor、Cache三者基本关系:
根据上述的类图所示,Executor接口的实现类BaseExecutor 中定义了一个Cache 接口的实现类PerpetualCache,则对BaseExecutor对象而言,它将使用PerpetialCache对象来维护缓存;具体代码如下:
BaseExecutor 实现 Executor 接口,同时定义 PerpetualCache 缓存对象
public abstract class BaseExecutor implements Executor {
private static final Log log = LogFactory.getLog(BaseExecutor.class);
protected Transaction transaction;
protected Executor wrapper;
//延迟加载队列(线程安全)
protected ConcurrentLinkedQueue<DeferredLoad> deferredLoads;
//本地缓存机制(Local Cache)防止循环引用(circular references)和加速重复嵌套查询(一级缓存)
//本地缓存
protected PerpetualCache localCache;
}
PerpetualCache 实现 Cache 接口,进行缓存维护
/**
* 永久缓存
* 一旦存入就一直保持
*
*/
public class PerpetualCache implements Cache {
//每个永久缓存有一个ID来识别
private String id;
//内部就是一个HashMap,所有方法基本就是直接调用HashMap的方法,不支持多线程?
private Map<Object, Object> cache = new HashMap<Object, Object>();
public PerpetualCache(String id) {
this.id = id;
}
@Override
public String getId() {
return id;
}
@Override
public int getSize() {
return cache.size();
}
@Override
public void putObject(Object key, Object value) {
cache.put(key, value);
}
@Override
public Object getObject(Object key) {
return cache.get(key);
}
@Override
public Object removeObject(Object key) {
return cache.remove(key);
}
@Override
public void clear() {
cache.clear();
}
@Override
public ReadWriteLock getReadWriteLock() {
return null;
}
综上,SqlSession对象、Executor对象、Cache对象之间的关系如下图所示:
3.一级缓存的生命周期
- Mybatis 在开启一个次数据库会话时,会创建一个新的SqlSession对象,sqlSession 对象中会有一个新的Executor对象,Executor对象中会持有一个新的PerpetualCache对象;当话结束时,SqlSession对象及其内部的Executor对象还有PerpetualCache对象也一并释放
- 如果调用了sqlSession对象的close()方法,会释放掉PerpetualCache对象,一级缓存将不可用;
- 如果SqlSession对象调用了clearCache(), 会清空PrepetualCache对象中的数据,但是该对象仍可使用
SqlSession 中执行了任何一个update操作,都会清空PerpetualCache对象的数据,但是该对象可以继续使用
![image.png](https://cdn.nlark.com/yuque/0/2021/png/12747730/1636511023235-3f7754e0-e5e0-4bb6-8f0c-c3eedc1abfa9.png#clientId=u39de18c9-8404-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=151&id=u3595b831&margin=%5Bobject%20Object%5D&name=image.png&originHeight=302&originWidth=794&originalType=binary&ratio=1&rotation=0&showTitle=false&size=27921&status=done&style=none&taskId=u63584c84-a028-42da-b47c-fdcaa869291&title=&width=397)<br />**close基本流程:sqlSession.close() -> executor.close() -> transaction.close() ->connection.close()**<br />�
此处代码 ......
clear基本流程:sqlSession.clear() -> executor.clear() -> prepetualCache.clear() -> hashMap.clear() ```java
第一步:
/**
- 这是MyBatis主要的一个类,用来执行SQL,获取映射器,管理事务 *
- 通常情况下,我们在应用程序中使用的Mybatis的API就是这个接口定义的方法。 / public interface SqlSession extends Closeable {
/**
- Clears local session cache
- 清理Session缓存
/
void clearCache();
}
/*
- 默认SqlSession实现
/
public class DefaultSqlSession implements SqlSession {
//核心clearCache
@Override
public void clearCache() {
//转而用执行器来clearLocalCache
executor.clearLocalCache();
}
}
第二步
/** - 执行器 / public interface Executor { //清理Session缓存 void clearLocalCache(); } /**
- 执行器基类
/
public abstract class BaseExecutor implements Executor {
//本地缓存
protected PerpetualCache localCache;
//本地输出参数缓存
protected PerpetualCache localOutputParameterCache;
@Override
public void clearLocalCache() {
if (!closed) {
localCache.clear();
localOutputParameterCache.clear();
}
}
}
第三步
/* */ public class PerpetualCache implements Cache { @Override public void clear() { cache.clear(); } } // 最后调用了hashMap的 clear()方法,清除元素,对象继续保留。 // 从上面的clear 流程可以看出, 一级缓存中的三个对象关系,相互包含。 ```4.一级缓存的工作流程
- 默认SqlSession实现
/
public class DefaultSqlSession implements SqlSession {
//核心clearCache
@Override
public void clearCache() {
//转而用执行器来clearLocalCache
executor.clearLocalCache();
}
}
对于某个查询,根据 statementId,params,rowBounds来构建一个key值,根据这个key值去缓存Cache中取出对应的key值存储的缓存结果;
判断从Cache中根据特定的key值取的数据数据是否为空,即是否命中;
如果命中,则直接将缓存结果返回;
如果没命中:
4.1 去数据库中查询数据,得到查询结果;<br /> 4.2 将key和查询到的结果分别作为key,value对存储到Cache中;<br /> 4.3. 将查询结果返回;
结束。
![image.png](https://cdn.nlark.com/yuque/0/2021/png/12747730/1636512601098-6c0e4faa-0bf9-4f2c-bf2c-0c26e1a8a843.png#clientId=u39de18c9-8404-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=519&id=u97c39744&margin=%5Bobject%20Object%5D&name=image.png&originHeight=1037&originWidth=867&originalType=binary&ratio=1&rotation=0&showTitle=false&size=92624&status=done&style=none&taskId=uf523481c-d683-4a83-a002-11a027abb6e&title=&width=433.5)
5.Cache接口的设计和CacheKey的定义
如下图所示,MyBatis定义了一个org.apache.ibatis.cache.Cache接口作为其Cache提供者的SPI(Service Provider Interface) ,所有的MyBatis内部的Cache缓存,都应该实现这一接口。MyBatis定义了一个PerpetualCache实现类实现了Cache接口,实际上,在SqlSession对象里的Executor 对象内维护的Cache类型实例对象,就是PerpetualCache子类创建的。
(MyBatis内部还有很多Cache接口的实现,一级缓存只会涉及到这一个PerpetualCache子类,Cache的其他实现将会放到二级缓存中介绍)。
我们知道,Cache最核心的实现其实就是一个Map,将本次查询使用的特征值作为key,将查询结果作为value存储到Map中。
现在最核心的问题出现了:怎样来确定一次查询的特征值?
换句话说就是:怎样判断某两次查询是完全相同的查询?
也可以这样说:如何确定Cache中的key值?
MyBatis认为,对于两次查询,如果以下条件都完全一样,那么就认为它们是完全相同的两次查询:
- 传入的 statementId
- 查询时要求的结果集中的结果范围 (结果的范围通过rowBounds.offset和rowBounds.limit表示);
- 这次查询所产生的最终要传递给JDBC java.sql.Preparedstatement的Sql语句字符串(boundSql.getSql())
- 传递给java.sql.Statement要设置的参数值
现在分别解释上述四个条件:
- 传入的statementId,对于MyBatis而言,你要使用它,必须需要一个statementId,它代表着你将执行什么样的Sql;
- MyBatis自身提供的分页功能是通过RowBounds来实现的,它通过rowBounds.offset和rowBounds.limit来过滤查询出来的结果集,这种分页功能是基于查询结果的再过滤,而不是进行数据库的物理分页; 由于MyBatis底层还是依赖于JDBC实现的,那么,对于两次完全一模一样的查询,MyBatis要保证对于底层JDBC而言,也是完全一致的查询才行。而对于JDBC而言,两次查询,只要传入给JDBC的SQL语句完全一致,传入的参数也完全一致,就认为是两次查询是完全一致的。
上述的第3个条件正是要求保证传递给JDBC的SQL语句完全一致;第4条则是保证传递给JDBC的参数也完全一致;
即3、4两条MyBatis最本质的要求就是:
调用JDBC的时候,传入的SQL语句要完全相同,传递给JDBC的参数值也要完全相同。
综上所述,CacheKey由以下条件决定:
statementId + rowBounds + 传递给JDBC的SQL + 传递给JDBC的参数值[
](https://blog.csdn.net/luanlouis/article/details/41280959)
CacheKey 创建:
对于每次的查询请求,Executor都会根据传递的参数信息以及动态生成的SQL语句,将上面的条件根据一定的计算规则,创建一个对应的CacheKey对象。
//创建缓存Key ==》 借鉴学习,当自己在开发中的key太长可以生成hash码来解决
@Override
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
if (closed) {
throw new ExecutorException("Executor was closed.");
}
CacheKey cacheKey = new CacheKey();
//MyBatis 对于其 Key 的生成采取规则为:[mappedStementId + offset + limit + SQL + queryParams + environment]生成一个哈希码
cacheKey.update(ms.getId());
cacheKey.update(Integer.valueOf(rowBounds.getOffset()));
cacheKey.update(Integer.valueOf(rowBounds.getLimit()));
cacheKey.update(boundSql.getSql());
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
// mimic DefaultParameterHandler logic
//模仿DefaultParameterHandler的逻辑,不再重复,请参考DefaultParameterHandler
for (int i = 0; i < parameterMappings.size(); i++) {
ParameterMapping parameterMapping = parameterMappings.get(i);
if (parameterMapping.getMode() != ParameterMode.OUT) {
Object value;
String propertyName = parameterMapping.getProperty();
if (boundSql.hasAdditionalParameter(propertyName)) {
value = boundSql.getAdditionalParameter(propertyName);
} else if (parameterObject == null) {
value = null;
} else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
value = parameterObject;
} else {
MetaObject metaObject = configuration.newMetaObject(parameterObject);
value = metaObject.getValue(propertyName);
}
cacheKey.update(value);
}
}
if (configuration.getEnvironment() != null) {
// issue #176
cacheKey.update(configuration.getEnvironment().getId());
}
return cacheKey;
}
一级缓存的性能
1.MyBatis对会话(Session)级别的一级缓存设计的比较简单,就简单地使用了HashMap来维护,并没有对HashMap的容量和大小进行限制。
读者有可能就觉得不妥了:如果我一直使用某一个SqlSession对象查询数据,这样会不会导致HashMap太大,而导致 java.lang.OutOfMemoryError错误啊? 读者这么考虑也不无道理,不过MyBatis的确是这样设计的。 MyBatis这样设计也有它自己的理由: a. 一般而言SqlSession的生存时间很短。一般情况下使用一个SqlSession对象执行的操作不会太多,执行完就会消亡; b. 对于某一个SqlSession对象而言,只要执行update操作(update、insert、delete),都会将这个SqlSession对象中对应的一级缓存清空掉,所以一般情况下不会出现缓存过大,影响JVM内存空间的问题; c. 可以手动地释放掉SqlSession对象中的缓存。
一级缓存是一个粗粒度的缓存,没有更新缓存和缓存过期的概念
MyBatis的一级缓存就是使用了简单的HashMap,MyBatis只负责将查询数据库的结果存储到缓存中去, 不会去判断缓存存放的时间是否过长、是否过期,因此也就没有对缓存的结果进行更新这一说了。
使用一级缓存的注意:
1、对于数据变化频率很大,并且需要高时效准确性的数据要求,我们使用SqlSession查询的时候,要控制好SqlSession的生存时间,SqlSession的生存时间越长,它其中缓存的数据有可能就越旧,从而造成和真实数据库的误差;同时对于这种情况,用户也可以手动地适时清空SqlSession中的缓存;2、对于只执行、并且频繁执行大范围的select操作的SqlSession对象,SqlSession对象的生存时间不应过长。
3.3 二级缓存
1. 二级缓存工作模式
在一级缓存中我们已经了解到,当开一个会话时,sqlSession 对象会使用一个executor对象完成来完成会话操作,Mybatis 的二级缓存关键就是对Executor对象做文章。如果用户配置了”cacheEnabled=true“,那么Mybatis 在为SqlSession对象创建Executor对象时,会为Executor对象加上一个装饰者:CachingExecutor,这时sqlSession 通过CachingExecutor 来完成操作请求。CachingExecutor对于查询请求,会先判断该查询请求在Application级别的二级缓存中是否有缓存结果,如果有查询结果,则直接返回缓存结果;如果缓存中没有,再交给真正的Executor对象来完成查询操作,之后CachingExecutor会将真正Executor返回的查询结果放置到缓存中,然后在返回给用户。
CachingExecutor是Executor的装饰者,以增强Executor的功能,使其具有缓存查询的功能,这里用到了设计模式中的装饰者模式,
CachingExecutor和Executor的接口的关系如下类图所示:
2.二级缓存的划分
Mybatis 并不是简单的对整个Application只有一个Cache缓存对象,它将缓存划分的更细,即是Mapper 级别的,即每一个mapper都拥有一个Cache对象。
a.为每一个Mapper分配一个Cache缓存对象(使用
节点配置); b.多个Mapper共用一个Cache缓存对象(使用节点配置);
a.为每一个Mapper分配一个Cache缓存对象(使用
MyBatis将Application级别的二级缓存细分到Mapper级别,即对于每一个Mapper.xml,如果在其中使用了
注: 上述的每一个Cache对象,都会有一个自己所属的namespace命名空间,并且会将Mapper的 namespace作为它们的ID;
b.多个Mapper共用一个Cache缓存对象(使用
如果你想让多个Mapper公用一个Cache的话,你可以使用
[
](https://blog.csdn.net/luanlouis/article/details/41408341)
4.事物机制
五、Mybatis 源码模块
Mybatis 源码模块从整体来看主要分为两大模块:初始化阶段、会话阶段。
初始化阶段:加载程序运行时需要的配置信息,解析xml 配置和扫描 mapper 构建Configuration对象;简单的理解初始化过程就是创建Configuration 的过程。
会话阶段:创建SqlSession会话,建立和数据库的会话,委托Executor 对象执行,解析构建SQL语句,解析参数,返回执行结果
1.第一步:Mybatis 初始化机制(创建Configuration)
一、Mybatis 初始化做了什么
我们在项目中使用Mybatis的时候会进行一些基本的配置和高级设置,这些信息被加载到Mybatis 内部是怎么维护和处理的?
Mybatis 其实使用了一种比较简单的方式,使用 —org.apache.ibatis.session.Configuration 对象作为完成对这些信息存储的容器,Configuration 对象组织结构和XML配置文件中的组织结构几乎完全一样。
�
� Mybatis 根据初始化好的Configuration信息就可以操数据了
可以这么说,Mybatis的初始化就是创建Configuration的过程。
1.1 Mybatis 初始化的两种方式
- 基于XML配置文件:基于XML配置文件的方式是将MyBatis的所有配置信息放在XML文件中,MyBatis通过加载并XML配置文件,将配置文信息组装成内部的Configuration对象
- 基于Java API:这种方式不使用XML配置文件,需要MyBatis使用者在Java代码中,手动创建Configuration对象,然后将配置参数set 进入Configuration对象中
二、MyBatis基于XML配置文件创建Configuration对象的过程
整个分析从一段测试代码开始,总体来说这段代码主要包括三个过程,初始化configuration —> 创建sqlSession —> 执行SQL语句 返回结果
String resource = "resources/mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession = sqlSessionFactory.openSession();
try {
User user = sqlSession.selectOne("com.mybatis.test.UserDao.selectUser", 1);
} finally {
sqlSession.close();
}
这段代码的功能是根据配置文件mybatis-config.xml,创建SqlSessionFactory对象,然后返回sqlSession,执行SQL语句。Mybatis的初始化就是从第三行代码。
Mybatis初始化基本过程
SqlSessionFactoryBuilder 根据传入的数据流返回Configuration对象,然后根据Configuration对象创建默认的SqlSessionFactory实例。
初始化的工程基本流程图如下:
由上图所示,mybatis初始化要经过简单的以下几步:
1. 调用SqlSessionFactoryBuilder对象的build(inputStream)方法;
2. SqlSessionFactoryBuilder会根据输入流inputStream等信息创建XMLConfigBuilder对象;
3. SqlSessionFactoryBuilder调用XMLConfigBuilder对象的parse()方法;
4. XMLConfigBuilder对象返回Configuration对象;
5. SqlSessionFactoryBuilder根据Configuration对象创建一个DefaultSessionFactory对象;
6. SqlSessionFactoryBuilder返回 DefaultSessionFactory对象给Client,供Client使用。
SqlSessionFactoryBuilder 代码如下:
public class SqlSessionFactoryBuilder {
//SqlSessionFactoryBuilder有9个build()方法
//发现mybatis文档老了,http://www.mybatis.org/core/java-api.html,关于这块对不上
//以下3个方法都是调用下面第4种方法
public SqlSessionFactory build(Reader reader) {
return build(reader, null, null);
}
public SqlSessionFactory build(Reader reader, String environment) {
return build(reader, environment, null);
}
public SqlSessionFactory build(Reader reader, Properties properties) {
return build(reader, null, properties);
}
//第4种方法是最常用的,它使用了一个参照了XML文档或更特定的SqlMapConfig.xml文件的Reader实例。
//可选的参数是environment和properties。Environment决定加载哪种环境(开发环境/生产环境),包括数据源和事务管理器。
//如果使用properties,那么就会加载那些properties(属性配置文件),那些属性可以用${propName}语法形式多次用在配置文件中。和Spring很像,一个思想?
public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
try {
//委托XMLConfigBuilder来解析xml文件,并构建
XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
return build(parser.parse());
} catch (Exception e) {
//这里是捕获异常,包装成自己的异常并抛出的idiom?,最后还要reset ErrorContext
throw ExceptionFactory.wrapException("Error building SqlSession.", e);
} finally {
ErrorContext.instance().reset();
try {
reader.close();
} catch (IOException e) {
// Intentionally ignore. Prefer previous error.
}
}
}
//以下3个方法都是调用下面第8种方法
public SqlSessionFactory build(InputStream inputStream) {
return build(inputStream, null, null);
}
public SqlSessionFactory build(InputStream inputStream, String environment) {
return build(inputStream, environment, null);
}
public SqlSessionFactory build(InputStream inputStream, Properties properties) {
return build(inputStream, null, properties);
}
//第8种方法和第4种方法差不多,Reader换成了InputStream
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
try {
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
return build(parser.parse());
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error building SqlSession.", e);
} finally {
ErrorContext.instance().reset();
try {
inputStream.close();
} catch (IOException e) {
// Intentionally ignore. Prefer previous error.
}
}
}
//最后一个build方法使用了一个Configuration作为参数,并返回DefaultSqlSessionFactory
public SqlSessionFactory build(Configuration config) {
return new DefaultSqlSessionFactory(config);
}
}
�上述的初始化过程中,涉及到了以下几个对象:
SqlSessionFactoryBuilder : SqlSessionFactory的构造器,用于创建SqlSessionFactory,采用了Builder设计模式 Configuration :该对象是mybatis-config.xml文件中所有mybatis配置信息 SqlSessionFactory:SqlSession工厂类,以工厂形式创建SqlSession对象,采用了Factory工厂设计模式 XmlConfigBuilder :负责将mybatis-config.xml配置文件解析成Configuration对象,供SqlSessonFactoryBuilder使用,创建SqlSessionFactory
创建Configuration对象的过程
接着上述Mybatis初始化基本过程讨论,当SqlSessionFactoryBuilder执行build()的方法,调用了XMLConfigBuilder的parse()方法,返回了Configuration对象。基本流程代码如下:
// 1. SqlSessionFactoryBuilder 调用 build()方法
// 2. build()方法 内部创建XMLConfigBuilder对象,调用parse()方法
public class SqlSessionFactoryBuilder {
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
try {
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
return build(parser.parse());
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error building SqlSession.", e);
} finally {
ErrorContext.instance().reset();
try {
inputStream.close();
} catch (IOException e) {
// Intentionally ignore. Prefer previous error.
}
}
}
}
//3. XMLConfigBuilder parse()方法解析配置文件,将配置设置到Configuration属性上
/**
* XML配置构建器,建造者模式,继承BaseBuilder
*
*/
public class XMLConfigBuilder extends BaseBuilder {
//解析配置
public Configuration parse() {
//如果已经解析过了,报错
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
parsed = true;
//根节点是configuration
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}
}
//解析配置
private void parseConfiguration(XNode root) {
try {
//分步骤解析
//issue #117 read properties first
//1.properties
propertiesElement(root.evalNode("properties"));
//2.类型别名
typeAliasesElement(root.evalNode("typeAliases"));
//3.插件
pluginElement(root.evalNode("plugins"));
//4.对象工厂
objectFactoryElement(root.evalNode("objectFactory"));
//5.对象包装工厂
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
//6.设置
settingsElement(root.evalNode("settings"));
// read it after objectFactory and objectWrapperFactory issue #631
//7.环境
environmentsElement(root.evalNode("environments"));
//8.databaseIdProvider
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
//9.类型处理器
typeHandlerElement(root.evalNode("typeHandlers"));
//10.映射器
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
�那么parse()方法是如何处理XML文件,生成Configuration对象的呢?
1. XMLConfigBuilder会将XML配置文件的信息转换为Document对象,而XML配置定义文件DTD转换成XMLMapperEntityResolver对象,然后将二者封装到XpathParser对象中,XpathParser的作用是提供根据Xpath表达式获取基本的DOM节点Node信息的操作。如下图所示:
我们将上述的MyBatis初始化基本过程的序列图细化,
2.第二步:执行SQL语句(创建SqlSession并使用完成执行)
Mybatis的核心过程我们已经学习了第一步,即初始化创建Configuration对象的过程,当configuration 对象创建完成后,以后Sql语句的执行就交给SqlSession 去完成了,中间有很多操作过程和细节会有很多参与者。
SqlSession 的工作过程分析:
1. 开启一个数据库访问回话—-创建sqlSession 对象
SqlSession sqlSession = sqlSessionFactory.openSession();
DefaultSqlSessionFactor创建一个Session的基本过程: 在创建完成SqlSession去创建Executor 对象的时候,会把事物和缓存都创建到Executror 中。
/**
* 默认的SqlSessionFactory
*
*/
public class DefaultSqlSessionFactory implements SqlSessionFactory {
private final Configuration configuration;
public DefaultSqlSessionFactory(Configuration configuration) {
this.configuration = configuration;
}
//最终都会调用2种方法:openSessionFromDataSource,openSessionFromConnection
//以下6个方法都会调用openSessionFromDataSource
@Override
public SqlSession openSession() {
return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
}
}
// openSession -> openSessionFromDataSource
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;
try {
final Environment environment = configuration.getEnvironment();
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
//通过事务工厂来产生一个事务
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
//生成一个执行器(事务包含在执行器里)
final Executor executor = configuration.newExecutor(tx, execType);
//然后产生一个DefaultSqlSession
return new DefaultSqlSession(configuration, executor, autoCommit);
} catch (Exception e) {
//如果打开事务出错,则关闭它
closeTransaction(tx); // may have fetched a connection so lets call close()
throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e);
} finally {
//最后清空错误上下文
ErrorContext.instance().reset();
}
}
//产生执行器 openSession -> openSessionFromDataSource -> new Executor
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
executorType = executorType == null ? defaultExecutorType : executorType;
//这句再做一下保护,囧,防止粗心大意的人将defaultExecutorType设成null?
executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
Executor executor;
//然后就是简单的3个分支,产生3种执行器BatchExecutor/ReuseExecutor/SimpleExecutor
if (ExecutorType.BATCH == executorType) {
executor = new BatchExecutor(this, transaction);
} else if (ExecutorType.REUSE == executorType) {
executor = new ReuseExecutor(this, transaction);
} else {
executor = new SimpleExecutor(this, transaction);
}
//如果要求缓存,生成另一种CachingExecutor(默认就是有缓存),装饰者模式,所以默认都是返回CachingExecutor
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
//此处调用插件,通过插件可以改变Executor行为
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
Mybatis封装了对数据库的访问,把数据库的会话和事务控制放到了sqlSession对象中
2.为SqlSession传递一个配置的SQL语句的statement Id 和参数,然后返回结果
User user = sqlSession.selectOne("com.mybatis.test.UserDao.selectUser", 1);
上面代码中的“com.mybatis.test.UserDao.selectUser”,是配置再UserMapper.xml中的Statement ID,params是传递的查询参数。
说明:Mybatis中的selectOne 底层其实调用的是selectList()方法,所以在使用这个API的时候,不能明确知道只有唯一一条数据的时候需要加limit 1,不然因为脏数据导致SQL报错。
//核心selectOne
@Override
public <T> T selectOne(String statement, Object parameter) {
// Popular vote was to return null on 0 results and throw exception on too many.
//转而去调用selectList,很简单的,如果得到0条则返回null,得到1条则返回1条,得到多条报TooManyResultsException错
// 特别需要主要的是当没有查询到结果的时候就会返回null。因此一般建议在mapper中编写resultType的时候使用包装类型
//而不是基本类型,比如推荐使用Integer而不是int。这样就可以避免NPE
List<T> list = this.<T>selectList(statement, parameter);
if (list.size() == 1) {
return list.get(0);
} else if (list.size() > 1) {
throw new TooManyResultsException("Expected one result (or null) to be returned by selectOne(), but found: " + list.size());
} else {
return null;
}
}
因为底层调用的是selectList,所以接下来对简单跟踪selectList() 的实现。
@Override
public <E> List<E> selectList(String statement, Object parameter) {
return this.selectList(statement, parameter, RowBounds.DEFAULT);
}
//核心selectList
@Override
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
try {
//根据statement id找到对应的MappedStatement
//根据命名空间 + xml中的id 去获取
MappedStatement ms = configuration.getMappedStatement(statement);
//转而用执行器来查询结果,注意这里传入的ResultHandler是null
return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
说明:Mybatis 在初始化的时候,会将Mybatis的配置信息全部加载到内存中,使用org.apache.ibatis.session.Configuration实例来维护。使用者可以使用sqlSession.getConfiguration()方法来获取。Mybatis 的配置文件中配置信息的组织格式和内存中对象的组织格式几乎完全对应。
<mapper namespace="com.mybatis.test.UserDao">
<select id="selectUser" resultType="com.mybaits.test.User">
SELECT * FROM User where id = #{id}
</select>
</mapper>
加载到内存中会生成一个对应的MapperStatement对象,然后会以key=”com.mybatis.test.UserDao.selectUser”, value为MappedStatement对象的形式维护在Configuration的一个mappedStatements中,当我们使用的时候通过id值来获取就可以了。
//映射的语句,存在Map里
protected final Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>("Mapped Statements collection");
从上述的代码中我们可以看到SqlSession的职能是:
sqlSession 根据Statement ID,在Mybatis配置对象Configuration中获取到对应的MapperStatement对象,然后调用mybatis执行器来执行具体的操作。
3. Mybatis执行器Executor根据SqlSession传递的参数执行query()方法。
/**
* BaseExecutor 类部分代码
*
*/
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
// 1.根据具体传入的参数,动态地生成需要执行的SQL语句,用BoundSql对象表示
BoundSql boundSql = ms.getBoundSql(parameter);
// 2.为当前的查询创建一个缓存Key,缓存key 的生成在缓存模块已经讲述过了,请自己查阅
CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
@SuppressWarnings("unchecked")
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());
//如果已经关闭,报错
if (closed) throw new ExecutorException("Executor was closed.");
//先清局部缓存,再查询.但仅查询堆栈为0,才清。为了处理递归调用
if (queryStack == 0 && ms.isFlushCacheRequired()) {
clearLocalCache();
}
List<E> list;
try {
//加一,这样递归调用到上面的时候就不会再清局部缓存了
queryStack++;
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
if (list != null) {
//先根据cachekey从localCache去查
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
// 3.缓存中没有值,直接从数据库中读取数据
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
} finally {
queryStack--;
}
if (queryStack == 0) {
for (DeferredLoad deferredLoad : deferredLoads) {
deferredLoad.load();
}
deferredLoads.clear(); // issue #601
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
clearLocalCache(); // issue #482
}
}
return list;
}
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
List<E> list;
localCache.putObject(key, EXECUTION_PLACEHOLDER);
try {
//4. 执行查询,返回List 结果,然后 将查询的结果放入缓存之中
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
localCache.removeObject(key);
}
localCache.putObject(key, list);
if (ms.getStatementType() == StatementType.CALLABLE) {
localOutputParameterCache.putObject(key, parameter);
}
return list;
}
** query-->queryFromDatabase-->doQuery**
/**
*
*SimpleExecutor类的doQuery()方法实现
*
*/
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
Statement stmt = null;
try {
Configuration configuration = ms.getConfiguration();
//5. 根据既有的参数,创建StatementHandler对象来执行查询操作
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
//6. 创建java.Sql.Statement对象,传递给StatementHandler对象
stmt = prepareStatement(handler, ms.getStatementLog());
//7. 调用StatementHandler.query()方法,返回List结果集
return handler.<E>query(stmt, resultHandler);
} finally {
closeStatement(stmt);
}
}
上述的Executor.query()方法几经转折,最后会创建一个StatementHandler对象,然后将必要的参数传递给StatementHandler,使用StatementHandler来完成对数据库的查询,最终返回List结果集。
从上面的代码中我们可以看出,Executor的功能和作用是:
(1、根据传递的参数,完成SQL语句的动态解析,生成BoundSql对象,供StatementHandler使用;
(2、为查询创建缓存,以提高性能(具体它的缓存机制不是本文的重点,我会单独拿出来跟大家探讨,感兴趣的读者可以关注我的其他博文);
(3、创建JDBC的Statement连接对象,传递给StatementHandler对象,返回List查询结果。
4. StatementHandler对象负责设置Statement对象中的查询参数、处理JDBC返回的resultSet,将resultSet加工为List 集合返回:
**接着上面的Executor第六步,看一下:prepareStatement() 方法的实现:**
/**
*
*SimpleExecutor类的doQuery()方法实现
*
*/
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
Statement stmt = null;
try {
Configuration configuration = ms.getConfiguration();
//5. 根据既有的参数,创建StatementHandler对象来执行查询操作
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
//6. 创建java.Sql.Statement对象,传递给StatementHandler对象
stmt = prepareStatement(handler, ms.getStatementLog());
//7. 调用StatementHandler.query()方法,返回List结果集
return handler.<E>query(stmt, resultHandler);
} finally {
closeStatement(stmt);
}
}
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
Statement stmt;
Connection connection = getConnection(statementLog);
stmt = handler.prepare(connection);
//对创建的Statement对象设置参数,即设置SQL 语句中 ? 设置为指定的参数
handler.parameterize(stmt);
return stmt;
}
以上我们可以总结StatementHandler对象主要完成两个工作:
** (1. 对于JDBC的PreparedStatement类型的对象,创建的过程中,我们使用的是SQL语句字符串会包含 若干个? 占位符,我们其后再对占位符进行设值。**<br />StatementHandler通过parameterize(statement)方法对Statement进行设值; <br /> **(2.StatementHandler通过List<E> query(Statement statement, ResultHandler resultHandler)方法来完成执行Statement,和将Statement对象返回的resultSet封装成List;**
5. StatementHandler 的parameterize(statement) 方法的实现:
/**
* StatementHandler 类的parameterize(statement) 方法实现
*/
public void parameterize(Statement statement) throws SQLException {
//使用ParameterHandler对象来完成对Statement的设值
parameterHandler.setParameters((PreparedStatement) statement);
}
/**
*
*ParameterHandler类的setParameters(PreparedStatement ps) 实现
* 对某一个Statement进行设置参数
*/
public void setParameters(PreparedStatement ps) throws SQLException {
ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
if (parameterMappings != null) {
for (int i = 0; i < parameterMappings.size(); i++) {
ParameterMapping parameterMapping = parameterMappings.get(i);
if (parameterMapping.getMode() != ParameterMode.OUT) {
Object value;
String propertyName = parameterMapping.getProperty();
if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params
value = boundSql.getAdditionalParameter(propertyName);
} else if (parameterObject == null) {
value = null;
} else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
value = parameterObject;
} else {
MetaObject metaObject = configuration.newMetaObject(parameterObject);
value = metaObject.getValue(propertyName);
}
// 每一个Mapping都有一个TypeHandler,根据TypeHandler来对preparedStatement进行设置参数
TypeHandler typeHandler = parameterMapping.getTypeHandler();
JdbcType jdbcType = parameterMapping.getJdbcType();
if (value == null && jdbcType == null) jdbcType = configuration.getJdbcTypeForNull();
// 设置参数
typeHandler.setParameter(ps, i + 1, value, jdbcType);
}
}
}
}
从上述的代码可以看到,StatementHandler 的parameterize(Statement) 方法调用了 ParameterHandler的setParameters(statement) 方法, ParameterHandler的setParameters(Statement)方法负责 根据我们输入的参数,对statement对象的 ? 占位符处进行赋值。
6. StatementHandler 的List query(Statement statement, ResultHandler resultHandler)方法的实现:
/**
* PreParedStatement类的query方法实现
*/
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
// 1.调用preparedStatemnt。execute()方法,然后将resultSet交给ResultSetHandler处理
PreparedStatement ps = (PreparedStatement) statement;
ps.execute();
//2. 使用ResultHandler来处理ResultSet
return resultSetHandler.<E> handleResultSets(ps);
}
/**
*ResultSetHandler类的handleResultSets()方法实现
*
*/
public List<Object> handleResultSets(Statement stmt) throws SQLException {
final List<Object> multipleResults = new ArrayList<Object>();
int resultSetCount = 0;
ResultSetWrapper rsw = getFirstResultSet(stmt);
List<ResultMap> resultMaps = mappedStatement.getResultMaps();
int resultMapCount = resultMaps.size();
validateResultMapsCount(rsw, resultMapCount);
while (rsw != null && resultMapCount > resultSetCount) {
ResultMap resultMap = resultMaps.get(resultSetCount);
//将resultSet
handleResultSet(rsw, resultMap, multipleResults, null);
rsw = getNextResultSet(stmt);
cleanUpAfterHandlingResultSet();
resultSetCount++;
}
String[] resultSets = mappedStatement.getResulSets();
if (resultSets != null) {
while (rsw != null && resultSetCount < resultSets.length) {
ResultMapping parentMapping = nextResultMaps.get(resultSets[resultSetCount]);
if (parentMapping != null) {
String nestedResultMapId = parentMapping.getNestedResultMapId();
ResultMap resultMap = configuration.getResultMap(nestedResultMapId);
handleResultSet(rsw, resultMap, null, parentMapping);
}
rsw = getNextResultSet(stmt);
cleanUpAfterHandlingResultSet();
resultSetCount++;
}
}
return collapseSingleResultList(multipleResults);
}
从上述代码我们可以看出,StatementHandler 的List
query(Statement statement, ResultHandler resultHandler)方法的实现,是调用了ResultSetHandler的handleResultSets(Statement) 方法。ResultSetHandler的handleResultSets(Statement) 方法会将Statement语句执行后生成的resultSet 结果集转换成List 结果集:
//
// DefaultResultSetHandler 类的handleResultSets(Statement stmt)实现
//HANDLE RESULT SETS
//
public List<Object> handleResultSets(Statement stmt) throws SQLException {
final List<Object> multipleResults = new ArrayList<Object>();
int resultSetCount = 0;
ResultSetWrapper rsw = getFirstResultSet(stmt);
List<ResultMap> resultMaps = mappedStatement.getResultMaps();
int resultMapCount = resultMaps.size();
validateResultMapsCount(rsw, resultMapCount);
while (rsw != null && resultMapCount > resultSetCount) {
ResultMap resultMap = resultMaps.get(resultSetCount);
//将resultSet
handleResultSet(rsw, resultMap, multipleResults, null);
rsw = getNextResultSet(stmt);
cleanUpAfterHandlingResultSet();
resultSetCount++;
}
String[] resultSets = mappedStatement.getResulSets();
if (resultSets != null) {
while (rsw != null && resultSetCount < resultSets.length) {
ResultMapping parentMapping = nextResultMaps.get(resultSets[resultSetCount]);
if (parentMapping != null) {
String nestedResultMapId = parentMapping.getNestedResultMapId();
ResultMap resultMap = configuration.getResultMap(nestedResultMapId);
handleResultSet(rsw, resultMap, null, parentMapping);
}
rsw = getNextResultSet(stmt);
cleanUpAfterHandlingResultSet();
resultSetCount++;
}
}
return collapseSingleResultList(multipleResults);
}
7. ResultSetHandler 映射结果集
是否有对Mybatis 把查询出的数据表中的字段值怎么和实体映射好奇和疑惑过?如果有,就好好看下面的代码,下面代码只是一个大致流程,详细自己下载代码慢慢看。
简单总结一下自己的理解:
- 执行器Executor 执行SQL的任务最后会交给JDBC的Statement去执行,在Mybatis中Executor执行器执行SQL的时候会创建StatementHandler,调用query()方法
- StatemntHandler 执行select,将结果给 ResultHandler 处理;
List query(Statement statement, ResultHandler resultHandler) - statementHandler 执行完将结果交由ResultHandler去处理
- ResultHandler核心方法handleRowValues() 去处理结果,处理结果的时候关键参数:ResultSetWrapper:查询数据库中得到的结果和字段,ResultMap:statement ID中定义的返回值
- 然后根据两个关键参数一行一行映射值,映射的基本过程,就是拿着数据库查询出的字段,过滤去掉“_”,处理驼峰命名去Map中查询是否存在,如果存在根据类型转换器映射值成功,如果不存在,映射值失败,实体中的值为null,
- 最后返回结果
�
//处理结果集
private void handleResultSet(ResultSetWrapper rsw, ResultMap resultMap, List<Object> multipleResults, ResultMapping parentMapping) throws SQLException {
try {
if (parentMapping != null) {
handleRowValues(rsw, resultMap, null, RowBounds.DEFAULT, parentMapping);
} else {
if (resultHandler == null) {
//如果没有resultHandler
//新建DefaultResultHandler
DefaultResultHandler defaultResultHandler = new DefaultResultHandler(objectFactory);
//调用自己的handleRowValues
handleRowValues(rsw, resultMap, defaultResultHandler, rowBounds, null);
//得到记录的list
multipleResults.add(defaultResultHandler.getResultList());
} else {
//如果有resultHandler
handleRowValues(rsw, resultMap, resultHandler, rowBounds, null);
}
}
} finally {
//最后别忘了关闭结果集,这个居然出bug了
// issue #228 (close resultsets)
closeResultSet(rsw.getResultSet());
}
}
// 内部调用
private void handleRowValues(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException {
if (resultMap.hasNestedResultMaps()) {
ensureNoRowBounds();
checkResultHandler();
handleRowValuesForNestedResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);
} else {
handleRowValuesForSimpleResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);
}
}
private void handleRowValuesForSimpleResultMap(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler resultHandler, RowBounds rowBounds, ResultMapping parentMapping)
throws SQLException {
DefaultResultContext resultContext = new DefaultResultContext();
skipRows(rsw.getResultSet(), rowBounds);
while (shouldProcessMoreRows(resultContext, rowBounds) && rsw.getResultSet().next()) {
ResultMap discriminatedResultMap = resolveDiscriminatedResultMap(rsw.getResultSet(), resultMap, null);
// 获取值
Object rowValue = getRowValue(rsw, discriminatedResultMap);
storeObject(resultHandler, resultContext, rowValue, parentMapping, rsw.getResultSet());
}
}
//核心,取得一行的值
private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap) throws SQLException {
//实例化ResultLoaderMap(延迟加载器)
final ResultLoaderMap lazyLoader = new ResultLoaderMap();
//调用自己的createResultObject,内部就是new一个对象(如果是简单类型,new完也把值赋进去)
Object resultObject = createResultObject(rsw, resultMap, lazyLoader, null);
if (resultObject != null && !typeHandlerRegistry.hasTypeHandler(resultMap.getType())) {
//一般不是简单类型不会有typehandler,这个if会进来
final MetaObject metaObject = configuration.newMetaObject(resultObject);
boolean foundValues = !resultMap.getConstructorResultMappings().isEmpty();
if (shouldApplyAutomaticMappings(resultMap, false)) {
//自动映射咯
//这里把每个列的值都赋到相应的字段里去了
foundValues = applyAutomaticMappings(rsw, resultMap, metaObject, null) || foundValues;
}
foundValues = applyPropertyMappings(rsw, resultMap, metaObject, lazyLoader, null) || foundValues;
foundValues = lazyLoader.size() > 0 || foundValues;
resultObject = foundValues ? resultObject : null;
return resultObject;
}
return resultObject;
}
//自动映射咯
private boolean applyAutomaticMappings(ResultSetWrapper rsw, ResultMap resultMap, MetaObject metaObject, String columnPrefix) throws SQLException {
final List<String> unmappedColumnNames = rsw.getUnmappedColumnNames(resultMap, columnPrefix);
boolean foundValues = false;
for (String columnName : unmappedColumnNames) {
String propertyName = columnName;
if (columnPrefix != null && !columnPrefix.isEmpty()) {
// When columnPrefix is specified,
// ignore columns without the prefix.
if (columnName.toUpperCase(Locale.ENGLISH).startsWith(columnPrefix)) {
propertyName = columnName.substring(columnPrefix.length());
} else {
continue;
}
}
final String property = metaObject.findProperty(propertyName, configuration.isMapUnderscoreToCamelCase());
if (property != null && metaObject.hasSetter(property)) {
final Class<?> propertyType = metaObject.getSetterType(property);
if (typeHandlerRegistry.hasTypeHandler(propertyType)) {
final TypeHandler<?> typeHandler = rsw.getTypeHandler(propertyType, columnName);
//巧妙的用TypeHandler取得结果
final Object value = typeHandler.getResult(rsw.getResultSet(), columnName);
// issue #377, call setter on nulls
if (value != null || configuration.isCallSettersOnNulls()) {
if (value != null || !propertyType.isPrimitive()) {
//然后巧妙的用反射来设置到对象
metaObject.setValue(property, value);
}
foundValues = true;
}
}
}
}
return foundValues;
}
private void handleRowValues(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException {
if (resultMap.hasNestedResultMaps()) {
ensureNoRowBounds();
checkResultHandler();
handleRowValuesForNestedResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);
} else {
handleRowValuesForSimpleResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);
}
}
//--------以下方法都是委派给ObjectWrapper------
//查找属性
public String findProperty(String propName, boolean useCamelCaseMapping) {
return objectWrapper.findProperty(propName, useCamelCaseMapping);
}
@Override
public String findProperty(String name, boolean useCamelCaseMapping) {
return metaClass.findProperty(name, useCamelCaseMapping);
}
public String findProperty(String name, boolean useCamelCaseMapping) {
if (useCamelCaseMapping) {
name = name.replace("_", "");
}
return findProperty(name);
}
六、Mybatis常见面试题
- 什么是Mybatis
- 讲一下Mybatis 的缓存
- Mybatis 是如何进行分页的?分页插件的原理是什么?
- 简述Mybatis的插件运行原理,如何编写一个插件
- Mybatis动态SQL是做什么的,都有哪些动态SQL,能简述一下动态SQL执行的原理不
{} 和 ${} 的区别是什么
- 为什么说Mybatis 是半自动ORM映射工具,它与全自动的却别在哪里
- Mybatis 是否支持延迟加载,如果支持,它的实现原理是什么?
- Mybatis 的好处是什么
- 简述Mybatis 的XML映射文件和Mybatis内部结构数据之间的映射关系
- 什么是Mybatis的接口绑定,有什么好处
- 接口绑定有几种实现方式,分别是怎么实现的
- 什么情况下用注解绑定,什么情况下用Xml绑定
- Mybatis 实现一对一有几种方式,具体是怎么操作的
- Mybatis 能执行一对一、一对多的关联查询吗?都有哪些实现方式,以及他们之间的区别?
- Mybatis 里面的动态SQL是怎么设定的,用什么语法
- Mybatis 是如何间Sql执行结果封装为目标对象并返回的?都有哪些映射方式?
- Xml 映射文件中,除了常见的select|insert|update|delete标签之外,还有哪些标签?
- 当实体类中的属性名和表中的字段名不一样,如何讲查询的结果封装到指定的pojo?
- 通常一个xml映射文件,都会写一个Dao 接口与之对应,Dao的工作原理?是否可以重载
- Mybatis 映射文件中,如果A标签通过includ 引用了B 标签的内容,请问,B标签能否定义在A标签的后面,还是说必须定义在A标签的前面?
- Mybatis 的Xml 映射文件中,不同的xml 映射文件,id 是否可以重复?
- Mybatis 中如何执行批处理
- Mybatis 都有哪些Executor执行器?他们之间的区别是什么?
- Mybatis 如何指定使用哪一种Executor 执行器
- resultMap 和 resultType 的区别
- 使用 Mybatis 的mapper 接口调用时有哪些要求