emmm时隔一个多月没写博客了,我终于还是没忍住对mybatis这个框架下手了哈哈哈哈。搞懂源码就是爽啊,本文大致脉络基于下图分析
mybatis是一款持久性的ORM框架,目的在于把数据库中的表中的信息转换成对象供我们操作,也就是说我们对数据库的操作有了mybatis可以转变为对对象的操作。
要记到一点市面上所有的ORM框架无论如何都离不开JDBC操作,我们所谓的mybatis也好hibernate也罢其实本质都是对JDBC的包装而已。
先来回顾一下传统的JDBC操作步骤
/**
* 使用JDBC连接并操作mysql数据库
*/
public static void main(String[] args) {
// 数据库驱动类名的字符串
String driver = "com.mysql.cj.jdbc.Driver";
// 数据库连接串
String url = "url";
// 用户名
String username = "username";
// 密码
String password = "password";
Connection conn = null;
Statement stmt = null;
ResultSet rs = null;
try {
// 1、加载数据库驱动( 成功加载后,会将Driver类的实例注册到DriverManager类中)
Class.forName(driver);
// 2、获取数据库连接
conn = DriverManager.getConnection(url, username, password);
// 3、获取数据库操作对象
stmt = conn.createStatement();
// 4、定义操作的SQL语句
String sql = "select * from user where id = 1";
// 5、执行数据库操作
rs = stmt.executeQuery(sql);
// 6、获取并操作结果集
while (rs.next()) {
System.out.println(rs.getInt("id"));
System.out.println(rs.getString("name"));
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// 7、关闭对象,回收数据库资源
if (rs != null) {
//关闭结果集对象
try {
rs.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (stmt != null) {
// 关闭数据库操作对象
try {
stmt.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (conn != null) {
// 关闭数据库连接对象
try {
if (!conn.isClosed()) {
conn.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
如果从mybatisPlus项目分析mybatis的源码写如下demo即可直接debug了,毕竟springboot+mybatisPlus项目给我简化了很多配置,很多细节可能分析不到了,个人建议把mybatis源码克隆到本地,然后进行后续的源码研究。
@Autowired
UserMapper userMapper;
@Test
public void test5() {
User user = userMapper.queryManyparam("zzh", 1);
User user2 = userMapper.queryManyparam("zzh", 1);
//俩个不同的session,导致一级缓存失效
System.out.println(user == user2);
}
原生mybatis操作数据库
private Configuration configuration;
private JdbcTransaction jdbcTransaction;
private Connection connection;
private Reader resourceAsReader;
private SqlSessionFactory sqlSessionFactory;
public void init() throws SQLException, IOException {
connection = DriverManager.getConnection("url", "username", "password");
resourceAsReader = Resources.getResourceAsReader("mybatis.xml");
jdbcTransaction = new JdbcTransaction(connection);
sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsReader);
configuration = sqlSessionFactory.getConfiguration();
}
/**
* 相同的sql只会编译处理一次
*/
@Test
public void b() throws IOException, SQLException {
init();
SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.SIMPLE);
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
System.out.println(mapper.query(1));
System.out.println(mapper.query(1));
}
更加原生的mybatis操作数据库
@Test
public void a() throws SQLException, IOException {
init();
ReuseExecutor reuseExecutor = new ReuseExecutor(this.configuration, jdbcTransaction);
MappedStatement mappedStatement = this.configuration.getMappedStatement("zzhTest.mybatis.mapper.UserMapper.queryManyparam");
HashMap<String, Object> hashMap = new HashMap<>();
hashMap.put("id",1);
hashMap.put("name","zzh");
List<Object> objects1 = reuseExecutor.doQuery(mappedStatement, hashMap, RowBounds.DEFAULT, SimpleExecutor.NO_RESULT_HANDLER, mappedStatement.getBoundSql(1));
List<Object> objects2 = reuseExecutor.doQuery(mappedStatement, hashMap, RowBounds.DEFAULT, SimpleExecutor.NO_RESULT_HANDLER, mappedStatement.getBoundSql(1));
System.out.println(objects1.get(0));
System.out.println(objects1.get(0) == objects2.get(0));
}
预处理次数的体现:控制台 Preparing: select * from user where id = ? 出现的次数即为预处理的次数
废话不多说我这里直接用原生的mybatis debug源码了(其实本质都是一样的咯),如下图片中的代码
我们从sqlSession中获取到的mapper对象本质是一个代理对象,debug进去newInstance()看是如何创建我们的mapper代理对象的?
把defultSqlSession、目标对象实现的接口类、methodCache包装成一个InvocationHandler对象,继而利用jdk代理生成对应的代理对象
小结:通过sqlSession.getMapper(UserMapper.class)获取到的是一个通过jdk代理生成的代理对象
mapper中的查询语句操作如下
直接debug mapper.query(1)来到这里。根据对数据库不同的操作有相应的分支进行处理,由于我们是查询操作来到select分支。
例如拿如下代码分析就是维护@Param(“id”) 中的id 与 1 的关系,此时并没有解析sql语句中的id哦
@Select("select * from user where id = #{id}")
User query(@Param("id") Integer Uid);
query(1);
2. 根据这个param+查询方法的全路径限定名,进行数据库的查询操作(result=sqlSession.selectOne(command.getName(), param);)。怎么查询?下文有讲哦
public Object getNamedParams(Object[] args) {
final int paramCount = names.size();
if (args == null || paramCount == 0) {
return null;
} else if (!hasParamAnnotation && paramCount == 1) {
Object value = args[names.firstKey()];
return wrapToMapIfCollection(value, useActualParamName ? names.get(0) : null);
} else {
final Map<String, Object> param = new ParamMap<>();
int i = 0;
/**
* 遍历所有的@params()中的参数name
*/
for (Map.Entry<Integer, String> entry : names.entrySet()) {
/**
* args:方法中传入的参数value数组
* entry.getValue():@params()中的参数name
*/
param.put(entry.getValue(), args[entry.getKey()]);
/**
* add generic param names (param1, param2, ...)
*/
final String genericParamName = GENERIC_NAME_PREFIX + (i + 1);
if (!names.containsValue(genericParamName)) {
param.put(genericParamName, args[entry.getKey()]);
}
i++;
}
/**
* 建立paramName与paramValue的关系
* void query(@param(id)int id,@param(name)String name)
* query(1,"zzh")
* 例如:id-1、name-zzh
*/
return param;
}
}
看源码可能有点绕,不过我注释也写了很全哦,不懂没关系再来梳理一遍参数解析过程。mybatis中有如下俩个map
由于下标都是一一对应的,通过args[names.getKey()]就可以获取到对应参数name的value值了。通过这样一个个维护一下关系,最终的param就形成了以参数name为key、以参数value为value的关系的一个map
我们debug进去会来到这里,先获取对应的MappedStatement,然后走cacheExecutor执行器中的操作
mybatis使用的是一种装饰器的模式,在真正的查询数据库之前会依次从二级缓存(SynchronizedCache)、一级缓存(localCache)中获取数据,如果还获取不到才会走查询数据库的逻辑
点进上图的executor.query()会来到下图的cacheExecutor中的query方法,在此之前cacheExecutor还干了俩件事
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
/**
* 获取二级缓存:SynchronizedCache
*/
Cache cache = ms.getCache();
if (cache != null) {
/**
* 尝试清空二级缓存(标记 clearOnCommit = true)
*/
flushCacheIfRequired(ms);
/**
* 可以设置 @Options(useCache = false) 关闭查询二级缓存
* resultHandler = Executor.NO_RESULT_HANDLER = null
*/
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, boundSql);
@SuppressWarnings("unchecked")
/**
* 从TransactionalCacheManager中获取二级缓存中的数据
*/
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
/**
* 二级缓存中获取不到数据,进行查询数据库操作
* delegate:SimpleExecutor
*/
list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
/**
* 把数据填充进暂存区中的entriesToAddOnCommit的这个map里面
* 暂存区:TransactionalCache
*/
tcm.putObject(cache, key, list); // issue #578 and #116
}
return list;
}
}
return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
从上面这段代码可以知道我们平常使用如下配置时的生效时机以及在mybatis中的源码体现对应在哪
在mapper类上加@CacheNamespace或者在mapper对应的xml文件中加 < cache> cache>
@Select("select * from user where id = #{id}")
@Options(useCache = false)
User query(@Param("id") Integer Uid);
@Options(flushCache = Options.FlushCachePolicy.TRUE)
@Select("select * from user where id = #{id}")
User query(@Param("id") Integer Uid);
配置在源码中的体现
mybatis清空二级缓存的流程时设置 clearOnCommit = true 并且清空entriesToAddOnCommit 这个map
private final Map<Object, Object> entriesToAddOnCommit;
private void flushCacheIfRequired(MappedStatement ms) {
/**
* 获取二级缓存
*/
Cache cache = ms.getCache();
/**
* 如果开启对应的方法上面开启了 FlushCachePolicy = true
* 那么此处会清除二级缓存(本质是标记 clearOnCommit = true)
*/
if (cache != null && ms.isFlushCacheRequired()) {
tcm.clear(cache);
}
}
@Override
public void clear() {
clearOnCommit = true;
entriesToAddOnCommit.clear();
}
接着分析cacheExecutor.query()中的tcm.getObject()这行代码可以知道怎么从二级缓存中获取数据的哦。
public Object getObject(Object key) {
/**
* delegate:Cache(以SynchronizedCache为头的Cache链条)
* key:statementId~sql~参数~id组成
*/
Object object = delegate.getObject(key);
/**
*
* 防止缓存穿透:二级缓存中没有数据,将key放入entriesMissedInCache(HashSet)中
*
*/
if (object == null) {
entriesMissedInCache.add(key);
}
// issue #146
/**
* 如果此时有人清空二级缓存那么clearOnCommit = true
* 此时的效果也相当于从二级缓存中获取不到数据
*/
if (clearOnCommit) {
return null;
} else {
return object;
}
}
如果是我们第一次debug到这肯定二级缓存中没有数据撒,还记得我们分析到哪了咩,分析到下图这。本质如下箭头标注的query()是调用同一个query()方法,如果没有获取到缓存将会执行箭头标注的方法(simpleExecutor.query(…))。
此query()大体思路分析如下:
@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());
if (closed) {
throw new ExecutorException("Executor was closed.");
}
/**
* 如果设置了flushCacheRequired属性将会清空缓存
* 例如:@Options(flushCache = Options.FlushCachePolicy.TRUE)
*/
if (queryStack == 0 && ms.isFlushCacheRequired()) {
/**
* 清空一级缓存中的数据
* 一级缓存:localCache
*/
clearLocalCache();
}
List<E> list;
try {
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--;
}
if (queryStack == 0) {
for (DeferredLoad deferredLoad : deferredLoads) {
deferredLoad.load();
}
/**
* 清空延时加载map中的数据
*/
deferredLoads.clear();
/**
* 设置了一级缓存的域范围为STATEMENT将会在每次查询完毕后清空一级缓存
*/
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
clearLocalCache();
}
}
return list;
}
思路分析完了来扣细节~~~~~~~~~~~~
清空了localOutputParameterCache、localCache(一级缓存)俩个map
localCache.getObject(key),直接根据key来获取
下面通过分析queryFromDatabase()方法一趟究竟
点开queryFromDatabase()方法代码如下,注释写的很详细着重看看doQuery()看如何进行查询的
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)和mybatis循环依赖有关
*/
localCache.putObject(key, EXECUTION_PLACEHOLDER);
try {
/**
* 执行BaseExecutor的实现类中的doQuery方法
*/
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
localCache.removeObject(key);
}
/**
* 把查询到的数据放入一级缓存
*/
localCache.putObject(key, list);
/**
* TODO 这个map作用暂时不清楚
* 如果当前的mappedStatement的类型为CALLABLE,那么会往localOutputParameterCache中put一个数值
*/
if (ms.getStatementType() == StatementType.CALLABLE) {
localOutputParameterCache.putObject(key, parameter);
}
return list;
}
mybatis查询数据库必然绕不开jdbc,点进doQuery()来到如下代码。查询和普通的jdbc操作一样分为俩步
@Override
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();
/**
* 创建合适的statementHandler,主要研究prepareStatementHandler
*/
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
/**
* 利用statementHandler创建对应的statement对象
*/
stmt = prepareStatement(handler, ms.getStatementLog());
return handler.query(stmt, resultHandler);
} finally {
/**
* 关闭statement
*/
closeStatement(stmt);
}
}
准备一个statement代码分为如下三步:mybatis中的prepareStatement()和我们熟悉的jdbc操作差不多,亦是如此就是多了几层包装而已。
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
Statement stmt;
/**
* 获取连接
*/
Connection connection = getConnection(statementLog);
/**
* 实例化prepareStatement
*/
stmt = handler.prepare(connection, transaction.getTimeout());
/**
* 为prepareStatement 填充参数,涉及到sql参数解析
*/
handler.parameterize(stmt);
return stmt;
}
准备statement第二步:源码就是封装了jdbc而已,就不贴代码贴几张图片了(本质都是connection.prepareStatement()),以下图片是的代码
准备statement第三步:填充statement中的参数,将我们的查询条件塞入sql语句中,即 select * from user where id = #{id} 转换成 select * from user where id = 1 的过程
public void setParameters(PreparedStatement ps) {
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;
/**
* select * from user where id = #{id}
* 获取#{id}中的name,例如:这种情况下面获取到的propertyName = id
*/
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对象
* parameterObject:即使我们sql中只有一个参数,mybatis也会默认额外生成param1这种
*/
MetaObject metaObject = configuration.newMetaObject(parameterObject);
/**
* 获取参数的具体值
*/
value = metaObject.getValue(propertyName);
}
TypeHandler typeHandler = parameterMapping.getTypeHandler();
JdbcType jdbcType = parameterMapping.getJdbcType();
if (value == null && jdbcType == null) {
jdbcType = configuration.getJdbcTypeForNull();
}
try {
/**
* 本质就是调用prepareStatement.setInt(0,value)
*/
typeHandler.setParameter(ps, i + 1, value, jdbcType);
} catch (TypeException | SQLException e) {
throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
}
}
}
}
}
上面的代码可能有点绕,现在重新理一遍思路。确定一个ps.setInt(i, value)操作分为三步:
第一步确定流程:由于我们一开始封装的boundSql对象包括了#{id} 的相关参数名称信息,且我们一开始就维护了一个map(params中维护了key:id、value :1的关系),我们通过解析#{ id } ,然后以id为key,从params中获取对应的value。
第二步确定流程:由于里面是一个for循环,例如select * from user where id = #{1} and name = “zzh”。第一次填充#{ id } ,i = 0,遍历到填充 #{ name }时,对应的 i = 1。
第三步确定流程:直接从parameterMapping中可以获取到参数类型
到此一个statement就成功创建好了接下来就是开始执行了execute操作了
来到handleResultSets()方法,观察到无论走哪个分支都会调用其中的handleResultSet()方法
来到handleResultSet()方法,观察无论如何都会调用handleRowValues()方法
来到handleRowValues()方法,这里如果结果集只是简单的属性映射将会走简单结果集处理逻辑,如果存在嵌套结果集那么会走嵌套结果集的逻辑
针对结果一行行进行处理,本质创建一个空的行接收对象封装成metaObject对象,然后基于metaObject对象进行属性填充。
获取对应列的值(rs.getInt(“id”)),然后把值填充到metaObject对应的property上面。
private boolean applyPropertyMappings(ResultSetWrapper rsw, ResultMap resultMap, MetaObject metaObject, ResultLoaderMap lazyLoader, String columnPrefix)
throws SQLException {
final List<String> mappedColumnNames = rsw.getMappedColumnNames(resultMap, columnPrefix);
boolean foundValues = false;
final List<ResultMapping> propertyMappings = resultMap.getPropertyResultMappings();
/**
* 遍历所有的属性字段进行属性填充
*/
for (ResultMapping propertyMapping : propertyMappings) {
String column = prependPrefix(propertyMapping.getColumn(), columnPrefix);
if (propertyMapping.getNestedResultMapId() != null) {
// the user added a column attribute to a nested result map, ignore it
column = null;
}
if (propertyMapping.isCompositeResult()
|| (column != null && mappedColumnNames.contains(column.toUpperCase(Locale.ENGLISH)))
|| propertyMapping.getResultSet() != null) {
/**
* 获取要填充的某个属性字段的value值
*/
Object value = getPropertyMappingValue(rsw.getResultSet(), metaObject, propertyMapping, lazyLoader, columnPrefix);
// issue #541 make property optional
final String property = propertyMapping.getProperty();
if (property == null) {
continue;
}
else if (value == DEFERRED) {
foundValues = true;
continue;
}
if (value != null) {
foundValues = true;
}
if (value != null || (configuration.isCallSettersOnNulls() && !metaObject.getSetterType(property).isPrimitive())) {
// gcode issue #377, call setter on nulls (value is not 'found')
metaObject.setValue(property, value);
}
}
}
return foundValues;
}
按照属性类型的不同分别对应三种不同的获取方式
private Object getPropertyMappingValue(ResultSet rs, MetaObject metaResultObject, ResultMapping propertyMapping, ResultLoaderMap lazyLoader, String columnPrefix)
throws SQLException {
/**
* 存在子查询,先走嵌套子查询逻辑,进行相应的子查询操作,查到相应的子查询中的数值然后返回
*/
if (propertyMapping.getNestedQueryId() != null) {
return getNestedQueryMappingValue(rs, metaResultObject, propertyMapping, lazyLoader, columnPrefix);
}
/**
* 存在ResultSet类型的结果映射,进行相应的属性填充操作
*/
else if (propertyMapping.getResultSet() != null) {
addPendingChildRelation(rs, metaResultObject, propertyMapping); // TODO is that OK?
return DEFERRED;
}
/**
* 简单单属性填充
*/
else {
final TypeHandler<?> typeHandler = propertyMapping.getTypeHandler();
final String column = prependPrefix(propertyMapping.getColumn(), columnPrefix);
return typeHandler.getResult(rs, column);
}
}
int result = rs.getInt(columnName);
先执行子查询中的逻辑,直至子查询中的简单属性填充完毕,然后返回子查询的结果
private Object getNestedQueryMappingValue(ResultSet rs, MetaObject metaResultObject, ResultMapping propertyMapping, ResultLoaderMap lazyLoader, String columnPrefix)
throws SQLException {
/**
* 子查询语句的全路径限定名
*/
final String nestedQueryId = propertyMapping.getNestedQueryId();
/**
* 需要填充属性字段的name
*/
final String property = propertyMapping.getProperty();
/**
* 子查询所有信息包装成的一个MappedStatement对象
*/
final MappedStatement nestedQuery = configuration.getMappedStatement(nestedQueryId);
/**
* 子查询的参数信息
*/
final Class<?> nestedQueryParameterType = nestedQuery.getParameterMap().getType();
/**
* 传入子查询的参数value
*/
final Object nestedQueryParameterObject = prepareParameterForNestedQuery(rs, propertyMapping, nestedQueryParameterType, columnPrefix);
Object value = null;
if (nestedQueryParameterObject != null) {
final BoundSql nestedBoundSql = nestedQuery.getBoundSql(nestedQueryParameterObject);
final CacheKey key = executor.createCacheKey(nestedQuery, nestedQueryParameterObject, RowBounds.DEFAULT, nestedBoundSql);
final Class<?> targetType = propertyMapping.getJavaType();
/**
* 如果一级缓存中有数值,且缓存值是一个占位符号,与循环依赖有关
*/
if (executor.isCached(nestedQuery, key)) {
/**
* 把此数据添加到延时加载,为的是解决循环依赖问题
*/
executor.deferLoad(nestedQuery, metaResultObject, property, key, targetType);
value = DEFERRED;
} else {
final ResultLoader resultLoader = new ResultLoader(configuration, executor, nestedQuery, nestedQueryParameterObject, targetType, key, nestedBoundSql);
/**
* 如果是懒加载执行相应逻辑
*/
if (propertyMapping.isLazy()) {
lazyLoader.addLoader(property, metaResultObject, resultLoader);
value = DEFERRED;
} else {
/**
* 其他情况正常加载,进行查询数据库操作
*/
value = resultLoader.loadResult();
}
}
}
return value;
}
resultLoader.loadResult()会重复这段代码的执行
最终我们不论是简单属性还是子查询属性,都能获取到了,最后也就完成了我们的属性填充了
最终我们获取到了对应的值,然后就可以把值填充进metaObject中了。最终我们的这个metaObject会被包装然后返回。我们最终得到的数据也就是这个MetaObject的包装对象了。