热门标签 | HotTags
当前位置:  开发笔记 > 数据库 > 正文

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

Mybatis是Java开发中比较常用的ORM框架。在日常工作中,我们都是直接通过SpringBoot自动配置,并直接使用,但是却不知道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 getMapper(Class type) {
    return configuration.getMapper(type, this);
}

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

// Configuration 对象
public  T getMapper(Class 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, MapperProxyFactory<&#63;>> knownMappers = new HashMap<>();

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

  @SuppressWarnings("unchecked")
  public  T getMapper(Class type, SqlSession sqlSession) {
    final MapperProxyFactory mapperProxyFactory = (MapperProxyFactory) 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  void addMapper(Class 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 mapperProxy) {
    return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}

public T newInstance(SqlSession sqlSession) {
    final MapperProxy 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 文件
<&#63;xml version="1.0" encoding="UTF-8" &#63;>


    


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

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

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

public class MapperProxy implements InvocationHandler, Serializable {
    
  private final SqlSession sqlSession;
  private final Class mapperInterface;
  private final Map methodCache;

  public MapperProxy(SqlSession sqlSession, Class mapperInterface, Map 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 a),这种情况下,会构造 map -> [{"arg0", a_arg}, {"collection", a_arg}]
  • amethod(List a),这种情况下,会构造 map -> [{"arg0", a_arg}, {"collection", a_arg}, {"list", a_arg}]
  • amethod(Integer[] a),这种情况下,会构造 map -> [{"arg0", a_arg}, {"array", a_arg}]
  • 但是,如果有两个参数,那么就不会这么存放,而是按照常规的方式:
  • amethod(List a,List b)  则会构造 map -> [{"arg0", a_arg}, {"arg1", b_arg}, {"param1", a_arg}, {"param2", b_arg}]
  • amethod(List 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 selectOne(String statement, Object parameter) {
    // Popular vote was to return null on 0 results and throw exception on too many.
    List 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  List 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 对象包含的信息是由

推荐阅读
  • 在说Hibernate映射前,我们先来了解下对象关系映射ORM。ORM的实现思想就是将关系数据库中表的数据映射成对象,以对象的形式展现。这样开发人员就可以把对数据库的操作转化为对 ... [详细]
  • 推荐一个ASP的内容管理框架(ASP Nuke)的优势和适用场景
    本文推荐了一个ASP的内容管理框架ASP Nuke,并介绍了其主要功能和特点。ASP Nuke支持文章新闻管理、投票、论坛等主要内容,并可以自定义模块。最新版本为0.8,虽然目前仍处于Alpha状态,但作者表示会继续更新完善。文章还分析了使用ASP的原因,包括ASP相对较小、易于部署和较简单等优势,适用于建立门户、网站的组织和小公司等场景。 ... [详细]
  • VScode格式化文档换行或不换行的设置方法
    本文介绍了在VScode中设置格式化文档换行或不换行的方法,包括使用插件和修改settings.json文件的内容。详细步骤为:找到settings.json文件,将其中的代码替换为指定的代码。 ... [详细]
  • Java序列化对象传给PHP的方法及原理解析
    本文介绍了Java序列化对象传给PHP的方法及原理,包括Java对象传递的方式、序列化的方式、PHP中的序列化用法介绍、Java是否能反序列化PHP的数据、Java序列化的原理以及解决Java序列化中的问题。同时还解释了序列化的概念和作用,以及代码执行序列化所需要的权限。最后指出,序列化会将对象实例的所有字段都进行序列化,使得数据能够被表示为实例的序列化数据,但只有能够解释该格式的代码才能够确定数据的内容。 ... [详细]
  • 本文介绍了Java工具类库Hutool,该工具包封装了对文件、流、加密解密、转码、正则、线程、XML等JDK方法的封装,并提供了各种Util工具类。同时,还介绍了Hutool的组件,包括动态代理、布隆过滤、缓存、定时任务等功能。该工具包可以简化Java代码,提高开发效率。 ... [详细]
  • android listview OnItemClickListener失效原因
    最近在做listview时发现OnItemClickListener失效的问题,经过查找发现是因为button的原因。不仅listitem中存在button会影响OnItemClickListener事件的失效,还会导致单击后listview每个item的背景改变,使得item中的所有有关焦点的事件都失效。本文给出了一个范例来说明这种情况,并提供了解决方法。 ... [详细]
  • 本文讨论了Alink回归预测的不完善问题,指出目前主要针对Python做案例,对其他语言支持不足。同时介绍了pom.xml文件的基本结构和使用方法,以及Maven的相关知识。最后,对Alink回归预测的未来发展提出了期待。 ... [详细]
  • 本文讲述了如何通过代码在Android中更改Recycler视图项的背景颜色。通过在onBindViewHolder方法中设置条件判断,可以实现根据条件改变背景颜色的效果。同时,还介绍了如何修改底部边框颜色以及提供了RecyclerView Fragment layout.xml和项目布局文件的示例代码。 ... [详细]
  • 本文介绍了在SpringBoot中集成thymeleaf前端模版的配置步骤,包括在application.properties配置文件中添加thymeleaf的配置信息,引入thymeleaf的jar包,以及创建PageController并添加index方法。 ... [详细]
  • 本文讨论了在Spring 3.1中,数据源未能自动连接到@Configuration类的错误原因,并提供了解决方法。作者发现了错误的原因,并在代码中手动定义了PersistenceAnnotationBeanPostProcessor。作者删除了该定义后,问题得到解决。此外,作者还指出了默认的PersistenceAnnotationBeanPostProcessor的注册方式,并提供了自定义该bean定义的方法。 ... [详细]
  • eclipse学习(第三章:ssh中的Hibernate)——11.Hibernate的缓存(2级缓存,get和load)
    本文介绍了eclipse学习中的第三章内容,主要讲解了ssh中的Hibernate的缓存,包括2级缓存和get方法、load方法的区别。文章还涉及了项目实践和相关知识点的讲解。 ... [详细]
  • ZSI.generate.Wsdl2PythonError: unsupported local simpleType restriction ... [详细]
  • XML介绍与使用的概述及标签规则
    本文介绍了XML的基本概念和用途,包括XML的可扩展性和标签的自定义特性。同时还详细解释了XML标签的规则,包括标签的尖括号和合法标识符的组成,标签必须成对出现的原则以及特殊标签的使用方法。通过本文的阅读,读者可以对XML的基本知识有一个全面的了解。 ... [详细]
  • Java验证码——kaptcha的使用配置及样式
    本文介绍了如何使用kaptcha库来实现Java验证码的配置和样式设置,包括pom.xml的依赖配置和web.xml中servlet的配置。 ... [详细]
  • HDFS2.x新特性
    一、集群间数据拷贝scp实现两个远程主机之间的文件复制scp-rhello.txtroothadoop103:useratguiguhello.txt推pushscp-rr ... [详细]
author-avatar
xiaonq
这个家伙很懒,什么也没留下!
Tags | 热门标签
RankList | 热门文章
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有