当前位置: 首页 > 图灵资讯 > 技术篇> #yyds干货盘点#Mybatis如何执行SQL语句

#yyds干货盘点#Mybatis如何执行SQL语句

来源:图灵教育
时间:2023-06-06 09:32:34

mybatis 操作数据库的过程

// 第一步:阅读mybatiss-config.Inputstreamml配置文件 inputStream = Resources.getResourceAsStream("mybatis-config.xml");// 第二步:构建SqlSessionFactory(框架初始化)SqlSessionFactory sqlSessionFactory = new  SqlSessionFactoryBuilder().bulid();// 第三步:打开sqlsessionSqlsesion session = sqlSessionFactory.openSession();// 第四步:获取Mapper接口对象(底层为动态代理)AccountMapper accountMapper = session.getMapper(AccountMapper.class);// 第五步:调用Mapper接口对象操作数据库;Account account = accountMapper.selectByPrimaryKey(1);

通过调用 session.getMapper (AccountMapper.class) 所得到的 AccountMapper 它是一个动态代理对象,因此执行

accountMapper.selectByPrimaryKey (1) 在方法之前,它将被接受 invoke () 拦截,先执行 invoke () 中的逻辑。

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {    try {        //  如果Object是Object,则直接调用要执行的方法,不拦截        if (Object.class.equals(method.getDeclaringClass())) {            return method.invoke(this, args);            //如果是默认方法,也就是java8中的default方法        } else if (isDefaultMethod(method)) {            // default方法直接执行            return invokeDefaultMethod(proxy, method, args);        }    } catch (Throwable t) {        throw ExceptionUtil.unwrapThrowable(t);    }     // MapperMethod从缓存中获取    final MapperMethod mapperMethod = cachedMapperMethod(method);    return mapperMethod.execute(sqlSession, args);}

从 methodCache 获取对应 DAO 方法的 MapperMethod

MapperMethod 主要功能是执行 SQL 句子的相关操作将在初始化时实例化两个对象:SqlCommand(Sql 命令)和 MethodSignature(方法签名)。

/**   * 根据Mapper接口类型、接口方法和核心配置对象 MapperMethod对象构建   * @param mapperInterface   * @param method   * @param config   */  public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {    this.command = new SqlCommand(config, mapperInterface, method);    // Mapper接口中的数据库操作方法(如Account) selectById(Integer id);)密封方法签名MethodSignature    this.method = new MethodSignature(config, mapperInterface, method);  }

new SqlCommand()调用 SqlCommand 分类结构方法:

public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) {      // 在Mapper接口中获取某种方法的方法名      // accountmapper.selectByPrimaryKey(1)      final String methodName = method.getName();      // 获取方法所在的类      final Class<?> declaringClass = method.getDeclaringClass();      // Mapper语句对象(在配置文件中<mapper></mapper>封装中间的sql语句)      MappedStatement ms = resolveMappedStatement(mapperInterface, methodName, declaringClass,          configuration);      if (ms == null) {        if (method.getAnnotation(Flush.class) != null) {          name = null;          type = SqlCommandType.FLUSH;        } else {          throw new BindingException("Invalid bound statement (not found): "              + mapperInterface.getName() + "." + methodName);        }      } else {        // 如com.bjpowernode.mapper.AccountMapper.selectByPrimaryKey        name = ms.getId();        // SQL类型:增加 删 改 查        type = ms.getSqlCommandType();        if (type == SqlCommandType.UNKNOWN) {          throw new BindingException("Unknown execution method for: " + name);        }      }    }private MapperMethod cachedMapperMethod(Method method) {     MapperMethod mapperMethod = (MapperMethod)this.methodCache.get(method);     if (mapperMethod == null) {         mapperMethod = new MapperMethod(this.mapperInterface, method, this.sqlSession.getConfiguration());         this.methodCache.put(method, mapperMethod);     }     return mapperMethod; }

调用 mapperMethod.execute (sqlSession, args)

在 mapperMethod.execute () 在方法中,我们可以看到:mybatis 定义了 5 种 SQL 操作类型:insert/update/delete/select/flush。其中,select 操作类型可分为五类,这五类返回结果不同,分别对应:

・返回参数为空:executeWithResultHandler ();

・查询多个记录:executeForMany (),返回对象为 JavaBean

・返参对象为 map:executeForMap (), 通过这种方法查询数据库,最终返回结果不是 JavaBean,而是 Map

・游标查询:executeForCursor ();关于什么是游标查询,自己百度哈;

・查询单条记录: sqlSession.selectOne (),通过这种查询方法,最终只会返回一个结果;

通过源代码跟踪我们不难发现:当调用时 mapperMethod.execute () 执行 SQL 无论是insert///update/delete/flush,还是 select(包括 5 种不同的 select), 本质上是通过的 sqlSession 调用的。在 SELECT 虽然调用了操作 MapperMethod 本质上,中间的方法仍然是通过的 Sqlsession 下的 select (), selectList (), selectCursor (), selectMap () 实现等方法。

而 SqlSession 最后,调用执行器的内部实现 Executor(稍后会详细说明)。在这里,我们可以先看一看。 mybatis 在执行 SQL 语句调用过程:
accountmapper.selectByPrimaryKey (1) 为例:

・调用 SqlSession.getMapper ():得到 xxxMapper (如 UserMapper) 动态代理对象;

・accountmapper调用accountmapp.selectByPrimaryKey (1):在 xxxMapper 动态代理内部将根据要执行的情况进行 SQL 语句类型 (insert/update/delete/select/flush) 来调用 SqlSession 对应的不同方法,如 sqlSession.insert ();

・在 sqlSession.insert () 在实现方法的逻辑中,它将被转移到 executor.query () 进行查询;

・executor.query () 最后,它将被转移 statement 这里就是类操作。 jdbc 操作了。

有些人会好奇,为什么要通过不断的转移,SqlSession->Executor->Statement,而不是直接调用 Statement 执行 SQL 语句呢?因为在调用 Statement 以前会处理一些常见的逻辑,比如 Executor 的实现类 BaseExecutor 会有一级缓存相关逻辑,在 CachingExecutor 会有二次缓存的相关逻辑。若直接调用 Statement 执行 SQL 句子,所以在每一个 Statement 在实现类中,写一套一级缓存和二级缓存的逻辑是冗余的。这篇文章稍后会详细讨论。

// SQL命令(分析mybatiss-config.在xml配置文件时生成)  private final SqlCommand command;    public Object execute(SqlSession sqlSession, Object[] args) {    Object result;    // 从command对象中获取要执行的SQL语句类型,INSERT/UPDATE/DELETE/SELECT    switch (command.getType()) {      // 插入      case INSERT: {        // 将接口方法中的参数转换为SQL可识别的参数        // 如:accountMapper.selectByPrimaryKey(1)        // 将参数“1”转换为SQL可识别的参数        Object param = method.convertArgsToSqlCommandParam(args);        // sqlSession.insert(): 调用SqlSession执行插入操作        // rowCountResult(): SQL语句的执行结果获得        result = rowCountResult(sqlSession.insert(command.getName(), param));        break;      }      // 更新      case UPDATE: {        Object param = method.convertArgsToSqlCommandParam(args);        // sqlSession.insert(): 调用SqlSession执行更新操作        // rowCountResult(): SQL语句的执行结果获得        result = rowCountResult(sqlSession.update(command.getName(), param));        break;      }      // 删除      case DELETE: {        Object param = method.convertArgsToSqlCommandParam(args);        // sqlSession.insert(): 调用SqlSession执行更新操作        // rowCountResult(): SQL语句的执行结果获得        result = rowCountResult(sqlSession.delete(command.getName(), param));        break;      }      // 查询      case SELECT:        // method.returnsVoid(): void是否为返参        // method.hasResultHandler(): 处理器是否有相应的结果?        if (method.returnsVoid() && method.hasResultHandler()) {          executeWithResultHandler(sqlSession, args);          result = null;        } else if (method.returnsMany()) { // 查询多个记录          result = executeForMany(sqlSession, args);        } else if (method.returnsMap()) { // 查询结果返回Map          result = executeForMap(sqlSession, args);        } else if (method.returnsCursor()) { // 以游标的形式查询          result = executeForCursor(sqlSession, args);        } else {          // 参数转换 转化为sqlcommand参数          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;  }

这样一行代码出现在上面很多地方:method.convertArgsToSqlCommandParam (args),该方法的作用是将方法参数转换为 SqlCommandParam;paramnameresolver.getNamedParams () 实现。在看 paramNameResolver.getNamedParams () 以前,我们先来看看 paramNameResolver 是什么?

public Object convertArgsToSqlCommandParam(Object[] args) {      return paramNameResolver.getNamedParams(args);    }

在我们面前,我们在实例化 MethodSignature 对象 (new MethodSignature) 在其结构方法中,将实例化 ParamNameResolver 对象,对象主要用于处理接口形式的参数,最后将参数放置在一个位置 map(即属性 names)中。map 的 key 是参数的位置,value 名称为参数。

public MethodSignature(Configuration configuration, Class<?> mapperInterface, Method method) {  ...  this.paramNameResolver = new ParamNameResolver(configuration, method);}

对 names 解释字段:

假设在 xxxMapper 有这样一种接口方法 selectByIdAndName ()

・selectByIdAndName (@Param ("id") String id, @Param ("name") String name) 转化为 map 为 {{0, "id"}, {1, "name"}}

・selectByIdAndName (String id, String name) 转化为 map 为 {{0, "0"}, {1, "1"}}

・selectByIdAndName (int a, RowBounds rb, int b) 转化为 map 为 {{0, "0"}, {2, "1"}}

构造方法将经历以下步骤

  1. 该方法的参数类型和方法的参数通过反射注释,method.getParameterAnnotations () 该方法返回注解二维数组,每种方法的参数包含一个注解数组。
  2. 遍历所有参数
  • 首先判断这个参数的类型是否特殊,RowBounds 和 ResultHandler,是的,跳过,我们不处理
  • 判断这个参数是否用于判断 Param 注释,如果使用的话 name 就是 Param 并将注解值 name 放到 map 键是方法中参数的位置,value 为 Param 的值
  • 若未使用 Param 注释,判断是否打开 UseActualParamName,若打开,则使用 java8 得到方法名称的反射,这里容易引起异常,

具体原因参考上一篇博文的具体原因.

  • 如果不满足上述条件,则该参数的名称为参数的下标
// 由于key有param1,param2,通用key前缀,param3等;  public static final String GENERIC_NAME_PREFIX = "param";  // 存储参数的位置和相应的参数名称  private final SortedMap<Integer, String> names;  // 是否使用@Param注释  private boolean hasParamAnnotation;  public ParamNameResolver(Configuration config, Method method) {    // 通过注释获得方法的参数类型数组    final Class<?>[] paramTypes = method.getParameterTypes();    // 通过反射获得的参数注解数组    final Annotation[][] paramAnnotations = method.getParameterAnnotations();    // SortedMap对象用于存储所有参数名称    final SortedMap<Integer, String> map = new TreeMap<>();    // 参数注解数组长度,即方法用于参数中的几个地方@Param    // 如selectByIdandname(@Param("id") String id, @Param("name") String name)中,paramCount=2    int paramCount = paramAnnotations.length;    // 遍历所有参数    for (int paramIndex = 0; paramIndex < paramCount; paramIndex++) {      // 判断该参数的类型是否为特殊类型,Rowbounds和ResultHandler,是的话跳过      if (isSpecialParameter(paramTypes[paramIndex])) {        continue;      }      String name = null;      for (Annotation annotation : paramAnnotations[paramIndex]) {        // 判断该参数是否使用@Param注释        if (annotation instanceof Param) {          // Param注释用于标记当前方法          hasParamAnnotation = true;          // 如果使用,name是Param注解值          name = ((Param) annotation).value();          break;        }      }      // 若经上述处理,参数名还是null,这意味着当前参数没有指定@Param注释      if (name == null) {        // 判断UseactualParmnameme是否打开        if (config.isUseActualParamName()) {          // 若打开,如果打开,使用java8的反射获得参数对应的属性名          name = getActualParamName(method, paramIndex);        }        // 如果name仍然是nulll        if (name == null) {          // use the parameter index as the name ("0", "1", ...)          // 以map中的参数下标为参数的name,如 ("0", "1", ...)          name = String.valueOf(map.size());        }      }      // 将参数放入map中,key是参数在方法中的位置,value是参数的name(@param的value值/参数对应的属性名/参数在map中的位置下标)      map.put(paramIndex, name);    }    // 最后,使用Collections工具类的静态方法将结果map变成不可修改的类型    names = Collections.unmodifiableSortedMap(map);  }

getNamedParams(): 该方法将对应参数名和参数值,并保存额外的一份 param 开始时,参数顺序数字的值

public Object getNamedParams(Object[] args) {    // 这里的names是Paramnameresolver中的names,在构建Paramnameresolver对象时,Map创建了    // 获取方法参数的数量    final int paramCount = names.size();    // 没有参数    if (args == null || paramCount == 0) {      return null;    // 只有一个参数,而且没有使用@Param注释。    } else if (!hasParamAnnotation && paramCount == 1) {      // 直接返回,不做任务处理      return args[names.firstKey()];    } else {      // 包装成Parammap对象。这个对象继承了Hashmap,重写了get方法。      final Map<String, Object> param = new ParamMap<>();      int i = 0;      // names中的所有键值对      for (Map.Entry<Integer, String> entry : names.entrySet()) {        // 以参数名为key, 将相应的参数值作为value放入结果param对象中        param.put(entry.getValue(), args[entry.getKey()]);        // 按顺序(param1)添加一般参数名称, param2, ...)        final String genericParamName = GENERIC_NAME_PREFIX + (i + 1);        // 确保不覆盖@Param 命名的参数        if (!names.containsValue(genericParamName)) {          param.put(genericParamName, args[entry.getKey()]);        }        i++;      }      return param;    }  }}

getNamedParams () 总结:

  1. 只有一个参数时,直接返回,不做任务处理;
  2. 否则,存入 Map 键值对的形式如下:paramName=paramValue

・selectByIdAndName (@Param ("id") String id, @Param ("name") String name): 引入的参数是 ["1", “张三”],最后分析出来 map 为:{“id”:”1”,”“name”:” 张三”}

・selectByIdAndName (String id, @Param ("name") String name): 引入的参数是 ["1", “张三”],最后分析出来 map 为:{param1:”1”,”“name”:” 张三”}

假设执行的 SQL 语句是 select 类型,继续向下看代码

在 mapperMethod.execute (), convertargstosqlcomandparman () 方法处理方法参数后,假设此时我们调用查询单个记录,那么下一步将执行 sqlSession.selectOne () 方法。

sqlSession.selectOne () 源码分析:

sqlSession.selectOne () 也是调的 sqlSession.selectList () 方法,只是返回 list 第一条数据。当 list 当有多个数据时,抛出异常。

@Overridepublic <T> T selectOne(String statement, Object parameter) {  // 调用当前selectlist方法  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;  }}

sqlSession.selectList () 方法

@Override  public <E> List<E> selectList(String statement, Object parameter) {    return this.selectList(statement, parameter, RowBounds.DEFAULT);  }

继续看:

@Override  public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {    try {      // configurationMappedStatements根据key(id的全路径)获取MappedStatement对象      MappedStatement ms = configuration.getMappedStatement(statement);      // 实现类Baseexecutor的query()方法调用Executor      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();    }  }

在 sqlSession.selectList () 在方法中,我们可以看到调用 executor.query ()假设我们打开了二次缓存,那么 executor.query () 调用的是 executor 的实现类 CachingExecutor 中的 query (),二次缓存的逻辑是 CachingExecutor 在这一类中实现。