Mybatis Source Parsing - How does SqlSession implement database operations?

Keywords: Java Session Database Mybatis Spring

Mybatis Source Parsing (IV) - How does SqlSession implement database operations?

_If you use a database request operation as a comparison, the first three articles are preparing for the request, and the real operation is what this article will tell you.As with the title, the most central point of this article is that SqlSession implements source code resolution for database operations.By convention, however, I still have the following questions:

  • 1. How was the SqlSession created?Does each database operation create a new SqlSession?(Many students might say that SqlSession was created through SqlSessionFactory.openSession(), but this answer is given a maximum of 5 points on a 10-point scale.)
  • 2. The relationship between SqlSession and Transaction?In the same method, Mybatis requests the database multiple times. Do you want to create multiple SqlSessions?
  • 3. How does SqlSession implement database operations?

_The content of this chapter is to analyze around the above three issues, so take the problem to see the source code!

1. Creation of SqlSession

_When learning Mybatis, we often see that the SqlSession creation method is SqlSessionFactory.openSession(), so let's take this as a starting point, first look at the method source code of SqlSessionFactory.openSession() (note DefaultSqlSessionFactory), which calls the openSessionFromDataSource() method internally, so let's look inside this methodPart Source:


  private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
    Transaction tx = null;
    try {
      final Environment environment = configuration.getEnvironment();
      // Created a TransactionFactory Transaction Factory (if you have a close look at the SqlSessionFactoryBean.buildSqlSessionFactory() process, you should see the SpringManagedTransactionFactory)
      final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
      // Get a Transaction Transaction from TransactionFactory 
      tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
      // Get an Executor from execType (default is SIMPLE)
      final Executor executor = configuration.newExecutor(tx, execType);
      // A DefaultSqlSession object was returned
      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();
    }
  }
  

_There are three steps to create the entire SqlSession:

  • 1. Obtain the TransactionFactory Transaction Factory object (if you have a close look at the SqlSessionFactoryBean.buildSqlSessionFactory() process, you should be able to see the SpringManagedTransactionFactory)
  • 2. Obtain a Transaction through TransactionFactory
  • 3. Get an Executor based on execType (default is SIMPLE)
  • 4. Create and return a DefaultSqlSession object

_From the source code we know that every time a SqlSession (or DefaultSqlSession, to be exact) is created, a Transaction (SpringManagedTransaction in Mybatis-Spring) transaction object is generated.In other words:

  • 1. A Transaction object is a one-to-one correspondence with a SqlSession object.
  • 2. The same SqlSession no matter how many database operations it performs.As long as no close is performed, the entire operation is performed in the same Transaction.

_Students who have read previous articles should have doubts that whether SqlSession was created for MapperProxy or SqlSession called in MapperMethod is actually SqlSessionTemplate, which is not the same SqlSession object as DefaultSqlSession here.So let's briefly analyze the differences and responsibilities between the two.

Secret between SqlSessionTemplate and DefaultSqlSession

_In previous articles, we mentioned that every time a MapperFactoryBean is created, a SqlSessionTemplate object is created, and MapperFactoryBean passes the SqlSessionTemplate to MapperProxy when it acquires a MapperProxy.That is, the life cycle of SqlSessionTemplate is the same as that of MapperProxy.(Note: MapperProxy is injected into the Spring container, so the result is not difficult to imagine)

_In the previous articles, we briefly described that sqlSessionProxy is maintained inside SqlSessionTemplate, and sqlSessionProxy is a SqlSession object created by a dynamic proxy, and SqlSessionTemplate's database operation method insert/update is delegated to sqlSessionProxy.So let's look at the creation of this sqlSessionProxy:


  public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType,
      PersistenceExceptionTranslator exceptionTranslator) {

    notNull(sqlSessionFactory, "Property 'sqlSessionFactory' is required");
    notNull(executorType, "Property 'executorType' is required");

    this.sqlSessionFactory = sqlSessionFactory;
    this.executorType = executorType;
    this.exceptionTranslator = exceptionTranslator;
    // Created by the Proxy.newProxyInstance() dynamic proxy 
    this.sqlSessionProxy = (SqlSession) newProxyInstance(
        SqlSessionFactory.class.getClassLoader(),
        new Class[] { SqlSession.class },
        new SqlSessionInterceptor());
  }
  

_In the code for creating sqlSessionProxy, we can see that its specified proxy object is SqlSessionInterceptor (SqlSession Interceptor?), then the key code must be in this SqlSessionInterceptor, look at SqlSessionInterceptor and find it is an internal class with the following source code:


 private class SqlSessionInterceptor implements InvocationHandler {
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
      // Get a SqlSession based on the condition (note that the SqlSession at this time is DefaultSqlSession), and the SqlSession at this time may be newly created, or it may be the SqlSession at the last request.
      SqlSession sqlSession = getSqlSession(
          SqlSessionTemplate.this.sqlSessionFactory,
          SqlSessionTemplate.this.executorType,
          SqlSessionTemplate.this.exceptionTranslator);
      try {
        // Reflect Execute SqlSession Method
        Object result = method.invoke(sqlSession, args);
        // Determine if the current SqlSession is transactional and if it is not commit
        if (!isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
          // force commit even on non-dirty sessions because some databases require
          // a commit/rollback before calling close()
          sqlSession.commit(true);
        }
        return result;
      } catch (Throwable t) {
        Throwable unwrapped = unwrapThrowable(t);
        // Determine if it is PersistenceExceptionTranslator and not empty, then close the current session and leave sqlSession empty to prevent finally from closing repeatedly
        if (SqlSessionTemplate.this.exceptionTranslator != null && unwrapped instanceof PersistenceException) {
          // release the connection to avoid a deadlock if the translator is no loaded. See issue #22
          closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
          sqlSession = null;
          Throwable translated = SqlSessionTemplate.this.exceptionTranslator.translateExceptionIfPossible((PersistenceException) unwrapped);
          if (translated != null) {
            unwrapped = translated;
          }
        }
        throw unwrapped;
      } finally {
        //As long as the current session is not empty, the current session operation is closed, which in turn determines whether the session is released or closed directly based on whether the current session has transactions.
        if (sqlSession != null) {
          closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
        }
      }
    }
  }
  

_The entire invoke is divided into five steps:

  • 1. Get a SqlSession based on the condition (note that the SqlSession at this time is DefaultSqlSession), at which point the SqlSession may be newly created or may be the last SqlSession requested.
  • 2. Reflect Execute SqlSession Method
  • 3. Determine if the current SqlSession is controlled by the firm and if it is not commit
  • 4. Decide if it is PersistenceExceptionTranslator and it is not empty, then close the current session and leave sqlSession empty to prevent finally from repeatedly closing
  • 5. As long as the current session is not empty, the current session operation will be closed, and closing the current session operation will determine whether the session is released or closed directly depending on whether there are transactions in the current session.

_Step 1 is the core of this discussion and its internal process is as follows:


 public static SqlSession getSqlSession(SqlSessionFactory sessionFactory, ExecutorType executorType, PersistenceExceptionTranslator exceptionTranslator) {

    notNull(sessionFactory, "No SqlSessionFactory specified");
    notNull(executorType, "No ExecutorType specified");
    
    // Get a SqlSessionHolder from TransactionSynchronization Manager (a transaction synchronization manager in Spring) (you should literally understand that it maintains SqlSession internally)
    SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);
    // Determine if SqlSessionHolder is empty and synchronized with transactions
    if (holder != null && holder.isSynchronizedWithTransaction()) {
      if (holder.getExecutorType() != executorType) {
        throw new TransientDataAccessResourceException("Cannot change the ExecutorType when there is an existing transaction");
      }
      // Increase reference count by 1
      holder.requested();
      // Returns the SqLSession held
      return holder.getSqlSession();
    }

    // If a SqlSessionHolder is not available from the Transaction Synchronization Manager, call sessionFactory.openSession() to create a new SqlSession 
    SqlSession session = sessionFactory.openSession(executorType);
    
    // To determine whether there are currently transactions, create a SqlSessionHolder based on SqlSession and register it in TransactionSynchronization Manager for next use in the current transaction
    if (TransactionSynchronizationManager.isSynchronizationActive()) {
      Environment environment = sessionFactory.getConfiguration().getEnvironment();

      if (environment.getTransactionFactory() instanceof SpringManagedTransactionFactory) {
        holder = new SqlSessionHolder(session, executorType, exceptionTranslator);
        TransactionSynchronizationManager.bindResource(sessionFactory, holder);
        TransactionSynchronizationManager.registerSynchronization(new SqlSessionSynchronization(holder, sessionFactory));
        holder.setSynchronizedWithTransaction(true);
        holder.requested();
      } else {
        if (TransactionSynchronizationManager.getResource(environment.getDataSource()) == null) {
          if (logger.isDebugEnabled()) {
            logger.debug("SqlSession [" + session + "] was not registered for synchronization because DataSource is not transactional");
          }
        } else {
          throw new TransientDataAccessResourceException(
              "SqlSessionFactory must be using a SpringManagedTransactionFactory in order to use Spring transaction synchronization");
        }
      }
    } else {
      if (logger.isDebugEnabled()) {
        logger.debug("SqlSession [" + session + "] was not registered for synchronization because synchronization is not active");
      }
    }

    return session;
  }

_The whole process is divided into several steps:

  • 1. Obtain a SqlSessionHolder through TransactionSynchronization Manager (a transaction synchronization manager in Spring) (you should literally understand that it is internally maintained with SqlSession)
  • 2. To determine whether the SqlSessionHolder is empty or synchronized with a transaction, return the SqLSession held
  • 3. If a SqlSessionHolder is not available from the Transaction Synchronization Manager, call sessionFactory.openSession() to create a new SqlSession
  • 4. Determine if there is a transaction at hand, or create a SqlSessionHolder based on SqlSession and register it with TransactionSynchronization Manager for next use in the current transaction

_From the steps above, we find that the whole acquisition of SqlSession is strongly related to transactions, and from the above process, we can get several key points of information:

  • 1. No matter how many times a method in mapper is called in the same transaction, the same sqlSession is used in the end, that is, the same sqlSession is used in a transaction.
  • 2. If no transaction is opened, calling the method in mapper once will create a new sqlSession to execute the method.

_If careful students find that this TransactionSynchronization Manager transaction synchronization manager is owned by Spring, this is a perfect example Mybatis-Spring Description of the subitem in:

MyBatis-Spring will help you seamlessly integrate MyBatis code into Spring.It will allow MyBatis to participate in Spring's transaction management, create mapper s and SqlSession s, and inject them into bean s.

Problems found in DEBUG tests

When testing DEBUG, the author found that whenever a transaction was started, the SqlSessionHolder would eventually be acquired through TransactionSynchronization Manager.getResource (sessionFactory), even if it was the first request after restarting the program, and the author had not reached a breakpoint in the constructors of SqlSessionHolder and DefaultSqlSession Here, the author is really puzzled, guessing that the TransactionSynchronization Manager.getResource (sessionFactory) method has created SqlSession dynamically through reflection, but to see the source code is not found, the strength is not allowed at present!!

_Successfully acquires the SqlSession (DefaultSqlSession, to be exact) and invokes the specific DefaultSqlSession method through method.invoke() reflection.After the method call is completed, determine if the current SqlSession is controlled by the transaction, if not commit, and then call the close SqlSession() method to "close" the SqlSession.Why quote here?The reason is that the internal processing of this method determines whether the current SqlSession is controlled by the transaction. Yes, it only subtracts the reference counter by one, and does not really close the SqlSession (which is also to enable the next use of the same SqlSession), or executes the ongoing session.close() operation if it is not controlled by the transaction.

_So far, we understand that the creation of a SqlSession (DefaultSqlSession) is not simply a call to sessionFactory.openSession(), which relates to SqlSessionTemplate, DefaultSqlSession, SqlSessionInterceptor, and Spring Transaction (Transaction Synchronization Manager).So it is important to understand the connections and roles of these objects.

2. SqlSession Implements Database Operation

_From the above analysis, we know that any Mapper interface method request will ultimately request DefaultSqlSession, which encapsulates database operations internally and ultimately relies on other SqlSession subclasses to operate the database.So let's start with the selectList() method inside DefaultSqlSession to show how it encapsulates database operations.

    
  public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
    try {
      // Get the MappedStatement for the specified method from the configuration (note: the statement is passed down from the name field of SqlCommand in MapperMethod, and the name itself comes from the id of the MappedStatement, so the final statement will be in the form of com.xxx.findUserByName)
      MappedStatement ms = configuration.getMappedStatement(statement);
      // Perform real database operations by delegating Executor's query()
      List<E> result = executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
      return result;
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }

_We can see that all methods of DefaultSqlSession basically have the following two steps:

  • 1. MappedStatement obtained from configuration for the specified method
  • 2. Perform real database operations by delegating Executor

_We know that the SqlSource of the method to be executed is stored inside the MappedStatement (which holds the Sql fragment information parsed from Mapper.xml) and then executes the database operation through Executor's query() method.Let's start with the Executor Inheritance Diagram:

_From the inheritance diagram we can see BaseExecutor and CachingExecutor, which represent the two caching mechanisms respectively. BaseExecutor maintains an object named localCache internally, which is the actual controller of the first-level cache, which CachingExecutor uses for the second-level cache and delegates its internal implementation to BaseExecutor by delegating itImplements first-level caching.You can read this article about Mybatis caching mechanisms: Chat about MyBatis Cache Mechanism

_Whether it is a first-level cache or a second-level cache mechanism, it will eventually call BaseExecutor, while the default BaseExecutor implementation of Mybatis is SimpleExecutor, so focus on the implementation of these two classes, the following is the query() source code of BaseExecutor:

  public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    // Get BoundSql from MappedStatement (actually by calling getBoundSql() of SqlSource in MappedStatement)
    BoundSql boundSql = ms.getBoundSql(parameter);
    // Resolves the cacheKey by argument, which is the first-level cached key 
    CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
    return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
 }

_We can see that the core of the whole method is to get the BoundSql from MappedStatement (actually by calling getBoundSql() of SqlSource in MappedStatement) and then calling the overloaded method query(), which does first-level caching. This is not described here as long as you know that the last call was to SimpleExe.Cutor's doQuery() method is fine, check the doQuery() method source code:


  public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
    Statement stmt = null;
    try {
      // Get Configuration from MappedStatement 
      Configuration configuration = ms.getConfiguration();
      // A StatementHandler object was created using Configuration's newStatementHandler() method
      StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
      // Call the prepareStatement() method to get the Statement object (the interface that actually executes the static SQl)
      stmt = prepareStatement(handler, ms.getStatementLog());
      // Call the StatementHandler.query() method to execute
      return handler.<E>query(stmt, resultHandler);
    } finally {
      closeStatement(stmt);
    }
  }
  

_JDBC has been hooked up since then. If students who are familiar with JDBC should know exactly what the Statement object is doing at a glance, it is customary to first analyze the internal execution process steps of the doQuery() method:

  • 1. Get Configuration from MappedStatement
  • 2. A StatementHandler object was created using Configuration's newStatementHandler() method
  • 3. Call the prepareStatement() method to get the Statement object (the interface that actually executes the static SQl)
  • 4. Call StatementHandler.query() method execution (actually delegate Statement execution internally)

_Among them, we should focus on the analysis of 2, 3 and 4.Let's start with Step 2, which involves a key object, StatementHandler, whose inheritance diagram is as follows:

_This structure is similar to Executor:

  • 1. SimpleStatementHandler, which corresponds to the Statement interface commonly used in JDBC for simple SQL processing
  • 2. PreparedStatementHandler, which corresponds to the PreparedStatement in JDBC for pre-compiled SQL processing
  • 3. CallableStatementHandler, which corresponds to CallableStatement in JDBC and is used to perform stored procedure-related processing
  • 4. RoutingStatementHandler, which is the route of the above three interfaces, has no actual operation but is responsible for the creation and invocation of the above three StatementHandlers.

_Look back at configuration.newStatementHandler(), and without guessing, it must have been RoutingStatementHandler, and its internal delegate defaults to PreparedStatementHandler (the MappedStatement builder method specifies the default statementType = StatementType.PREPARED).

_The three main functions of step three are to precompile and get the Statement (PreparedStatementHandler so the default is PreparedStatement), its prepareStatement() method source code:


  private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
    Statement stmt;
    // Create a Connection link to see the source code obtained through transaction.getConnection().
    Connection connection = getConnection(statementLog);
    // PrepareStatement is obtained by precompilation, which eventually calls the connection.prepareStatement() method
    stmt = handler.prepare(connection);
    // Set parameter information whose parameters are obtained from BoundSql
    handler.parameterize(stmt);
    return stmt;
  }
  

_The whole process is divided into three steps:

  • 1. Create a Connection link to see the source code obtained through transaction.getConnection().
  • 2. PrepareStatement is obtained by precompilation, which eventually calls the connection.prepareStatement() method
  • 3. Set parameter information, whose parameters are obtained from BoundSql

Step 4 calls the StatementHandler.query() method, using PreparedStatementHandler as an example, with the following source code:


  public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
    PreparedStatement ps = (PreparedStatement) statement;
    // Execute SQL
    ps.execute();
    // Parsing the returned data through the handleResultSets() method of ResultSetHandler
    return resultSetHandler.<E> handleResultSets(ps);
  }

_There are two process steps within this method:

  • 1. Executing SQL
  • 2. Parse the returned data through the handleResultSets() method of ResultSetHandler

_ResultSetHandler here is used to handle ResultSet in JDBC (I believe that students who have used JDBC are not unfamiliar with it), so the logic of parsing the returned data is not detailed here.

_From the analysis above, we can easily find that starting from SqlSession delegating Executor to execute the database, the execution of the whole Executor is actually an encapsulation of an execution of JDBC, which can be seen at a glance by students familiar with JDBC.

3. Personal Summary

_SqlSession's relationship to transactions:

  • 1. No matter how many times a method in mapper is called in the same transaction, the same sqlSession is used in the end, that is, the same sqlSession is used in a transaction.
  • 2. If no transaction is opened, calling the method in mapper once will create a new sqlSession to execute the method.

_SqlSession one-time execution flowchart:

This article is published by blog OpenWrite Release!

Posted by chiaki*misu on Thu, 21 Nov 2019 19:15:05 -0800