前情提要
在上一篇 MyBatis 源码学习 | Day 2 | MyBatis 初始化 中,我们探究了使用 MyBatis 操作数据库过程中 MyBatis 的第一阶段——初始化阶段,今天我们将探究数据的读写阶段。
MyBatis 操作数据库 Demo ↓
/**
* 使用 MyBatis 操作数据库
*
* @author nx-xn2002
* @date 2024-08-02
*/
public class QueryWithMyBatis {
public static void main(String[] args) throws IOException {
//第一阶段:MyBatis初始化
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
//第二阶段:数据读写阶段
SqlSession sqlSession = sqlSessionFactory.openSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
List<User> users = mapper.listAll();
users.forEach(System.out::println);
User user = mapper.selectUserById(1L);
System.out.println(user);
sqlSession.close();
}
}
数据读写阶段
在上一阶段中,通过 Resources
类读取解析配置文件,SqlSessionFactoryBuilder
类使用配置文件,我们获得了能够对数据库连接和相关操作进行管理的 SqlSessionFactory
工厂类对象。在数据读写阶段,我们需要一个 SqlSession
对象来执行命令、获取 mapper
映射、管理事务,在源码中原话就是这样说的:
/**
* The primary Java interface for working with MyBatis.
* Through this interface you can execute commands, get mappers and manage transactions.
*
* @author Clinton Begin
*/
public interface SqlSession extends Closeable {
// ...
}
在我们的程序里,是调用了 SqlSessionFactory
对象的 openSession
方法来获取 SqlSession
对象
SqlSession sqlSession = sqlSessionFactory.openSession();
而在上一篇最后,我们提到 SqlSessionFactory
实际上是一个接口,此处使用的是它的一个默认的实现类 DefaultSqlSessionFactory
的对象,所以我们进入到这个实现类的 openSession
方法,看一下具体是如何创建 SqlSession
对象的。
方法源码如下:
@Override
public SqlSession openSession() {
return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
}
可以看到,openSession
方法实际上是调用了 openSessionFromDataSource
方法,我们再进入到这个核心方法中:
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;
DefaultSqlSession var8;
try {
Environment environment = this.configuration.getEnvironment();
TransactionFactory transactionFactory = this.getTransactionFactoryFromEnvironment(environment);
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
Executor executor = this.configuration.newExecutor(tx, execType);
var8 = new DefaultSqlSession(this.configuration, executor, autoCommit);
} catch (Exception var12) {
this.closeTransaction(tx);
throw ExceptionFactory.wrapException("Error opening session. Cause: " + var12, var12);
} finally {
ErrorContext.instance().reset();
}
return var8;
}
可以看到,在这里面,之前读取的配置信息被用来创建事务工厂 TransactionFactory
、执行器 Executor
和 DefaultSqlSession
对象,而其中的 DefaultSqlSession
提供了一系列的增删改查、提交、回滚的方法。在读写阶段中,我们只需要创建一次 SqlSession
对象就可以供我们进行多次的数据库操作复用。
继续往下看,UserMapper mapper = sqlSession.getMapper(UserMapper.class)
一句通过刚刚的 DefaultSqlSession
类对象的 getMapper
方法,获取到了一个 UserMapper
接口的实现类对象,我们进入源码查看细节
@Override
public <T> T getMapper(Class<T> type) {
return configuration.getMapper(type, this);
}
可以看到,这里是调用了 Configuration
类对象的 getMapper
方法,在一路向下走,会注意到实际上最后调用的是 MapperRegistry
类的同名方法
@SuppressWarnings("unchecked")
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) 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);
}
}
此处我们传入的是想要构造的 UserMapper.class
和刚刚的 DefaultSqlSession
对象,在这里面,final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type)
中 knownMappers
是一个 HashMap
对象,可以定位到,在 MyBatis 初始化阶段中解析配置文件时调用了相关的 addMapper
方法来将对应的 <Class<?>, MapperProxyFactory<?>>
键值对放入到这个哈希表中,此时如果传入的 type
是之前正确解析过的,就能够正常拿到对应的 MapperProxyFactory
对象
我们再来看到最后返回值部分的 return mapperProxyFactory.newInstance(sqlSession)
里的 newInstance
方法,源代码如下:
@SuppressWarnings("unchecked")
protected T newInstance(MapperProxy<T> mapperProxy) {
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}
public T newInstance(SqlSession sqlSession) {
final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}
显然,我们可以知道,最后是返回了一个基于反射的动态代理对象 MapperProxy
类对象,于是我们可以直接去到 MapperProxy
类的 invoke
方法中去查看相关实现,这是一个动态代理方法,用于拦截并调用接口的实现方法,看到源代码如下
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
} else if (method.isDefault()) {
return invokeDefaultMethod(proxy, method, args);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
final MapperMethod mapperMethod = cachedMapperMethod(method);
return mapperMethod.execute(sqlSession, args);
}
可以看到,如果调用的只是 Object
类的方法,或者使用 default
修饰的方法,就会直接去运行。而除此之外,则会执行以下两句代码
final MapperMethod mapperMethod = cachedMapperMethod(method);
return mapperMethod.execute(sqlSession, args);
第一句执行的是尝试从缓存中获取一个 MapperMethod
对象,观察一下 cachedMapperMethod
方法
private MapperMethod cachedMapperMethod(Method method) {
return methodCache.computeIfAbsent(method, k -> new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
}
可以看到,这里是尝试从一个 Map
对象中去取出一个 MapperMethod
对象,如果对象不存在,就创建并返回。这里有点类似本地缓存里的 LoadingCache
,我觉得根据实际需求,其实可以考虑使用 Caffine
的缓存实现来提升性能。
在获取到 MapperMethod
对象后,将会执行它的 execute
方法并返回结果,可以看到具体的方法实现如下:
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
switch (command.getType()) {
case INSERT: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.insert(command.getName(), param));
break;
}
case UPDATE: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.update(command.getName(), param));
break;
}
case DELETE: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.delete(command.getName(), param));
break;
}
case SELECT:
if (method.returnsVoid() && method.hasResultHandler()) {
executeWithResultHandler(sqlSession, args);
result = null;
} else if (method.returnsMany()) {
result = executeForMany(sqlSession, args);
} else if (method.returnsMap()) {
result = executeForMap(sqlSession, args);
} else if (method.returnsCursor()) {
result = executeForCursor(sqlSession, args);
} else {
Object param = method.convertArgsToSqlCommandParam(args);
result = sqlSession.selectOne(command.getName(), param);
if (method.returnsOptional()
&& (result == null || !method.getReturnType().equals(result.getClass()))) {
result = Optional.ofNullable(result);
}
}
break;
case FLUSH:
result = sqlSession.flushStatements();
break;
default:
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;
}
在我们的程序里,具体执行的其实就是 SELECT
下的 result = executeForMany(sqlSession, args);
这一句,过程较为复杂,我们直接看到两个核心方法,下面是 CachingExecutor
类的两个方法,执行查询时,最后是可以定位到这两个方法处
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds,
ResultHandler resultHandler) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameterObject);
CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds,
ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
Cache cache = ms.getCache();
if (cache != null) {
flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, boundSql);
@SuppressWarnings("unchecked")
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
tcm.putObject(cache, key, list); // issue #578 and #116
}
return list;
}
}
return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
在 getBoundSql
方法中,会层层转化去掉 if
、where
等标签,获取到 SQL 语句,然后 createCacheKey
为本次查询操作技术缓存键值,可以看到,在最后的实现里,如果命中缓存,就可以直接从缓存里获取结果,否则,就通过 delegate
对象调用 query
方法。通过分析,我们可以知道,这里是调用了 BaseExecutor
类对象的同名方法
@SuppressWarnings("unchecked")
@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.");
}
if (queryStack == 0 && ms.isFlushCacheRequired()) {
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();
}
// issue #601
deferredLoads.clear();
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// issue #482
clearLocalCache();
}
}
return list;
}
此处表明 MyBatis 开始使用数据库展开查询操作,最终会定位到一个 PreparedStatement
对象执行 execute
方法进行查询,然后最后循环遍历结果对象的每一个属性为各个属性进行赋值操作,至此,数据读写完成。
总结
在这一阶段里,MyBatis 的工作流程大致如下:
- 建立数据库连接,获取
SqlSession
对象 - 获取当前映射接口对应的数据库操作节点,并生成接口实现类
- 接口实现类拦截对接口中方法的调用,完成其中数据操作方法的调用实现
- 将数据库操作节点中的语句进行处理,转换为标准的 SQL 语句
- 尝试从缓存中获得结果,如果找不到就继续从数据库中查询
- 从数据库查询结果
- 处理结果集
- 建立输出对象
- 对输出对象的属性进行赋值
- 在缓存中记录结果
- 返回查询结果