MyBatis是怎么玩的

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
简介:

MyBatis

MyBatis 是支持定制化 SQL、存储过程以及高级映射的优秀的持久层框架。MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。MyBatis 可以对配置和原生Map使用简单的 XML 或注解,将接口和 Java 的 POJOs(Plain Old Java Objects,普通的 Java对象)映射成数据库中的记录。

在深入的了解MyBatis之前,我们先来看一下,没有MyBatis的时候,我们是怎么去处理与数据库的交互的。

一、从JDBC开始

JDBC(Java DataBase Connectivity,java数据库连接)是一种用于执行SQL语句的Java API,可以为多种关系数据库提供统一访问。

看一下下面这个例子,逻辑非常简单,就是从数据库中查询出所有的的用户:

import java.sql.*;

public class FirstExample {
   static final String JDBC_DRIVER = "com.mysql.jdbc.Driver";  
   static final String DB_URL = "jdbc:mysql://localhost/EMP";
   static final String USER = "username";
   static final String PASS = "password";
   
   public static void main(String[] args) {
       Connection conn = null;
       Statement stmt = null;
       try{
          Class.forName("com.mysql.jdbc.Driver");
          conn = DriverManager.getConnection(DB_URL,USER,PASS);
          stmt = conn.createStatement();
          String sql = "SELECT id, first, last, age FROM user";
          ResultSet rs = stmt.executeQuery(sql);
          List<User> userList = new ArrayList<>();
          while(rs.next()){
             //Retrieve by column name
             int id  = rs.getInt("id");
             int age = rs.getInt("age");
             String first = rs.getString("first");
             String last = rs.getString("last");
             User newUser = new User();
             newUser.setId(id);
             newUser.setAge(age);
             newUser.setFirst(first);
             newUser.setLast(last);
             userList.add(newUser);
          }
          rs.close();
          stmt.close();
          conn.close();
       }catch(SQLException se){
          //Handle errors for JDBC
          se.printStackTrace();
       }catch(Exception e){
          //Handle errors for Class.forName
          e.printStackTrace();
       }finally{
          //finally block used to close resources
          try{
             if(stmt!=null)
                stmt.close();
          }catch(SQLException se2){
          }// nothing we can do
          try{
             if(conn!=null)
                conn.close();
          }catch(SQLException se){
             se.printStackTrace();
          }
       }
    }
}

这段代码看起来没什么复杂的,这样进行数据库操作也是完全的可以,可以借助代码生成工具快速的生成对一个表的CRUD操作,但是对于比较复杂的查询,生成工具就力不从心了。

那么,我们就来对比一下MyBatis是怎么与数据库进行交互的。

二、Get started with MyBatis

和很多的Java框架一样,如果你需要使用MyBatis,那么就需要MyBatis的jar包出现在classpath中,可以通过下载、Maven、Gradle等等方式来处理这个问题。

根据国际惯例,框架都需要配置。

2.1 配置

下面是一个非常基础的配置mybatis-config.xml:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
  <environments default="development">
    <environment id="development">
      <transactionManager type="JDBC"/>
      <dataSource type="POOLED">
        <property name="driver" value="${driver}"/>
        <property name="url" value="${url}"/>
        <property name="username" value="${username}"/>
        <property name="password" value="${password}"/>
      </dataSource>
    </environment>
  </environments>
  <mappers>
    <mapper resource="org/mybatis/example/BlogMapper.xml"/>
  </mappers>
</configuration>

BlogMapper.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="org.mybatis.example.BlogMapper">
  <select id="selectBlog" resultType="Blog">
    select * from Blog where id = #{id}
  </select>
</mapper>

2.2 加载配置

String resource = "org/mybatis/example/mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

2.3 执行数据库操作

SqlSession session = sqlSessionFactory.openSession();
try {
  Blog blog = session.selectOne("org.mybatis.example.BlogMapper.selectBlog", 101);
} finally {
  session.close();
}

使用MyBatis对数据库进行操作的风格与使用JDBC是完全不一样的,接下来,我们就来详细的看一下。

三、MyBatis

Mybatis大致可以分为两个phase,一,配置阶段,二,执行阶段

  1. 在启动阶段主要完成的:

    1. 配置文件的解析
    2. 创建核心的Configuration对象,并通过配置创建相关联的对象,并用这些对象填充Configuration对象
    3. 创建SqlSession实例
  2. 在执行阶段,根据之前的配置,通过SqlSession执行,参数处理,SQL语句解析处理,语句执行,处理返回。

3.1 配置阶段

3.1.1 配置都包含了什么内容

  1. 对于配置文件的读取,Mybatis抽象了一个名为Resources的类,用于从classpath中找到配置文件,并将其转为InputStream或者是Reader
  2. 将上一步中获得的InputStream或者Reader对象传递给SqlSessionFactoryBuilder,builder会new XMLConfigBuilder,调用其parse方法,得到Configuration的实例,将Configuration的实例传递给DefaultSqlSessionFactory。由此可见,Mybatis的配置的解析是通过XMLConfigBuilder完成的。
3.1.1.1 包罗万象的Configuration

在详细了解XMLConfigBuilder之前,我们需要看一下Mybatis的Configuration都包含了哪些内容。从官方给出的文档上来看,Configuration包含以下的配置信息:

  • properties
    These are externalizable, substitutable properties that can be configured in a typical Java Properties file instance, or passed in through sub-elements of the properties element. For example:
<properties resource="org/mybatis/example/config.properties">
<property name="username" value="dev_user"/>
<property name="password" value="F2Fa3!33TYyg"/>
</properties>
  • settings

    <settings>
      <setting name="cacheEnabled" value="true"/>
      <setting name="lazyLoadingEnabled" value="true"/>
      <setting name="multipleResultSetsEnabled" value="true"/>
      <setting name="useColumnLabel" value="true"/>
      <setting name="useGeneratedKeys" value="false"/>
      <setting name="autoMappingBehavior" value="PARTIAL"/>
      <setting name="autoMappingUnknownColumnBehavior" value="WARNING"/>
      <setting name="defaultExecutorType" value="SIMPLE"/>
      <setting name="defaultStatementTimeout" value="25"/>
      <setting name="defaultFetchSize" value="100"/>
      <setting name="safeRowBoundsEnabled" value="false"/>
      <setting name="mapUnderscoreToCamelCase" value="false"/>
      <setting name="localCacheScope" value="SESSION"/>
      <setting name="jdbcTypeForNull" value="OTHER"/>
      <setting name="lazyLoadTriggerMethods" value="equals,clone,hashCode,toString"/>
    </settings>
  • typeAliases

    <typeAliases>
    <typeAlias alias="Author" type="domain.blog.Author"/>
    <typeAlias alias="Blog" type="domain.blog.Blog"/>
    <typeAlias alias="Comment" type="domain.blog.Comment"/>
    <typeAlias alias="Post" type="domain.blog.Post"/>
    <typeAlias alias="Section" type="domain.blog.Section"/>
    <typeAlias alias="Tag" type="domain.blog.Tag"/>
    </typeAliases>
<typeAliases>
<package name="domain.blog"/>
</typeAliases>
@Alias("author")
public class Author {
...
}
  • typeHandlers
    Whenever MyBatis sets a parameter on a PreparedStatement or retrieves a value from a ResultSet, a TypeHandler is used to retrieve the value in a means appropriate to the Java type. The following table describes the default TypeHandlers
  • objectFactory
    Each time MyBatis creates a new instance of a result object, it uses an ObjectFactory instance to do so. The default ObjectFactory does little more than instantiate the target class with a default constructor, or a parameterized constructor if parameter mappings exist. If you want to override the default behaviour of the ObjectFactory
  • plugins
    MyBatis allows you to intercept calls to at certain points within the execution of a mapped statement. By default, MyBatis allows plug-ins to intercept method calls of:

    • Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
    • ParameterHandler (getParameterObject, setParameters)
    • ResultSetHandler (handleResultSets, handleOutputParameters)
    • StatementHandler (prepare, parameterize, batch, update, query)
  • environments

    • environment

    MyBatis can be configured with multiple environments. This helps you to apply your SQL Maps to multiple databases for any number of reasons.

    One important thing to remember though: While you can configure multiple environments, you can only choose ONE per SqlSessionFactory instance.

    • transactionManager

    There are two TransactionManager types (i.e. type="[JDBC|MANAGED]") that are included with MyBatis

  • dataSource
  • databaseIdProvider

MyBatis is able to execute different statements depending on your database vendor.

  • mappers

Now that the behavior of MyBatis is configured with the above configuration elements, we’re ready to define our mapped SQL statements. But first, we need to tell MyBatis where to find them. Java doesn’t really provide any good means of auto-discovery in this regard, so the be.

Configuration类中,包含了

  • environment
  • databaseId
  • setting的配置项
  • jdbcTypeForNull
  • lazyLoadTriggerMethods
  • defaultStatementTimeout
  • defaultFetchSize
  • defaultExecutorType
  • autoMappingBehavior

    • NONE
      Disables auto-mapping.
    • PARTIAL(default)
      Will only auto-map results with no nested result mappings defined inside
    • FULL
      Will auto-map result mappings of any complexity (containing nested or otherwise)
  • autoMappingUnknownColumnBehavior

    • NONE(default)
      Do nothing
    • WARNING
      Output warning log
    • FAILING
      Fail mapping
  • variables(properties)
  • reflectorFactory
    Reflector represents a cached set of class definition information that allows for easy mapping between property names and getter/setter methods
  • objectFactory
    MyBatis uses an ObjectFactory to create all needed new Objects.
  • objectWrapperFactory
    Wrapper of the object, 主要在MetaObject中使用
  • proxyFactory
    proxyFactory,default is JavassistProxyFactory
  • configurationFactory
    Configuration factory class. Used to create Configuration for loading deserialized unread properties.
  • mapperRegistry
    通过字段Map<Class<?>, MapperProxyFactory<?>> knownMappers缓存mapper,有两个重要的方法:
  1. public <T> void addMapper(Class<T> type)
    首先会添加一个typeMapperProxyFactory实例的对应关系到knownmapper中,随后创建一个MapperAnnotationBuilder的实例,并调用其parse方法
  2. public <T> T getMapper(Class<T> type, SqlSession sqlSession)
    尝试从knownmapper中,通过type找到对应的MapperProxyFactory,如果有MapperProxyFactory,则调用其newInstance()创建一个mapper的代理实例。
  • interceptorChain
    使用一个ArrayList保存了所有的Interceptor,其最重要的方法为:
  public Object pluginAll(Object target) {
    for (Interceptor interceptor : interceptors) {
      target = interceptor.plugin(target);
    }
    return target;
  }
  • typeHandlerRegistry

    //JDBC Type
    private final Map<JdbcType, TypeHandler<?>> JDBC_TYPE_HANDLER_MAP = new EnumMap<JdbcType, TypeHandler<?>>(JdbcType.class);
    //Java Type
    private final Map<Type, Map<JdbcType, TypeHandler<?>>> TYPE_HANDLER_MAP = new ConcurrentHashMap<Type, Map<JdbcType, TypeHandler<?>>>();
    //Unknown Type
    private final TypeHandler<Object> UNKNOWN_TYPE_HANDLER = new UnknownTypeHandler(this);
    //All Type
    private final Map<Class<?>, TypeHandler<?>> ALL_TYPE_HANDLERS_MAP = new HashMap<Class<?>, TypeHandler<?>>();
    //NULL Type
    private static final Map<JdbcType, TypeHandler<?>> NULL_TYPE_HANDLER_MAP = new HashMap<JdbcType, TypeHandler<?>>();
  • typeAliasRegistry
  • languageRegistry
  • mappedStatements(Map<String, MappedStatement>)
  • caches (Map<String, Cache>)
  • resultMaps(Map<String, ResulMap>)
  • parameterMaps(Map<String, ParameterMap>)
  • keyGenerators(Map<String, KeyGenerator>)
  • loadedResources(Set<String>)
  • sqlFragments(<String, XNode>)
  • cacheRefMap(Map<String, String>)
  • incompleteStatements(Collection<XMLStatementBuilder>)
  • incompleteCacheRefs(Collection<CacheRefResolver>)
  • incompleteResultMaps(Collection<ResultMapResolver>)
  • incompleteMethods(Collection<MethodResolver>)
3.1.1.2 new Configuration时都做了些什么

在new Configuration的时候,初始化了一些默认的type alias,

typeAliasRegistry.registerAlias("JDBC", JdbcTransactionFactory.class);
typeAliasRegistry.registerAlias("MANAGED", ManagedTransactionFactory.class);

typeAliasRegistry.registerAlias("JNDI", JndiDataSourceFactory.class);
typeAliasRegistry.registerAlias("POOLED", PooledDataSourceFactory.class);
typeAliasRegistry.registerAlias("UNPOOLED", UnpooledDataSourceFactory.class);

typeAliasRegistry.registerAlias("PERPETUAL", PerpetualCache.class);
typeAliasRegistry.registerAlias("FIFO", FifoCache.class);
typeAliasRegistry.registerAlias("LRU", LruCache.class);
typeAliasRegistry.registerAlias("SOFT", SoftCache.class);
typeAliasRegistry.registerAlias("WEAK", WeakCache.class);

typeAliasRegistry.registerAlias("DB_VENDOR", VendorDatabaseIdProvider.class);

typeAliasRegistry.registerAlias("XML", XMLLanguageDriver.class);
typeAliasRegistry.registerAlias("RAW", RawLanguageDriver.class);

typeAliasRegistry.registerAlias("SLF4J", Slf4jImpl.class);    typeAliasRegistry.registerAlias("COMMONS_LOGGING", JakartaCommonsLoggingImpl.class);    typeAliasRegistry.registerAlias("LOG4J", Log4jImpl.class);
typeAliasRegistry.registerAlias("LOG4J2", Log4j2Impl.class);
typeAliasRegistry.registerAlias("JDK_LOGGING", Jdk14LoggingImpl.class);
typeAliasRegistry.registerAlias("STDOUT_LOGGING", StdOutImpl.class);
typeAliasRegistry.registerAlias("NO_LOGGING", NoLoggingImpl.class);

typeAliasRegistry.registerAlias("CGLIB", CglibProxyFactory.class);
typeAliasRegistry.registerAlias("JAVASSIST", JavassistProxyFactory.class);

同时设置了默认的language driver为XMLLanguageDriver,并注册是RawLanguageDriver

3.1.2 配置文件的处理过程

3.1.2.1 XML

在Mybatis中Configuration实例的各项配置是通过XMLConfigBuilder完成的,具体的来看,主要是通过:

  private void parseConfiguration(XNode root) {
    try {
      //issue #117 read properties first
      propertiesElement(root.evalNode("properties"));
      Properties settings = settingsAsProperties(root.evalNode("settings"));
      loadCustomVfs(settings);
      typeAliasesElement(root.evalNode("typeAliases"));
      pluginElement(root.evalNode("plugins"));
      objectFactoryElement(root.evalNode("objectFactory"));
      objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
      reflectorFactoryElement(root.evalNode("reflectorFactory"));
      settingsElement(settings);
      // read it after objectFactory and objectWrapperFactory issue #631
      environmentsElement(root.evalNode("environments"));
      databaseIdProviderElement(root.evalNode("databaseIdProvider"));
      typeHandlerElement(root.evalNode("typeHandlers"));
      mapperElement(root.evalNode("mappers"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    }
  }

进行的。

这里需要关注一下的是mapperElement,因为在Mybatis的xml配置中,mapper的配置方式有两类四种:

  • 第一类是通过XML,
  • 另外一类是通过java的interface,
  • 两种类型的配置可以混合。

XML的配置是通过:XMLMapperBuilder进行解析处理的。
Interface的配置是通过:MapperAnnotationBuilder进行解析处理的。

<!-- 1. Using classpath relative resources -->
<mappers>
  <mapper resource="org/mybatis/builder/AuthorMapper.xml"/>
  <mapper resource="org/mybatis/builder/BlogMapper.xml"/>
  <mapper resource="org/mybatis/builder/PostMapper.xml"/>
</mappers>

<!-- 2. Using url fully qualified paths -->
<mappers>
  <mapper url="file:///var/mappers/AuthorMapper.xml"/>
  <mapper url="file:///var/mappers/BlogMapper.xml"/>
  <mapper url="file:///var/mappers/PostMapper.xml"/>
</mappers>

<!-- 3. Using mapper interface classes -->
<mappers>
  <mapper class="org.mybatis.builder.AuthorMapper"/>
  <mapper class="org.mybatis.builder.BlogMapper"/>
  <mapper class="org.mybatis.builder.PostMapper"/>
</mappers>

<!-- 4. Register all interfaces in a package as mappers -->
<mappers>
  <package name="org.mybatis.builder"/>
</mappers>

XMLMapperBuilder解析xml配置的时候,首先会处理namespace,然后依次处理:

  • cache-ref

    1. 读取出cache-ref中的namespace,然后通过CacheRefResolverresolveCacheRef()方法处理
    2. resolveCacheRef()将处理逻辑代理到MapperBuilderAssistant

        private final MapperBuilderAssistant assistant;
        public Cache resolveCacheRef() {
          return assistant.useCacheRef(cacheRefNamespace);
        }
    3. MapperBuilderAssistant则是从ConfigurationgetCache(namespace)

      public Cache useCacheRef(String namespace) {
        if (namespace == null) {
          throw new BuilderException("cache-ref element requires a namespace attribute.");
        }
        try {
          unresolvedCacheRef = true;
          Cache cache = configuration.getCache(namespace);
          if (cache == null) {
            throw new IncompleteElementException("No cache for namespace '" + namespace + "' could be found.");
          }
          currentCache = cache;
          unresolvedCacheRef = false;
          return cache;
        } catch (IllegalArgumentException e) {
          throw new IncompleteElementException("No cache for namespace '" + namespace + "' could be found.", e);
        }
      }
 与其他的配置类似,最终的信息都会汇总到Configuration对象中去。
  • cache

    1. 读取xml中的配置
    2. 通过builderAssistant完成Cache对象的创建,并将新创建出来的Cache对象添加到Configuration中。

      builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
  • parameterMap
    Deprecated
  • resultMap

    1. 抓出xml文件中,所有的resultMap,逐个进行解析
    2. 在处理每一个resultMap的时候,XMLMapperBuilder从xml中读取resultMap的各项属性配置,然后同样是将处理代理到builderAssistant完成resultMap的创建
    3. 在获得resultMap对象之后,因为resultMap是可以extends其他的resultMap的,且被继承的resultMap有可能已经解析,也有可能还没有解析,此时,ResultMapResolver负责处理这个问题,而ResultMapResolver又将问题抛给builderAssistant来完成最终的处理。
    4. builderAssistant

      • 如果parentResultMap还没有加载完成,Mybatis会抛出异常

      IncompleteElementException("Could not find a parent resultmap with id '" + extend + "'");

      XMLMapperBuilder会捕获IncompleteElementException,并将ResultMapResolver添加到Configuration中:configuration.addIncompleteResultMap

      • 如果parentResultMap已经加载完成,

        1. 则使用subResultMapconstructor替换parentResultMapconstructor
        2. 合并subResultMapparentResultMap
        3. 通过ResultMap.Builder完成ResultMap实例的创建,并将其添加到Configuration
  • sql

This element can be used to define a reusable fragment of SQL code that can be included in other statements. It can be statically (during load phase) parametrized. Different property values can vary in include instances. For example:

<sql id="userColumns"> ${alias}.id,${alias}.username,${alias}.password </sql>

The SQL fragment can then be included in another statement, for example:

<select id="selectUsers" resultType="map">
  select
    <include refid="userColumns"><property name="alias" value="t1"/></include>,
    <include refid="userColumns"><property name="alias" value="t2"/></include>
  from some_table t1
    cross join some_table t2
</select>

从上述可以看出,sql配置的解析应该是比较简单:将sql作为应该XNode,以(Id,XNode)的形式,存储到XMLMapperBuilder.sqlFragments中。

  • select|Insert|update|delete

select|Insert|update|delete是最为复杂的配置项,Mybatis将其处理交由XMLStatementBuilder进行处理。

XMLStatementBuilderparseStatementNode()中,包括以下的处理:

  • 简单配置项的读取
  • 通过XMLIncludeTransformer.applyIncludes完成对include节点的处理
  • 处理selectKey
  • 处理完includeselectKey,Mybatis会将初始的includeselectKey标签进行替换或者移除,然后通过LanguageDriver完成对SQL语句的处理,返回一个SqlSource的实例。

    默认的LanguageDriverXMLLanguageDriver,其将SQL语句的解析处理代理给XMLScriptBuilder.parseScriptNode()

    public SqlSource parseScriptNode() {
      List<SqlNode> contents = parseDynamicTags(context);
      MixedSqlNode rootSqlNode = new MixedSqlNode(contents);
      SqlSource sqlSource = null;
      if (isDynamic) {
        sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
      } else {
        sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
      }
      return sqlSource;
    }
在`parseDynamicTags`中返回的`List<SqlNode>`,包含以下几种`SqlNode`

| tag       | SqlNode           | handler          |
| --------- | ----------------- | ---------------- |
| ${}       | TextSqlNode       | N/A              |
| trim      | TrimSqlNode       | TrimHandler      |
| where     | WhereSqlNode      | WhereHandler     |
| set       | SetSqlNode        | SetHandler       |
| foreach   | ForEachSqlNode    | ForEachHandler   |
| if        | IfSqlNode         | IfHandler        |
| choose    | ChooseSqlNode     | ChooseHandler    |
| when      | IfSqlNode         | IfHandler        |
| otherwise | MixedSqlNode      | OtherwiseHandler |
| bind      | VarDeclSqlNode    | BindHandler      |
| 普通        | StaticTextSqlNode | N/A              |
  • 在获得SqlSource对象之后,Mybatis更加配置初始化KeyGenerator
  • 最后通过MappedStatement.Builder生成MappedStatement,并将其添加到Configuration中。

到此为止,一个mapper的xml配置已经处理完成,接下来就是要把namespace所对应的java class通过configuration.addMapper添加到Configuration上去。

最后处理,分别调用pending的resolver:

parsePendingResultMaps();
parsePendingCacheRefs();
parsePendingStatements();
3.1.2.2 Annotation

那么,现在还有一个问题没有解决,那就是Mapper的定义,可以通过XML的方式,也可以通过Annotation的方式,或者是两种混合使用,我们已经清楚了XML方式的配置是怎么处理的,那么Annotation是在哪里处理的,是怎么处理的呢?

当调用configuration.addMapper的时候,Mybatis会代理到MapperRegistry,进而通过MapperAnnotationBuilder来完成Annotation的处理,Mybatis支持的Annotation如下表:

Annotation Target XML equivalent
@CacheNamespace Class <cache>
@Property N/A <property>
@CacheNamespaceRef Class <cacheRef>
@ConstructorArgs Method <constructor>
@Arg N/A <arg>``
@TypeDiscriminator Method <discriminator>
@Case N/A <case>
@Results Method <resultMap>
@Result N/A <result>``
@One N/A <association>
@Many N/A <collection>
@MapKey Method
@Options Method Attributes of mapped statements.
@Insert @Update @Delete @Select Method <insert> <update> <delete> <select>
@InsertProvider @UpdateProvider @DeleteProvider @SelectProvider Method <insert> <update> <delete> <select>
@Param Parameter N/A
@SelectKey Method <selectKey>
@ResultMap Method N/A
@ResultType Method N/A
@Flush Method N/A

当MapperAnnotationBuilder开始处理Annotation之前,MapperAnnotationBuilder会通过configuration.isResourceLoaded(resource)检查以Mapper为namespace的相关资源有没有被加载,如果没有被加载过,则先加载并解析XML的配置,然后处理Cache及CacheRef,因为这两个Annotation是在Type层面的,所以优先处理,处理的方式和XML没有太多的区别。

接下来遍历Type上所有的methods

  1. 处理ParameterType

如果是单个parameter,则ParameterType为该参数的Type,否则,则为ParamMap.class

  1. 获取LanguageDriver
  2. 通过LanguageDriver获取SqlSource

    1. 获取@Insert/@Update/@Delete/@Select
    2. 获取@InsertProvider/@UpdateProvider/@DeleteProvider/@SelectProvider
    3. 两种注解不能同时出现,只能二选一,
    4. 如果是第1种,则:

      1. sqlAnnotation中获取sql语句,结果为一个String类型的数组
      2. 用空格连接String数组
      3. 通过LanguageDriver创建SqlSource

        • 如果在sql语句中有<script>,则通过XMLScriptBuilder来处理SQL。
        • 如果没有<script>,则通过PropertyParser.parse,然后通过创建SqlSource
    5. 如果是第2种,则返回一个ProviderSqlSource的实例。
  3. 如果sql语句类型是INSERT或者是UPDATE,则根据SelectKeyOptions这两个Annotation确定KeyGenerator
  4. 处理@ResultMap,将ResultMapvalue,以,拼接到一起,形成resultMapId
  5. 如果是SELECT,则通过调用resultMapId = parseResultMap(method);

    1. 获取returnTypeConstructorArgsResultsTypeDiscriminator
    2. 如果Results有指定id的话,则
      resultMapId=type.getName() + "." + results.id()

    如果没有指定的话,则
    resultMapId=type.getName() + "." + method.getName() + suffix

    1. 将ConstructorArg转为ResultMapping对象。
    2. 处理Results,将其转为ResultMapping对象。
    3. 处理Discriminator
  6. 最终通过MapperBuilderAssistant得到MappedStatement对象。

3.2 数据库操作的执行过程

到目前为止,无论是XML的配置,还是Annotation的配置,都已经加载完成了,接下来就可以对mapper的方法进行调用了。

  1. 通过SqlSession获取Mapper的代理实例:

      //1. DefaultSqlSession.java
      public <T> T getMapper(Class<T> type) {
        return configuration.<T>getMapper(type, this);
      }
    
      //2. Configuration.java
      public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
        return mapperRegistry.getMapper(type, sqlSession);
      }
    
      //3. MapperRegistry.java
      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);
        }
      }
  2. 从上面的代码可以看出,我们得到的Mapper的对象,是一个MapperProxy的实例,MapperProxy的声明如下:

    public class MapperProxy<T> implements InvocationHandler, Serializable {...}

当我们执行一个方法时,其执行过程是这样的:

  1. MapperProxy尝试从methodCache中获取MapperMethod实例,如果没有,则创建一个实例,并添加到methodCache中。
  2. 执行mapperMethod.execute(sqlSession, args);
  3. MapperMethod包含两个实例:

    • SqlCommand
      通过MappedStatement获取类型和MappedStatement的ID
    • MethodSignature

      private final boolean returnsMany;
      private final boolean returnsMap;
      private final boolean returnsVoid;
      private final boolean returnsCursor;
      private final Class<?> returnType;
      private final String mapKey;
      private final Integer resultHandlerIndex;
      private final Integer rowBoundsIndex;
      private final ParamNameResolver paramNameResolver;
  在执行execute方法时,

  1. 通过SqlCommand判断是INSERT/UPDATE/DELETE/SELECT/FLUSH,下面以INSERT为例。
  2. 通过MethodSignature的ParamNameResolver获取到参数对象,会有三种情况:
     * 没有任何参数时,返回null
     * 如果只有一个参数,则是参数本身
     * 如果有多个参数,则返回一个Map
  3. 执行`rowCountResult(sqlSession.insert(command.getName(), param))`
  1. DefaultSqlSession通过MappedStatement的ID,从Configuration中获取到MappedStatement,然后交由Executor执行。
  2. 在配置Mybatis的时候,我们可以通过defaultExecutorType配置Executor为SIMPLE/REUSE/ BATCH,默认为SIMPLE。

    1. 清理本地缓存
    2. 通过MappedStatement获取到Configuration对象,通过:

      configuration.newStatementHandler(this, ms, parameter, RowBounds.DEFAULT, null, null);
     获取到StatementHandler对象,此时得到的对象为RoutingStatementHandler,该对象会根据MappedStatement的类型,将实际的操作代理给`SimpleStatementHandler/PreparedStatementHandler(默认)/CallableStatementHandler`。

     在创建PreparedStatementHandler的时候,会通过MappedStatement.getBoundSql->sqlSource.getBoundSql(parameterObject)得到BoundSql。

     最后,将所有的interceptor实例plugin到这个StatementHandler上去。

     plugin的动作,其实就是对这个StatementHandler进行逐层的动态代理。

  3. Mybatis封装了一个Transaction对象,从实现类JdbcTransaction中获取Connection,同时设定TransactionIsolation:
  protected void openConnection() throws SQLException {
    if (log.isDebugEnabled()) {
      log.debug("Opening JDBC Connection");
    }
    connection = dataSource.getConnection();
    if (level != null) {
      connection.setTransactionIsolation(level.getLevel());
    }
    setDesiredAutoCommit(autoCommmit);
  }
  4. 调用StatementHandler的prepare方法,获取到Statement对象:
    //1. 创建Statement实例
    stmt = handler.prepare(connection, transaction.getTimeout());
    //2. 添加参数
    handler.parameterize(stmt);
    /*
    1. 通过boundSql获取到List<ParameterMapping>
    2. 逐个遍历,通过相对应的TypeHandler往Statement上添加参数。
       parameterMapping.getTypeHandler();
    */
  //PreparedStatementHandler.java,创建Statement实例,
  //最终还是通过JDBC的connection对象创建处理PreparedStatement对象。
  protected Statement instantiateStatement(Connection connection) throws SQLException {
    String sql = boundSql.getSql();
    if (mappedStatement.getKeyGenerator() instanceof Jdbc3KeyGenerator) {
      String[] keyColumnNames = mappedStatement.getKeyColumns();
      if (keyColumnNames == null) {
        return connection.prepareStatement(sql, PreparedStatement.RETURN_GENERATED_KEYS);
      } else {
        return connection.prepareStatement(sql, keyColumnNames);
      }
    } else if (mappedStatement.getResultSetType() != null) {
      return connection.prepareStatement(sql, mappedStatement.getResultSetType().getValue(), ResultSet.CONCUR_READ_ONLY);
    } else {
      return connection.prepareStatement(sql);
    }
  }
  5. 调用Handler的update/query方法执行:
  public int update(Statement statement) throws SQLException {
    PreparedStatement ps = (PreparedStatement) statement;
    ps.execute();
    int rows = ps.getUpdateCount();
    Object parameterObject = boundSql.getParameterObject();
    KeyGenerator keyGenerator = mappedStatement.getKeyGenerator();
    keyGenerator.processAfter(executor, mappedStatement, ps, parameterObject);
    return rows;
  }

  public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
    PreparedStatement ps = (PreparedStatement) statement;
    ps.execute();
    return resultSetHandler.<E> handleResultSets(ps);
  }
     这两个方法的区别在于,update需要对key进行处理,而query则需要对resultSet进行处理。

     * Jdbc3KeyGenerator

       1. 通过PreparedStatement.getGeneratedKeys
       2. 获取配置中的keyProperties
       3. 获取相对应类型的TypeHandler,将TypeHandler处理过的值设置到对象中去。

     * SelectKeyGenerator

       1. 通过SimpleExecutor执行KeyStatement获取到Key的值
       2. 将值设置到Parameter对象中去

     * ResultHandler

       执行SELECT的时候,MapperMethod需要根据返回值的类型,选择不同的执行策略:
if (method.returnsVoid() && method.hasResultHandler()) {
  executeWithResultHandler(sqlSession, args);
  //使用指定的resultHandler或者null
  result = null;
} else if (method.returnsMany()) {
  result = executeForMany(sqlSession, args);
  //executor.query时传入的resultHandler为null
} else if (method.returnsMap()) {
  result = executeForMap(sqlSession, args);
  //executor.query时传入的resultHandler为null
  //返回前,通过DefaultMapResultHandler对结果进行处理
} else if (method.returnsCursor()) {
  result = executeForCursor(sqlSession, args);
  //executor.query时传入的resultHandler为null
} else {
  Object param = method.convertArgsToSqlCommandParam(args);
  result = sqlSession.selectOne(command.getName(), param);
  //executor.query时传入的resultHandler为null
}
       在Executor执行是,如果传入的ResultHandler为null,则尝试从localCache获取结果,如果结果为null才会从Database中查询。

       SimpleExecutor在doQuery时,会通过`configuration.newStatementHandler`时,会通过
       `configuration.newResultSetHandler(executor, mappedStatement, rowBounds, parameterHandler, resultHandler, boundSql);`

       创建ResultSetHandler,将resultHandler作为传入参数。

       ResultSetHandler在其方法:
public List<Object> handleResultSets(Statement stmt) throws SQLException {...}
       中,如果发现resultHandler为null,则会new DefaultResultHandler的实例,对result进行处理。

四、Mybatis中有哪些优秀的设计及值得借鉴的地方?

相关文章
|
SQL Java 数据库连接
|
7月前
|
SQL Java 数据库连接
|
SQL 算法 Java
Mybatis-plus超详细讲解(2022)
MyBatis-Plus(简称 MP)是一个 MyBatis 的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。 我们的愿景是成为 MyBatis 最好的搭档,就像 魂斗罗 中的 1P、2P,基友搭配,效率翻倍。
3697 1
|
2月前
|
SQL 缓存 Java
MyBatis系列
MyBatis系列
|
2月前
|
SQL 缓存 Java
Mybatis
【10月更文挑战第5天】Mybatis
33 0
|
4月前
|
SQL XML Java
Mybatis02(二)
Mybatis02(二)
31 0
|
5月前
|
SQL 算法 Java
MyBatis-Plus详解(3)
MyBatis-Plus详解(3)
75 0
|
SQL 安全 Java
Mybatis中# 和 $ 的使用详解
Mybatis中# 和 $ 的使用详解
195 0
|
SQL Java 数据库连接
|
SQL XML 存储
Mybatis总结
Mybatis总结
110 0

热门文章

最新文章