Hand-on instructions to read the mybatis core source code, grasp the underlying working principles and design ideas

Mybatis is currently the preferred ORM framework for the Java system of Internet companies. It has natural advantages. Many students only focus on the writing of CRUD programs for their company business, ignoring the importance of source reading.Here's a code sample written using the Mybatis API:

String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession session = sqlSessionFactory.openSession();
try {
    BusinessMapper mapper = session.getMapper(BusinessMapper.class);
    Business business = mapper.selectBusinessById(1);
    System.out.println(business);
    
}finally {
    session.close();
}

Next, follow the steps of the sample code step by step to analyze the secrets behind the code and reveal the true face of the mybatis source code.The given source code fragments have Chinese comments for students to further understand.

1. Global configuration resolution process

1.1 SqlSessionFactoryBuilder (Build Factory Class)

SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

Create a new SqlSessionFactoryBuilder object using the Builder mode.The build() method was called to create the SqlSessionFactory object, and there are nine overloaded build() methods in the SqlSessionFactoryBuilder that can be used in different ways to create the SqlSessionFactory object, which defaults to the singleton mode.

1.2 XmlConfigBuilder (parsing global configuration file)

Create an XmlConfigBuilder object to parse the global configuration file and return a Configuration object when parsing is complete.

public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
    try {
      //1. Create an XMLConfigBuilder object
      XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
      //2. Call parsing method to return Configuration object
      return build(parser.parse());
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error building SqlSession.", e);
    } finally {
      ErrorContext.instance().reset();
      try {
        reader.close();
      } catch (IOException e) {
        // Intentionally ignore. Prefer previous error.
      }
    }
  }

XMLConfigBuilder inherits from the abstract class BaseBuilder and parses the global configuration file. BaseBuilder also has subclasses to create different targets.For example:

  • XMLMapperBuilder: Parse Mapper Mapper
  • XMLStatementBuilder: Parse add-delete check labels
  • XMLScriptBuilder: Parse dynamic SQL

Next, look at the source code for the parse() method called by the XMLConfigBuilder object:

 public Configuration parse() {
    if (parsed) {
      throw new BuilderException("Each XMLConfigBuilder can only be used once.");
    }
    parsed = true;
    // XPathParser, dom, and SAX are all useful >>
    parseConfiguration(parser.evalNode("/configuration"));
    return configuration;
  }

There are many ways to parse xml configuration files in java. mybatis encapsulates DOM and SAX differently.Next, look at the parseConfiguration() method.

private void parseConfiguration(XNode root) {
    try {
      //issue #117 read properties first
      // Resolution of various labels for global profiles
      1,analysis<properties>Tags, which can read externally introduced property files, such as database.properties
      propertiesElement(root.evalNode("properties"));
      // 2. Resolve settings Tags
      Properties settings = settingsAsProperties(root.evalNode("settings"));
      //3. Get the Virtual File System custom implementation class, such as reading local files
      loadCustomVfs(settings);
      //4. Obtain log implementation class from <longImpl>tag
      loadCustomLogImpl(settings);
      //5. Resolve Type Alias
      typeAliasesElement(root.evalNode("typeAliases"));
      //6. Resolve plugins tags, such as PageHelper
      pluginElement(root.evalNode("plugins"));
      // Used to create objects
      objectFactoryElement(root.evalNode("objectFactory"));
      // Used for processing objects
      objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
      // Reflection toolbox
      reflectorFactoryElement(root.evalNode("reflectorFactory"));
      // settings subtag assignment, default value is provided here >>
      settingsElement(settings);
      // read it after objectFactory and objectWrapperFactory issue #631
      // Created Data Source >>
      environmentsElement(root.evalNode("environments"));
      // Parse the databaseIdProvider tag and generate the DatabaseIdProvider object
      databaseIdProviderElement(root.evalNode("databaseIdProvider"));
      // Used for mapping, the JavaType and JdbcType are obtained and stored in the TypeHandlerRegistry object
      typeHandlerElement(root.evalNode("typeHandlers"));
      // Resolve Referenced Mapper
      mapperElement(root.evalNode("mappers"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    }
  }

All values in the above method are encapsulated in the Configuration object.Below is a sequence diagram of the creation process.

2. Session Creation Process

SqlSession session = sqlSessionFactory.openSession();

This actually calls the openSessionFromDataSource() method of the DefaultSqlSessionFactory class.

private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
    Transaction tx = null;
    try {
      final Environment environment = configuration.getEnvironment();
      // Get Transaction Factory
      final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
      // Create Transaction
      tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
      // Create executors > according to the transaction factory and default executor type
      final Executor executor = configuration.newExecutor(tx, execType);
      return new DefaultSqlSession(configuration, executor, autoCommit);
    } catch (Exception e) {
      closeTransaction(tx); // may have fetched a connection so lets call close()
      throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }

2.1 Get the Environment object

Gets the Environment object from the Configuration object, which has a Transaction Factory class;

public final class Environment {
  private final String id;
  private final TransactionFactory transactionFactory;
  private final DataSource dataSource;
}

2.2 Create Transactions

Gets a TranscationFactory object from the Environment object, and the transaction factory type can be configured as JDBC or MANAGED.

  • JDBC; uses jdbc's Connection object to manage transactions;
  • MANAGED: Transactions will be managed by containers.
 private TransactionFactory getTransactionFactoryFromEnvironment(Environment environment) {
    if (environment == null || environment.getTransactionFactory() == null) {
      return new ManagedTransactionFactory();
    }
    return environment.getTransactionFactory();
 }

2.3 Create Executor

final Executor executor = configuration.newExecutor(tx, execType);

There are three basic types of executor Executor:

  • SIMPLE (default)
  • BATCH
  • REUSE
if (ExecutorType.BATCH == executorType) {
      executor = new BatchExecutor(this, transaction);
    } else if (ExecutorType.REUSE == executorType) {
      executor = new ReuseExecutor(this, transaction);
    } else {
      // Default SimpleExecutor
      executor = new SimpleExecutor(this, transaction);
}

The abstract class BaseExecutor implements the Executor interface, which is a reflection of the template design pattern.

Cache Decoration

In the newExecutor() method:

// Level 2 cache switch, cacheEnabled in settings defaults to true
if (cacheEnabled) {
  executor = new CachingExecutor(executor);
}

Proxy Plugin

// Implant plug-in logic so that all four objects have been intercepted
executor = (Executor) interceptorChain.pluginAll(executor);

2.4 Returns SqlSession

The SqlSession class includes two objects, Configuration and Executor.Below is a sequence diagram of the creation process.

3. Obtaining Agents

The name in the interface corresponds to the namespace in the apper.xml file, and the method name corresponds to the StatementId.

BusinessMapper mapper = session.getMapper(BusinessMapper.class);
Business business = mapper.selectBusinessById(1);
<mapper namespace="com.sy.mapper.BusinessMapper">

<select id="selectBusinessById" resultMap="BaseResultMap" statementType="PREPARED" >
        select * from bsuiness where bid = #{bid}
    </select>

3.1 getMapper() method

1. The **getMapper()** method in DefaultSqlSession:

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

2. The getMapper() method in the Configuration class:

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

3. The getMapper() method in MapperRegistry:

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

When parsing mapper tags and Mapper.xml, MapperProxyFactory corresponding to interface type and type has been put into a map to get the proxy object of Mapper, which is actually created by JDK dynamic proxy after getting the corresponding factory class from the map.

protected T newInstance(MapperProxy<T> mapperProxy) {
    // 1: Class loader;
    // 2: The interface implemented by the proxy class;
    // 3: Implement trigger management class for InvocationHandler
    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 implements the InvocationHandler interface with sqlSession, mapperInterface, methodCache parameters, and eventually creates a return proxy object (type is a $Proxy number) through the dynamic proxy of the JDK.This object inherits the Proxy class, the interface on which the instance is proxied, and holds a trigger management class of type MapperProxy.

3.2 MapperProxy implements a proxy for an interface

The dynamic proxy for JDK has three core roles:

  • Proxy class (implementation class)
  • Interface
  • Implement trigger management class for InvocationHandler

Used to generate proxy objects.The class being proxied must implement the interface, because methods are acquired through the interface and the proxy class implements the interface. Mapper inside MyBatis does not implement classes, it simply ignores the implementation classes and proxies the interfaces directly.

The process of getting a Mapper object is actually getting a JDK dynamic proxy object.This proxy class inherits the Proxy class and implements the proxied interface, which holds a trigger management class of type MapperProxy.Take a look at the time series diagram of the proxy class process.

4. Executing SQL

Business business = mapper.selectBusinessById(1);

4.1 MapperProxy.invoke() method

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
      // Methods such as toString hashCode equals getClass do not require walking to the process of executing SQL
      if (Object.class.equals(method.getDeclaringClass())) {
        return method.invoke(this, args);
      } else {
        // Increase the efficiency of getting mapperMethod to invoke MapperMethodInvoker
        // The normal method goes to invoke of PlainMethodInvoker
        return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
      }
    } catch (Throwable t) {
      throw ExceptionUtil.unwrapThrowable(t);
    }
  }

4.1.1. First decide whether to execute SQL or directly

Object's own method and the default method in Java 8 do not require SQL execution

4.1.2, Get Cache

To speed up MapperMethod acquisition when caching here

 private MapperMethodInvoker cachedInvoker(Method method) throws Throwable {
    try {
      // Map's method in Java8, gets a value based on the key, and assigns the value of the following Object to the key if the value is null
      // If not, create
      // Getting the MapperMethodInvoker object, there is only one invoke method
      return methodCache.computeIfAbsent(method, m -> {
        if (m.isDefault()) {
          // The default method of the interface (Java8), which inherits the default method of the interface whenever it is implemented, such as List.sort()
          try {
            if (privateLookupInMethod == null) {
              return new DefaultMethodInvoker(getMethodHandleJava8(method));
            } else {
              return new DefaultMethodInvoker(getMethodHandleJava9(method));
            }
          } catch (IllegalAccessException | InstantiationException | InvocationTargetException
              | NoSuchMethodException e) {
            throw new RuntimeException(e);
          }
        } else {
          // Create a MapperMethod
          return new PlainMethodInvoker(new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
        }
      });
    } catch (RuntimeException re) {
      Throwable cause = re.getCause();
      throw cause == null ? re : cause;
    }
  }

Map's computeIfAbsent() method: Assign the value of the following Object to the key only if the key does not exist or the value is null.Default methods for interfaces in Java8 and Java9 are handled specially and return DefaultMethodInvoker objects.The normal method returns PlainMethodInvoker, MapperMethod.

There are two more important properties in the MaprMethod object:

 // statement id (for example, com.sy.mapper.BusinessMapper.selectBusinessById) and SQL type
  private final SqlCommand command;
  // Method signature, primarily the type of return value
  private final MethodSignature method;

4.2 MappperMethod.execute() method

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);
          // Execution Entry of Common select Statement >>
          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;
  }

Depending on the type (INSERT, UPDATE, DELETE, SELECT) and return type:

  • ConvArgsToSqlCommandParam() is called to convert the method's parameters to those of SQL.
  • The insert(), update(), delete(), selectOne() methods of sqlSession are called.

The following focuses on the selectOne() method of the query.The selectOne() method of DefaultSqlSession was called.

4.3 DefaultSqlSession.selectOne() method

public <T> T selectOne(String statement, Object parameter) {
    // Going to DefaultSqlSession
    // 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;
    }
  }

In SelectList(), we first get the MappedStatement from Configuration based on commandname (StatementID), which has all the properties we configure in the xml, including id, statementType, sqlSource, useCache, input, output, and so on.

public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
    try {
      MappedStatement ms = configuration.getMappedStatement(statement);
      // If cacheEnabled = true (default), Executor will be decorated with CachingExecutor
      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();
    }
  }

Then executor.query(), as we mentioned earlier, there are three basic types of Executor, SIMPLE/REUSE/BATCH, and one packaging type, CachingExecutor.So which executor would you choose here?We'll go back and see how DefaultSqlSession was assigned at initialization, which is our session creation process.If secondary caching is enabled, the query() method of CachingExecutor is called first, with cache-related operations in it, and then the basic type of executor, such as the default SimpleExecutor, is called.Without opening the secondary cache, you will first go to the query() method of BaseExecutor (otherwise you will go to CachingExecutor).

4.4 CachingExector.query() method

public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    // Get SQL
    BoundSql boundSql = ms.getBoundSql(parameterObject);
    // Create CacheKey: What kind of SQL is the same SQL?>
    CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
    return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

4.4.1, Create CacheKey

How does the CacheKey of a secondary cache consist?In other words, what kind of query can you determine is the same query?The createCacheKey() method in BaseExector uses six elements:

public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
    if (closed) {
      throw new ExecutorException("Executor was closed.");
    }
    CacheKey cacheKey = new CacheKey();
    cacheKey.update(ms.getId()); 
    cacheKey.update(rowBounds.getOffset()); // 0
    cacheKey.update(rowBounds.getLimit()); // 2147483647 = 2^31-1
    cacheKey.update(boundSql.getSql());
    List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
    TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
    // mimic DefaultParameterHandler logic
    for (ParameterMapping parameterMapping : parameterMappings) {
      if (parameterMapping.getMode() != ParameterMode.OUT) {
        Object value;
        String propertyName = parameterMapping.getProperty();
        if (boundSql.hasAdditionalParameter(propertyName)) {
          value = boundSql.getAdditionalParameter(propertyName);
        } else if (parameterObject == null) {
          value = null;
        } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
          value = parameterObject;
        } else {
          MetaObject metaObject = configuration.newMetaObject(parameterObject);
          value = metaObject.getValue(propertyName);
        }
        cacheKey.update(value); // development
      }
    }
    if (configuration.getEnvironment() != null) {
      // issue #176
      cacheKey.update(configuration.getEnvironment().getId());
    }
    return cacheKey;
  }

That is, the same method, same page offset, the same SQL, the same parameter values, the same data source environment will be identified as the same query.

Notice the properties of the CacheKey class, where a List stores the six elements in order.

 private static final int DEFAULT_MULTIPLIER = 37;
  private static final int DEFAULT_HASHCODE = 17;

  private final int multiplier;
  private int hashcode;
  private long checksum;
  private int count;
  // 8/21/2017 - Sonarlint flags this as needing to be marked transient.  While true if content is not serializable, this is not always true and thus should not be marked transient.
  private List<Object> updateList;

How do I compare two CacheKey s to be equal?It is not efficient to compare six features in turn if they are equal at first, which is more than six times.Each class inherits from Object and has a hashCode() method for generating hash codes.It is used to quickly weight a set.

When cacheKey is generated, the update() method is called, and cacheKey's hashCode is updated, which is generated by a multiplicative hash.

public void update(Object object) {
    // Additive Hash
    int baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object);

    count++;
    checksum += baseHashCode;
    baseHashCode *= count;
    // 37 * 17 + 
    hashcode = multiplier * hashcode + baseHashCode;

    updateList.add(object);
  }

The hashCode() method in Object is a local method generated by a random number algorithm (OpenJDK8 is default and can be modified by -XX:hashCode).The hashCode() method in CacheKey is overridden to return to generating a new hashCode.

Why do you need 37 as a multiplier?This is an empirical value, similar to 31 in the String class.Look at the hashCode() method source code in the String class:

public int hashCode() {
    int h = hash;
    if (h == 0 && value.length > 0) {
        char val[] = value;

        for (int i = 0; i < value.length; i++) {
            //31 as multiplication factor
            h = 31 * h + val[i];
        }
        hash = h;
    }
    return h;
}

The equals() method in CacheKey is also overridden to compare whether cacheKey is equal.

@Override
public boolean equals(Object object) {
    // Same object
    if (this == object) {
      return true;
    }
    // The object being compared is not a CacheKey
    if (!(object instanceof CacheKey)) {
      return false;
    }
    
    final CacheKey cacheKey = (CacheKey) object;
    
    // hashcode inequality
    if (hashcode != cacheKey.hashcode) {
      return false;
    }
    // checksum is not equal
    if (checksum != cacheKey.checksum) {
      return false;
    }
    // count is not equal
    if (count != cacheKey.count) {
      return false;
    }
    
    for (int i = 0; i < updateList.size(); i++) {
      Object thisObject = updateList.get(i);
      Object thatObject = cacheKey.updateList.get(i);
      if (!ArrayUtil.equals(thisObject, thatObject)) {
        return false;
      }
    }
    return true;
}

If the hash value (multiplicative hash), check value (additive hash), any one of the number of features is not equal, they are not the same query, and finally they are recycled to compare features to prevent hash collisions.

After the CacheKey is generated, the query() method of the CachingExecutor class is called.

public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    // Get SQL
    BoundSql boundSql = ms.getBoundSql(parameterObject);
    // Create CacheKey: What kind of SQL is the same SQL?>
    CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
    return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

4.4.2, Processing secondary caches

@Override
  public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
      throws SQLException {
    Cache cache = ms.getCache();
    // Where was the cache object created?XMLMapperBuilder class xmlconfigurationElement()
    // Determined by <cache>tag
    if (cache != null) {
      // flushCache="true" Empty Level 1 and Level 2 caches >>
      flushCacheIfRequired(ms);
      if (ms.isUseCache() && resultHandler == null) {
        ensureNoOutParams(ms, boundSql);
        // Get secondary cache
        // Cache is managed through TransactionalCache Manager, TransactionalCache
        @SuppressWarnings("unchecked")
        List<E> list = (List<E>) tcm.getObject(cache, key);
        if (list == null) {
          list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
          // Write to Level 2 Cache
          tcm.putObject(cache, key, list); // issue #578 and #116
        }
        return list;
      }
    }
    // Go to SimpleExecutor | ReuseExecutor | BatchExecutor
    return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }

First, the getCache() method is called from the MappedStatement object to determine if the object is empty. If it is empty, there is no process to query and write to the secondary cache.

So when was the Cache object created?For parsing Mapper.xml The XMLMapperBuilder class of the configurationElement() method calls the cacheElement() method:

cacheElement(context.evalNode("cache"));

only Mapper.xml The cache tag in is not empty before it is resolved.

 private void cacheElement(XNode context) {
    // Resolve only if the cache tag is not empty
    if (context != null) {
      String type = context.getStringAttribute("type", "PERPETUAL");
      Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
      String eviction = context.getStringAttribute("eviction", "LRU");
      Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);
      Long flushInterval = context.getLongAttribute("flushInterval");
      Integer size = context.getIntAttribute("size");
      boolean readWrite = !context.getBooleanAttribute("readOnly", false);
      boolean blocking = context.getBooleanAttribute("blocking", false);
      Properties props = context.getChildrenAsProperties();
      builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
    }
  }

BuilderAssistant.useNewCacheThe () method creates a Cache object.

public Cache useNewCache(Class<? extends Cache> typeClass,
      Class<? extends Cache> evictionClass,
      Long flushInterval,
      Integer size,
      boolean readWrite,
      boolean blocking,
      Properties props) {
    Cache cache = new CacheBuilder(currentNamespace)
        .implementation(valueOrDefault(typeClass, PerpetualCache.class))
        .addDecorator(valueOrDefault(evictionClass, LruCache.class))
        .clearInterval(flushInterval)
        .size(size)
        .readWrite(readWrite)
        .blocking(blocking)
        .properties(props)
        .build();
    configuration.addCache(cache);
    currentCache = cache;
    return cache;
  }

Why should secondary caches be managed with TCM?

Let's think about a question, in one transaction:

  • 1. First insert a piece of data (no submission) and the secondary cache will be emptied.
  • 2. Query data in this transaction and write to the secondary cache.
  • 3. Submit the transaction, have an exception and roll back the data.

This occurs when the database does not have this data, but the secondary cache does.So MyBatis A secondary cache needs to be associated with a transaction.

So why doesn't the first level cache do this?

Because a session is a transaction, the transaction rolls back, the session ends, the cache is empty, does not exist Dirty data in the first level cache read.Secondary caching is cross-session, that is, cross-transaction, so it is possible to have different transaction access to the same method.

4.4.2.1 Write to secondary cache

tcm.putObject(cache, key, list); // issue #578 and #116

Call the putObject() method of TranscationalCacheManager, take out the TransactionalCache object from the map, and add the value to the map to be submitted.At this point the cache has not actually been written.

public void putObject(Cache cache, CacheKey key, Object value) {
    getTransactionalCache(cache).putObject(key, value);
}

private TransactionalCache getTransactionalCache(Cache cache) {
    return transactionalCaches.computeIfAbsent(cache, TransactionalCache::new);
}

Call the putObject() method of TranscationalCache

@Override
  public void putObject(Object key, Object object) {
    entriesToAddOnCommit.put(key, object);
  }

Write to the cache only when the transaction is actually committed.

4.4.2.2 Getting Secondary Cache

List<E> list = (List<E>) tcm.getObject(cache, key);

Remove the Transcational object from the map, which is also a layer-by-layer decorated cache object for PerpetualCache.The getObject() method layer recursively until it reaches the PerpetualCache and gets the value.

@Override
  public Object getObject(Object key) {
    // issue #116
    Object object = delegate.getObject(key);
    if (object == null) {
      entriesMissedInCache.add(key);
    }
    // issue #146
    if (clearOnCommit) {
      return null;
    } else {
      return object;
    }
  }

The getObject() method in PerpetualCache:

@Override
public Object getObject(Object key) {
    return cache.get(key);
}

4.5 BaseExecutor.query() method

4.5.1 Empty Local Cache

QueyStack records the query stack to prevent queries from processing the cache repeatedly when recursive.When flushCache=true, the local cache LocalCache is cleared first.

if (queryStack == 0 && ms.isFlushCacheRequired()) {
      // When flushCache="true", even queries empty the first level cache
      clearLocalCache();
}

If there is no cache, it is queried from the database.Call the **queryFromDatabase()** method.

list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);

LocalCacheScope == STATEMENT, the local cache is emptied.

if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
    // issue #482
    clearLocalCache();
}

4.5.2 Database Query

1. Use placeholders in the cache first.After executing the query, remove the placeholder and put in the data.

2. Execute the Exector's doQuery() method, which defaults to SimpleExector.

 private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    List<E> list;
    // Preemptive Placement
    localCache.putObject(key, EXECUTION_PLACEHOLDER);
    try {
      // The difference between the three Executor s, see doUpdate
      // Default Simple
      list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
    } finally {
      // Remove placeholders
      localCache.removeObject(key);
    }
    // Write Level 1 Cache
    localCache.putObject(key, list);
    if (ms.getStatementType() == StatementType.CALLABLE) {
      localOutputParameterCache.putObject(key, parameter);
    }
    return list;
  }

4.6 SimpleExecutor.query() method

4.6.1 Create StatementHandler

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();
      // Note that you have reached StatementHandler >>the key object for SQL processing
      StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
      // Get a Statement object
      stmt = prepareStatement(handler, ms.getStatementLog());
      // Execute Query
      return handler.query(stmt, resultHandler);
    } finally {
      // Close when used up
      closeStatement(stmt);
    }
  }

Get the RoutingStatementHandler first in configuration.newStatementHandler().RoutingStatementHandler does not have any implementation to create a basic StatementHandler, where the type of StatementHandler is determined based on the statementType inside the MappedStatement.The default is PREPARED (STATEMENT, PREPARED, CALLABLE).

public RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
    // How did StatementType come from?Add or delete statementType="PREPARED" in check label, default value PREPARED
    switch (ms.getStatementType()) {
      case STATEMENT:
        delegate = new SimpleStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
        break;
      case PREPARED:
        // What did you do when you created the StatementHandler?>
        delegate = new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
        break;
      case CALLABLE:
        delegate = new CallableStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
        break;
      default:
        throw new ExecutorException("Unknown statement type: " + ms.getStatementType());
    }

  }

StatementHandler contains ParamterHandler for processing parameters and ResultHandler for processing result sets.Both objects were created at the time of new above.

These two objects are created in the constructor in the BaseStatementHandler class, the parent of StatementHandler.

 protected BaseStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
    this.configuration = mappedStatement.getConfiguration();
    this.executor = executor;
    this.mappedStatement = mappedStatement;
    this.rowBounds = rowBounds;

    this.typeHandlerRegistry = configuration.getTypeHandlerRegistry();
    this.objectFactory = configuration.getObjectFactory();

    if (boundSql == null) { // issue #435, get the key before calculating the statement
      generateKeys(parameterObject);
      boundSql = mappedStatement.getBoundSql(parameterObject);
    }

    this.boundSql = boundSql;

    // Created the other two big objects of the four major objects >>
    // What were you doing when you created these two objects?
    this.parameterHandler = configuration.newParameterHandler(mappedStatement, parameterObject, boundSql);
    this.resultSetHandler = configuration.newResultSetHandler(executor, mappedStatement, rowBounds, parameterHandler, resultHandler, boundSql);
  }

These objects are all four large objects that can be intercepted by plug-ins, so they are wrapped in interceptors after they are created.Intercept calls are made in Configuration.

public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
    ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);
    // Implant plug-in logic (return proxy object)
    parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
    return parameterHandler;
  }

  public ResultSetHandler newResultSetHandler(Executor executor, MappedStatement mappedStatement, RowBounds rowBounds, ParameterHandler parameterHandler,
      ResultHandler resultHandler, BoundSql boundSql) {
    ResultSetHandler resultSetHandler = new DefaultResultSetHandler(executor, mappedStatement, parameterHandler, resultHandler, boundSql, rowBounds);
    // Implant plug-in logic (return proxy object)
    resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler);
    return resultSetHandler;
  }

  public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
    StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
    // Implant plug-in logic (return proxy object)
    statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
    return statementHandler;
  }

There are only three objects, and one more?When was it created?

4.6.2 Create Statement

Create a Statement object with the new StatementHandler.

private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
    Statement stmt;
    Connection connection = getConnection(statementLog);
    // Get the Statement object, and if there is a plug-in wrapper, it will go to the blocked business logic first
    stmt = handler.prepare(connection, transaction.getTimeout());
    // Set parameters for Statement, precompile sql statements, process parameters
    handler.parameterize(stmt);
    return stmt;
}
@Override
public void parameterize(Statement statement) throws SQLException {
    delegate.parameterize(statement);
}

4.6.3 Execute the query() method of StatementHandler

RoutingStatementHandler's query() method, delegate delegation, and finally executes PreparedStatementHandler's query() method.

 @Override
  public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
    return delegate.query(statement, resultHandler);
  }

4.6.4 Execute the query() method of the PreparedStatementHandler

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

4.6.5 ResultHandler Processing Result Set

resultSetHandler.handleResultSets(ps);

How do I convert a ResultSet to List <ObJect>?

ResultSetHandIer has only one implementation class: DefaultResultSetHandler is execution handleResultSets() method of DefaultResultSetHandler.First we get the first result set. If you do not configure a query to return multiple result sets, there is usually only one result set.If we don't use the while loop below, execute it once.The handleResuItSet() method is then called.

@Override
  public List<Object> handleResultSets(Statement stmt) throws SQLException {
    ErrorContext.instance().activity("handling results").object(mappedStatement.getId());

    final List<Object> multipleResults = new ArrayList<>();

    int resultSetCount = 0;
    ResultSetWrapper rsw = getFirstResultSet(stmt);

    List<ResultMap> resultMaps = mappedStatement.getResultMaps();
    int resultMapCount = resultMaps.size();
    validateResultMapsCount(rsw, resultMapCount);
    while (rsw != null && resultMapCount > resultSetCount) {
      ResultMap resultMap = resultMaps.get(resultSetCount);
      handleResultSet(rsw, resultMap, multipleResults, null);
      rsw = getNextResultSet(stmt);
      cleanUpAfterHandlingResultSet();
      resultSetCount++;
    }

    String[] resultSets = mappedStatement.getResultSets();
    if (resultSets != null) {
      while (rsw != null && resultSetCount < resultSets.length) {
        ResultMapping parentMapping = nextResultMaps.get(resultSets[resultSetCount]);
        if (parentMapping != null) {
          String nestedResultMapId = parentMapping.getNestedResultMapId();
          ResultMap resultMap = configuration.getResultMap(nestedResultMapId);
          handleResultSet(rsw, resultMap, null, parentMapping);
        }
        rsw = getNextResultSet(stmt);
        cleanUpAfterHandlingResultSet();
        resultSetCount++;
      }
    }

    return collapseSingleResultList(multipleResults);
  }
private void handleResultSet(ResultSetWrapper rsw, ResultMap resultMap, List<Object> multipleResults, ResultMapping parentMapping) throws SQLException {
    try {
      if (parentMapping != null) {
        handleRowValues(rsw, resultMap, null, RowBounds.DEFAULT, parentMapping);
      } else {
        if (resultHandler == null) {
          DefaultResultHandler defaultResultHandler = new DefaultResultHandler(objectFactory);
          handleRowValues(rsw, resultMap, defaultResultHandler, rowBounds, null);
          multipleResults.add(defaultResultHandler.getResultList());
        } else {
          handleRowValues(rsw, resultMap, resultHandler, rowBounds, null);
        }
      }
    } finally {
      // issue #228 (close resultsets)
      closeResultSet(rsw.getResultSet());
    }
  }

Tags: Programming SQL Session Mybatis xml

Posted on Fri, 15 May 2020 00:37:48 -0400 by daz_effect