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

前情提要

在上一篇 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、执行器 ExecutorDefaultSqlSession 对象,而其中的 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 方法中,会层层转化去掉 ifwhere 等标签,获取到 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 的工作流程大致如下:

  1. 建立数据库连接,获取 SqlSession 对象
  2. 获取当前映射接口对应的数据库操作节点,并生成接口实现类
  3. 接口实现类拦截对接口中方法的调用,完成其中数据操作方法的调用实现
  4. 将数据库操作节点中的语句进行处理,转换为标准的 SQL 语句
  5. 尝试从缓存中获得结果,如果找不到就继续从数据库中查询
  6. 从数据库查询结果
  7. 处理结果集
    • 建立输出对象
    • 对输出对象的属性进行赋值
  8. 在缓存中记录结果
  9. 返回查询结果
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值