首页 > 编程语言 > 浅谈MyBatis 如何执行一条 SQL语句
2021
07-01

浅谈MyBatis 如何执行一条 SQL语句

前言

Mybatis 是 Java 开发中比较常用的 ORM 框架。在日常工作中,我们都是直接通过 Spring Boot 自动配置,并直接使用,但是却不知道 Mybatis 是如何执行一条 SQL 语句的,而这篇文章就是来揭开 Mybatis 的神秘面纱。

基础组件

我们要理解 Mybatis 的执行过程,就必须先了解 Mybatis 中都有哪一些重要的类,这些类的职责都是什么?

SqlSession

我们都很熟悉,它对外提供用户和数据库之间交互需要使用的方法,隐藏了底层的细节。它默认是实现类是 DefaultSqlSession

Executor

这个是执行器,SqlSession 中对数据库的操作都是委托给它。它有多个实现类,可以使用不同的功能。

Configuration

它是一个很重要的配置类,它包含了 Mybatis 的所有有用信息,包括 xml 配置,动态 sql 语句等等,我们到处都可以看到这个类的身影。

MapperProxy

这是一个很重要的代理类,它代理的就是 Mybatis 中映射 SQL 的接口。也就是我们常写的 Dao 接口。

工作流程

初步使用

首先,我们需要得到一个 SqlSessionFactory 对象,该对象的作用是可以获取 SqlSession  对象。

// 读取配置
InputStream resourceAsStream = Resources.getResourceAsStream("config.xml");
SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
// 创建一个 SqlSessionFactory 对象
SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(resourceAsStream);

当我们得到一个 SqlSessionFactory 对象之后,就可以通过它的 openSession 方法得到一个 SqlSession 对象。

 SqlSession sqlSession = sqlSessionFactory.openSession(true);

最后,我们通过 SqlSession 对象获取 Mapper ,从而可以从数据库获取数据。

// 获取 Mapper 对象
HeroMapper mapper = sqlSession.getMapper(HeroMapper.class);
// 执行方法,从数据库中获取数据
Hero hero = mapper.selectById(1);

详细流程

获取 MapperProxy 对象

我们现在主要关注的就是 getMapper 方法,该方法为我们创建一个代理对象,该代理对象为我们执行 SQL 语句提供了重要的支持。

// SqlSession 对象
@Override
public <T> T getMapper(Class<T> type) {
    return configuration.getMapper(type, this);
}

getMapper  方法里面委托 Configuration 对象去获取对应的 Mapper 代理对象,之前说过 Configuration 对象里面包含了 Mybatis 中所有重要的信息,其中就包括我们需要的 Mapper 代理对象,而这些信息都是在读取配置信息的时候完成的,也就是执行sqlSessionFactoryBuilder.build 方法。

// Configuration 对象
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    return mapperRegistry.getMapper(type, sqlSession);
}

我们可以看到它又将获取 Mapper 代理对象的操作委托给了 MapperRegistry 对象(搁着俄罗斯套娃呢?),这个 MapperRegistry 对象里面就存放了我们想要的 Mapper 代理对象,如果你这么想,就错了,实际上,它存放的并不是我们想要的 Mapper 代理对象,而是 Mapper 代理对象的工厂,Mybatis 这里使用到了工厂模式。

public class MapperRegistry {

  private final Configuration config;
  private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>();

  public MapperRegistry(Configuration config) {
    this.config = config;
  }

  @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);
    }
  }

  public <T> void addMapper(Class<T> type) {
    if (type.isInterface()) {
      if (hasMapper(type)) {
        throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
      }
      boolean loadCompleted = false;
      try {
        knownMappers.put(type, new MapperProxyFactory<>(type));
        // It's important that the type is added before the parser is run
        // otherwise the binding may automatically be attempted by the
        // mapper parser. If the type is already known, it won't try.
        MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
        parser.parse();
        loadCompleted = true;
      } finally {
        if (!loadCompleted) {
          knownMappers.remove(type);
        }
      }
    }
  }
}

我只保留了 getMapper 方法和 addMapper 方法。

在 getMapper 方法中,它获取的是 MapperProxyFactory 对象,我们通过名称可以得出这是一个 Mapper 代理对象工厂,但是我们是要得到一个 MapperProxy 对象,而不是一个工厂对象,我们再来看 getMapper 方法,它通过 mapperProxyFactory.newInstance 来创建代理对象。

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 对象,并且通过 Proxy.newProxyInstance 方法(不会还有人不知道这是 JDK 动态代理吧),创建一个代理对象处理,这个代理对象就是我们想要的结果。这里没有体现出来代理了哪个对象啊?其实 mapperInterface 这是一个成员变量,它引用了需要被代理的对象。而这个成员变量实在创建 MapperProxyFactory 对象的时候赋值的,所以我们每一个需要被代理的接口,在 Mybatis 中都会为它生成一个 MapperProxyFactory 对象,该对象的作用就是为了创建所需要的代理对象。

缓存执行方法

当我们获取到代理对象 mapper 之后,就可以执行它里面的方法。
这里使用一个例子:

// Myabtis 所需要的接口
public interface HeroMapper {
    Hero selectById(Integer id);
}
// HeroMapper 接口所对应的 xml 文件
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
    PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
    "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="test.HeroMapper">
    <select id="selectById" resultType="test.Hero">
        select * from hero where id = #{id}
    </select>
</mapper>

我们执行 selectById 方法,获取一个用户的信息。

// 获取 Mapper 对象
HeroMapper mapper = sqlSession.getMapper(HeroMapper.class);
// 执行方法,从数据库中获取数据
Hero hero = mapper.selectById(1);

通过上面的解析已经知道,这里的 mapper 是一个代理对象的引用,而这个代理类则是 MapperProxy,所以我们主要是去了解 MapperProxy 这个代理类做了什么事情。

public class MapperProxy<T> implements InvocationHandler, Serializable {
    
  private final SqlSession sqlSession;
  private final Class<T> mapperInterface;
  private final Map<Method, MapperMethodInvoker> methodCache;

  public MapperProxy(SqlSession sqlSession, Class<T> mapperInterface, Map<Method, MapperMethodInvoker> methodCache) {
    this.sqlSession = sqlSession;
    this.mapperInterface = mapperInterface;
    this.methodCache = methodCache;
  }

  @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 {
        return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
      }
    } catch (Throwable t) {
      throw ExceptionUtil.unwrapThrowable(t);
    }
  }

  private MapperMethodInvoker cachedInvoker(Method method) throws Throwable {
      return methodCache.computeIfAbsent(method, m -> {
           return new PlainMethodInvoker(new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
      }
  }
    
  private static class PlainMethodInvoker implements MapperMethodInvoker {
      private final MapperMethod mapperMethod;

      public PlainMethodInvoker(MapperMethod mapperMethod) {
          super();
          this.mapperMethod = mapperMethod;
      }

      @Override
      public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {
          return mapperMethod.execute(sqlSession, args);
      }
  }
}

代理对象执行方法时都是直接执行 invoke() 方法,在这个方法中,我们主要就看一条语句 cachedInvoker(method).invoke(proxy, method, args, sqlSession);

我们首先看 cachedInvoker 方法,它的参数是 Method 类型,所以这个 method 表示的就是我们执行的方法 HeroMapper.selectById,它首先从缓存中获取是否之前已经创建过一个该方法的方法执行器 PlainMethodInvoker 对象,其实这只是一个包装类,可有可无,在工程上来说,有了这个包装类,会更加易于维护。而这个执行器里面只有一个成员对象,这个成员对象就是 MapperMethod,并且这个 MapperMethod 的构造函数中需要传递  HeroMapper、HeroMapper.selectById、Cofiguration 这三个参数。

以上步骤都执行完成之后,接下来我们可以看到执行了 PlainMethodInvoker 的 invoke 方法,而它又将真正的操作委托给了 MapperMethod,执行 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());
    }
    return result;
  }

这个方法中,我们可以看到熟悉的几个关键字:select、update、delete、insert,这个就是为了找到执行方式,我们因为是 select 语句,所以分支会走向 select,并且最终会执行到 sqlSession.selectOne 方法中,所以最终饶了一大圈,依然还是回到了我们一开始就提到的 SqlSession 对象中。
在这个方法中,首先会构造参数,也就是我们看到的 convertArgsToSqlCommandParam 方法,它的内部执行方式是按照如下方式来转换参数的:

使用 @param 自定义命名
amethod(@Param int a, @Param int b)  则会构造 map  ->  [{"a", a_arg}, {"b", b_arg}, {"param1",  a_arg}, {"param2", b_arg}],a 和 param1 是对参数 a 的命名,a_arg 是传递的实际的值。
虽然只有两个参数,但是最后却会在 Map 存在四个键值对,因为 Mybatis 最后自己会生成以 param 为前缀的参数名称,名称按照参数的位置进行命名。

不使用 @param

amethod(int a, int b),则会构造 map -> [{"arg0", a_arg}, {"arg1", b_arg}, {"param1", a_arg}, {"param2", b_arg}],因为没有对参数进行自定义命名,所以 Myabtis 就对参数取了一个默认的名称,以 arg 为前缀,位置为后缀进行命名。

在参数只有一个,并且参数为集合的情况下,会存放多个键值对:

  • amethod(Collection<Integer> a),这种情况下,会构造 map -> [{"arg0", a_arg}, {"collection", a_arg}]
  • amethod(List<Integer> a),这种情况下,会构造 map -> [{"arg0", a_arg}, {"collection", a_arg}, {"list", a_arg}]
  • amethod(Integer[] a),这种情况下,会构造 map -> [{"arg0", a_arg}, {"array", a_arg}]
  • 但是,如果有两个参数,那么就不会这么存放,而是按照常规的方式:
  • amethod(List<Integer> a,List<Integer> b)  则会构造 map -> [{"arg0", a_arg}, {"arg1", b_arg}, {"param1", a_arg}, {"param2", b_arg}]
  • amethod(List<Integer> a,int b)  则会构造 map -> [{"arg0", a_arg}, {"arg1", b_arg}, {"param1", a_arg}, {"param2", b_arg}]

不会作为参数的对象
在 Mybatis 中有两个特殊的对象:RowBounds、ResultHandler,这两个对象如果作为参数则不会放入到 map 中,但是会占据位置。

amethod(int a,RowBounds rb, int b),这种情况下,会构造 map -> [{"arg0", a_arg}, {"arg2", b_arg}, {"param1", a_arg}, {"param2", b_arg}]

注意这里的 b 参数的命名分别是 arg2 和 param2,arg2 是因为它的位置在参数的第 3 位,而 param2 则是因为它是第 2 个有效参数。

获取需要执行的 SQL 对象

参数构造完成之后,我们就需要寻找需要执行的 SQL 语句了。

@Override
  public <T> T selectOne(String statement, Object parameter) {
    // Popular vote was to return null on 0 results and throw exception on too many.
    List<T> list = this.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;
    }
  }

这里的 statement 虽然是 String 类型的,但是它并不是真正的 SQL 语句,它是一个寻找对应 MapperStatement 对象的名称,在我们的例子中,它就是 test.HeroMapper.selectById ,Mybatis 通过这个名称可以寻找到包含了 SQL 语句的对象。

我们跟踪代码的执行,最后会来到下面这个方法,这是一个包含三个参数的重载方法。

@Override
  public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
    try {
      MappedStatement ms = configuration.getMappedStatement(statement);
      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();
    }
  }

在第四行代码中,可以得知它通过 statement 从 Configuration 对象中获取了一个 MapperStatement 对象, MapperStatement 对象包含的信息是由 <select>、<update>、<delete> 、<insert> 元素提供的,我们在这些元素中定义的信息都会保存在该对象中,如:Sql 语句、resultMap、fetchSize 等等。

执行 SQL 语句

获取到包含 SQL 语句信息的对象之后,就会交给 Execute 执行器对象去执行后续的处理,也就是 executor.query 方法。

@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    BoundSql boundSql = ms.getBoundSql(parameter);
    CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
    return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}

获取需要自行的 Sql 语句,然后创建一个缓存使用的 key,用于二级缓存。

@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    // ....
    // 跟缓存有关,如果缓存中存在数据,则直接从缓存中返回,否则从数据库中查询
 list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
 return list;
}

最后会执行到一个 doQuery 方法

@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 handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
        stmt = prepareStatement(handler, ms.getStatementLog());
        return handler.query(stmt, resultHandler);
    } finally {
        closeStatement(stmt);
    }
}

这段代码创建了一个 Statement 对象的处理器 StatementHandler,这个处理器主要的工作就是完成 JDBC 中 PrepareStatement 对象的一些准备工作,包括:创建 PrepareStatement 对象,设置需要执行的 sql 语句,为 sql 语句中的参数赋值。完成这些工作之后,就开始从数据库获取数据了。

@Override
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
    PreparedStatement ps = (PreparedStatement) statement;
    ps.execute();
    return resultSetHandler.handleResultSets(ps);
}

第四行代码即执行对应的 Sql 查询,后续则是对结果进行处理。

总结

Mybatis 通过 MapperProxy 代理了我们的 Dao 接口类,以此来帮助我们执行预定义的 Sql 语句,通过 Cache 来缓存对应的执行结果,通过 StatementHandler  创建 PrepareStatement 对象,通过 jdbc 执行 SQL 操作。

到此这篇关于浅谈MyBatis 如何执行一条 SQL语句的文章就介绍到这了,更多相关MyBatis 执行SQL语句内容请搜索自学编程网以前的文章或继续浏览下面的相关文章希望大家以后多多支持自学编程网!

编程技巧