MyBatis 源码学习 | Day 3 | 数据读写阶段

前情提要

在上一篇 MyBatis 源码学习 | Day 2 | MyBatis 初始化 中,我们探究了使用 MyBatis 操作数据库过程中 MyBatis 的第一阶段------初始化阶段,今天我们将探究数据的读写阶段。

MyBatis 操作数据库 Demo ↓

java 复制代码
/**
 * 使用 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 映射、管理事务,在源码中原话就是这样说的:

java 复制代码
/**
 * 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 对象

java 复制代码
SqlSession sqlSession = sqlSessionFactory.openSession();

而在上一篇最后,我们提到 SqlSessionFactory 实际上是一个接口,此处使用的是它的一个默认的实现类 DefaultSqlSessionFactory 的对象,所以我们进入到这个实现类的 openSession 方法,看一下具体是如何创建 SqlSession 对象的。

方法源码如下:

java 复制代码
@Override
public SqlSession openSession() {
	return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
}

可以看到,openSession 方法实际上是调用了 openSessionFromDataSource 方法,我们再进入到这个核心方法中:

java 复制代码
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、执行器 ExecutorDefaultSqlSession 对象,而其中的 DefaultSqlSession 提供了一系列的增删改查、提交、回滚的方法。在读写阶段中,我们只需要创建一次 SqlSession 对象就可以供我们进行多次的数据库操作复用。

继续往下看,UserMapper mapper = sqlSession.getMapper(UserMapper.class) 一句通过刚刚的 DefaultSqlSession 类对象的 getMapper 方法,获取到了一个 UserMapper 接口的实现类对象,我们进入源码查看细节

java 复制代码
@Override
public <T> T getMapper(Class<T> type) {
	return configuration.getMapper(type, this);
}

可以看到,这里是调用了 Configuration 类对象的 getMapper 方法,在一路向下走,会注意到实际上最后调用的是 MapperRegistry 类的同名方法

java 复制代码
@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 方法,源代码如下:

java 复制代码
@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 方法中去查看相关实现,这是一个动态代理方法,用于拦截并调用接口的实现方法,看到源代码如下

java 复制代码
@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 修饰的方法,就会直接去运行。而除此之外,则会执行以下两句代码

java 复制代码
final MapperMethod mapperMethod = cachedMapperMethod(method);
return mapperMethod.execute(sqlSession, args);

第一句执行的是尝试从缓存中获取一个 MapperMethod 对象,观察一下 cachedMapperMethod 方法

java 复制代码
private MapperMethod cachedMapperMethod(Method method) {
    return methodCache.computeIfAbsent(method, k -> new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
}

可以看到,这里是尝试从一个 Map 对象中去取出一个 MapperMethod 对象,如果对象不存在,就创建并返回。这里有点类似本地缓存里的 LoadingCache,我觉得根据实际需求,其实可以考虑使用 Caffine 的缓存实现来提升性能。

在获取到 MapperMethod 对象后,将会执行它的 execute 方法并返回结果,可以看到具体的方法实现如下:

java 复制代码
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 类的两个方法,执行查询时,最后是可以定位到这两个方法处

java 复制代码
@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 方法中,会层层转化去掉 ifwhere 等标签,获取到 SQL 语句,然后 createCacheKey 为本次查询操作技术缓存键值,可以看到,在最后的实现里,如果命中缓存,就可以直接从缓存里获取结果,否则,就通过 delegate 对象调用 query 方法。通过分析,我们可以知道,这里是调用了 BaseExecutor 类对象的同名方法

java 复制代码
@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 的工作流程大致如下:

  1. 建立数据库连接,获取 SqlSession 对象
  2. 获取当前映射接口对应的数据库操作节点,并生成接口实现类
  3. 接口实现类拦截对接口中方法的调用,完成其中数据操作方法的调用实现
  4. 将数据库操作节点中的语句进行处理,转换为标准的 SQL 语句
  5. 尝试从缓存中获得结果,如果找不到就继续从数据库中查询
  6. 从数据库查询结果
  7. 处理结果集
    • 建立输出对象
    • 对输出对象的属性进行赋值
  8. 在缓存中记录结果
  9. 返回查询结果
相关推荐
西岸行者5 天前
学习笔记:SKILLS 能帮助更好的vibe coding
笔记·学习
悠哉悠哉愿意5 天前
【单片机学习笔记】串口、超声波、NE555的同时使用
笔记·单片机·学习
别催小唐敲代码5 天前
嵌入式学习路线
学习
毛小茛5 天前
计算机系统概论——校验码
学习
babe小鑫5 天前
大专经济信息管理专业学习数据分析的必要性
学习·数据挖掘·数据分析
winfreedoms5 天前
ROS2知识大白话
笔记·学习·ros2
在这habit之下5 天前
Linux Virtual Server(LVS)学习总结
linux·学习·lvs
我想我不够好。5 天前
2026.2.25监控学习
学习
im_AMBER5 天前
Leetcode 127 删除有序数组中的重复项 | 删除有序数组中的重复项 II
数据结构·学习·算法·leetcode
CodeJourney_J5 天前
从“Hello World“ 开始 C++
c语言·c++·学习