preface

This article will explain the creation process of SqlSession object and the implementation of secondary cache from the source point of view.

Build the SqlSession

SqlSessionFactoryBuilder = SqlSessionFactory = SqlSessionFactory = SqlSessionFactory SqlSessionFactory is the factory that produces the SqlSession object, which is the key entry class for the execution phase of Mybatis.

Build the entry SqlSessionFactoryBuilder

Start with the SqlSessionFactoryBuilder class.

There are many overloaded build() methods in SqlSessionFactoryBuilder, but there are two core methods:

  • SqlSessionFactory#build(InputStream inputStream, String environment, Properties properties)
  • SqlSessionFactory#build(Configuration config)

InputStream is a file stream of configuration files. Environment and properties are optional parameters. The build method first generates the XMLConfigBuilder object, then calls the parse() method to parse the stream of configuration files into a Configuration object, and then calls the second Build () method using the Configuration object.

public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
  try {
    // Initialize the resolver
    XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
    // Pass the built config back to build()
    return build(parser.parse());
  } catch (Exception e) {
    throw ExceptionFactory.wrapException("Error building SqlSession.", e);
  } finally {
    ErrorContext.instance().reset();
    try {
      inputStream.close();
    } catch (IOException e) {
      // Intentionally ignore. Prefer previous error.}}}Copy the code

2) SqlSessionFactory#build(Configuration Config) is used to build DefaultSqlSessionFactory after the Configuration object is parsed Object.

public SqlSessionFactory build(Configuration config) {
  return new DefaultSqlSessionFactory(config);
}
Copy the code

Configuration and Configuration Constructor (XmlConfigBuilder)

Mybatis supports Configuration in XML form. XpathParser is a tool class for XML parsing. The specific parsing process is as follows:

public XMLConfigBuilder(InputStream inputStream, String environment, Properties props) {
  this(new XPathParser(inputStream, true, props, new XMLMapperEntityResolver()), environment, props);
}
Copy the code

The XPathParser class calls two methods on initialization: commonConstructor() and createDocument().

public XPathParser(InputStream inputStream, boolean validation, Properties variables, EntityResolver entityResolver) {
  commonConstructor(validation, variables, entityResolver);
  this.document = createDocument(new InputSource(inputStream));
}
Copy the code

After the XPathParser object is created, it is returned to the XMLConfigBuilder construct, passing the created XPathParser object as a parameter to the other constructs of XMLConfigBuilder.

private XMLConfigBuilder(XPathParser parser, String environment, Properties props) {
  super(new Configuration());
  ErrorContext.instance().resource("SQL Mapper Configuration");
  this.configuration.setVariables(props);
  this.parsed = false;
  this.environment = environment;
  this.parser = parser;
}
Copy the code

The private construct is used to initialize the Configuration object and assign core properties. XMLConfigBuilder builds a Configuration object and then assigns the properties variable to the Configuration object by calling the constructor of its parent BaseBuilder class.

So just to summarize, to create an SqlSessionFactory object we first create an XMLConfigBuilder object, and to create an XMLConfigBuilder object we create an XathParser object, XpathParser parses the XML Configuration file. After successfully creating the XMLConfigBuilder object, call the parse() method to parse it into a Configuration object. Create an instance of DefaultSqlSessionFactory from the Configuration object.

Build the SqlSession instance

After creating the DefaultSqlSessionFactory instance, you can create the SqlSession object. The DefaultSqlSessionFactory class contains many openSession() overloaded methods.

@Override
public SqlSession openSession(a) {... }@Override
public SqlSession openSession(boolean autoCommit) {... }@Override
public SqlSession openSession(ExecutorType execType){... }@Override
public SqlSession openSession(TransactionIsolationLevel level){... }Copy the code

The second level cache

Mybatis uses two types of caches: local caches and secondary caches.

Whenever a new session is created, MyBatis creates a local cache associated with it. The results of any queries executed in the session are stored in the local cache, so when the same query is executed again with the same parameters, there is no need to actually query the database. The local cache is cleared when changes are made, a transaction commits or rolls back, and a session is closed.

To create the cache

Below according to the source code analysis of the implementation process of secondary cache

The build() method of the SqlSessionFactoryBuilder class parses the generated Configuration object inside the parser.parse() method.

Parser.parse () has an internal parseConfiguration(parser.evalnode (“/configuration”)); The Mybatis () method is used to parse the configuration in the Mybatis global configuration file Mybatis -config. XML.

Let’s focus on the mapper tag resolution method, mapperElement(root.evalNode(“mappers”));

We all know that there are four ways to configure a mapped mapper file in a global configuration file:

  • The package tag, which specifies the package name of the mapper
  • The resource property, which uses a resource reference relative to the classpath
  • Url property, using the fully qualified resource locator URL
  • Class property that implements the fully qualified class name of the class using the mapper interface

Mapper tag logic:

private void mapperElement(XNode parent) throws Exception {
  if(parent ! =null) {
    for (XNode child : parent.getChildren()) {
      // Configure the mapper using the package tag
      if ("package".equals(child.getName())) {
        String mapperPackage = child.getStringAttribute("name");
        configuration.addMappers(mapperPackage);
      } else {
        String resource = child.getStringAttribute("resource");
        String url = child.getStringAttribute("url");
        String mapperClass = child.getStringAttribute("class");
        // Configure the mapper with the resource attribute
        if(resource ! =null && url == null && mapperClass == null) {
          ErrorContext.instance().resource(resource);
          InputStream inputStream = Resources.getResourceAsStream(resource);
          XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
          mapperParser.parse();
        // Configure the mapper with the URL property
        } else if (resource == null&& url ! =null && mapperClass == null) {
          ErrorContext.instance().resource(url);
          InputStream inputStream = Resources.getUrlAsStream(url);
          XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
          mapperParser.parse();
        // Use the class attribute to configure the mapper
        } else if (resource == null && url == null&& mapperClass ! =null) { Class<? > mapperInterface = Resources.classForName(mapperClass); configuration.addMapper(mapperInterface); }else {
          throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
        }
      }
    }
  }
}
Copy the code

Each mapping file is parsed using the XMLMapperBuilder object calling the parse() method.

public void parse(a) {
  if(! configuration.isResourceLoaded(resource)) { configurationElement(parser.evalNode("/mapper"));
    configuration.addLoadedResource(resource);
    bindMapperForNamespace();
  }

  parsePendingResultMaps();
  parsePendingCacheRefs();
  parsePendingStatements();
}
Copy the code

Into the configurationElement (parser. EvalNode (“/mapper “)); Method, which creates a level 2 cache.

private void configurationElement(XNode context) {
  try {
    // Get the namespace attribute of the top-level tag mapper
    String namespace = context.getStringAttribute("namespace");
    if (namespace == null || namespace.isEmpty()) {
      throw new BuilderException("Mapper's namespace cannot be empty");
    }
    builderAssistant.setCurrentNamespace(namespace);
    // Parse the cache-ref label
    cacheRefElement(context.evalNode("cache-ref"));
    // Parse the cache label
    cacheElement(context.evalNode("cache"));
    parameterMapElement(context.evalNodes("/mapper/parameterMap"));
    resultMapElements(context.evalNodes("/mapper/resultMap"));
    sqlElement(context.evalNodes("/mapper/sql"));
    buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
  } catch (Exception e) {
    throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: "+ e, e); }}Copy the code

Cache-ref tag and cache tag can be used to parse cache tags. CacheRefElement ()

private void cacheRefElement(XNode context) {
  if(context ! =null) {
    configuration.addCacheRef(builderAssistant.getCurrentNamespace(), context.getStringAttribute("namespace"));
    CacheRefResolver cacheRefResolver = new CacheRefResolver(builderAssistant, context.getStringAttribute("namespace"));
    try {
      cacheRefResolver.resolveCacheRef();
    } catch(IncompleteElementException e) { configuration.addIncompleteCacheRef(cacheRefResolver); }}}Copy the code

Look again at the cacheElement() method

private void cacheElement(XNode context) {
  if(context ! =null) {
    // Get the value of the cache tag type attribute. If not, the default value is PERPETUAL
    String type = context.getStringAttribute("type"."PERPETUAL");
    // Resolve the alias of the type attribute value
    Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
    // Get eviction property value, default is LRU
    String eviction = context.getStringAttribute("eviction"."LRU");
    // Parse the eviction attribute alias
    Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);
    // Get the value of the flushInterval attribute
    Long flushInterval = context.getLongAttribute("flushInterval");
    // Get the size attribute value
    Integer size = context.getIntAttribute("size");
    // Get the readOnly attribute value
    booleanreadWrite = ! context.getBooleanAttribute("readOnly".false);
    // Get the blocking property value
    boolean blocking = context.getBooleanAttribute("blocking".false);
    // Get the internal property tag attribute
    Properties props = context.getChildrenAsProperties();
    // Create a new cachebuilderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props); }}Copy the code

Enter the useNewCache (…). methods

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;
}
Copy the code

You can see that in this method the cache object is built from the attribute value of the

tag. Let’s move to the build() method that actually builds

public Cache build(a) {
  // Set the default values for the Type and eviction properties
  setDefaultImplementations();
  // Create a cache instance
  Cache cache = newBaseCacheInstance(implementation, id);
  // Set attributes for the cache object
  setCacheProperties(cache);
  // issue #352, do not apply decorators to custom caches
  if (PerpetualCache.class.equals(cache.getClass())) {
    // Use decorator mode to add attributes to cache objects
    for (Class<? extends Cache> decorator : decorators) {
      cache = newCacheDecoratorInstance(decorator, cache);
      setCacheProperties(cache);
    }
    cache = setStandardDecorators(cache);
  } else if(! LoggingCache.class.isAssignableFrom(cache.getClass())) { cache =new LoggingCache(cache);
  }
  return cache;
}
Copy the code

As you can see, the decorator pattern is used to add the cache attributes to the cache object, and the newBaseCacheInstance() method is used to find the reflection mechanism used to create the cache.

After creating a cache object, add it to the level-2 cache map.

protected final Map<String, Cache> caches = new StrictMap<>("Caches collection");
Copy the code

The level 2 cache creation process is described above. How is the cache used?

Use the cache

The place to use the cache is in the CachingExecutor class. Let’s see what’s going on, using a query as an example.

@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
    throws SQLException {
  // Get the cache
  Cache cache = ms.getCache();
  if(cache ! =null) {
    // Refresh the cache as needed
    flushCacheIfRequired(ms);
    if (ms.isUseCache() && resultHandler == null) {
      ensureNoOutParams(ms, boundSql);
      @SuppressWarnings("unchecked")
      // Fetch the result set from the cache
      List<E> list = (List<E>) tcm.getObject(cache, key);
      if (list == null) {
        // Query the database if there is none in the cache
        list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
        // The result collection is stored in the cache
        tcm.putObject(cache, key, list); // issue #578 and #116
      }
      returnlist; }}// Delegate pattern, which is handed over to implementation classes such as SimpleExecutor to implement methods
  return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
Copy the code

Mybatis does not support caching of stored procedures, so if it is a stored procedure, an error will be reported here. Mybatis does not support caching of stored procedures.

private void ensureNoOutParams(MappedStatement ms, BoundSql boundSql) {
  if (ms.getStatementType() == StatementType.CALLABLE) {
    for (ParameterMapping parameterMapping : boundSql.getParameterMappings()) {
      if(parameterMapping.getMode() ! = ParameterMode.IN) {throw new ExecutorException("Caching stored procedures with OUT params is not supported. Please configure useCache=false in " + ms.getId() + " statement."); }}}}Copy the code

According to the key value in TransactionalCacheManager remove the cache, if we can find out the result set in the cache, it returns. If there is no cache, the query is executed and the result set is placed in the cache and the result is returned.

summary

This article mainly from the source of the SqlSession creation process and secondary cache analysis, such as Mybatis interested can pay attention to other articles in this column.