閱讀730 返回首頁    go 阿裏雲 go 技術社區[雲棲]


MyBatis是怎麼玩的

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" "https://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
  <environments default="development">
    <environment >
      <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" "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.mybatis.example.BlogMapper">
  <select  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 />
  <mapper />
  <mapper />
</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. 則使用`subResultMap`的`constructor`替換`parentResultMap`的`constructor`
   2. 合並`subResultMap`和`parentResultMap`
   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 > ${alias}.id,${alias}.username,${alias}.password </sql>
    

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

    <select  resultType="map">
    select
      <include ref><property name="alias" value="t1"/></include>,
      <include ref><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><idArg>
@TypeDiscriminator Method <discriminator>
@Case N/A <case>
@Results Method <resultMap>
@Result N/A <result><id>
@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

  2. 獲取LanguageDriver

  3. 通過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的實例。
  4. 如果sql語句類型是INSERT或者是UPDATE,則根據SelectKeyOptions這兩個Annotation確定KeyGenerator

  5. 處理@ResultMap,將ResultMapvalue,以,拚接到一起,形成resultMapId

  6. 如果是SELECT,則通過調用resultMapId = parseResultMap(method);

    1. 獲取returnTypeConstructorArgsResultsTypeDiscriminator
    2. 如果Results有指定id的話,則 resultMapId=type.getName() + "." + results.id() 如果沒有指定的話,則 resultMapId=type.getName() + "." + method.getName() + suffix
    3. 將ConstructorArg轉為ResultMapping對象。
    4. 處理Results,將其轉為ResultMapping對象。
    5. 處理Discriminator
  7. 最終通過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

    ```java
        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需要根據返回值的類型,選擇不同的執行策略:
    
       ```java
       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在其方法:
    
       ```java
       public List<Object> handleResultSets(Statement stmt) throws SQLException {...}
       ```
    
       中,如果發現resultHandler為null,則會new DefaultResultHandler的實例,對result進行處理。
    

四、Mybatis中有哪些優秀的設計及值得借鑒的地方?

最後更新:2017-07-04 20:02:56

  上一篇:go  網站不穩定關服務器什麼事?
  下一篇:go  源碼編譯更新nginx到最新版本,並開始nginx支持http2協議模塊.