Mybatis @Flush annotation analysis

While looking at the source code, I found the @flush annotation. I haven’t used it before, and that’s where this article comes in

Note: The executor type here must be BatchExecutor

First example

 @Test
  public void testShouldSelectClassUsingMapperClass(a){
    try(
      // Specify BATCH as the executor type for this query. The corresponding type is BatchExecutor
      SqlSession session = sqlMapper.openSession(ExecutorType.BATCH)
      ){
      ClassMapper mapper = session.getMapper(ClassMapper.class);
      long start = System.currentTimeMillis();
      
      // This query is normal query method
      List<ClassDo> classDos = mapper.listClassOwnerStudentByClassId(1.new String[]{"The dog egg"});
      System.out.println(classDos);
      System.out.println("Start updating");
      
      // Here are the two update operations
      System.out.println(mapper.updateClassNameById(1."Accounting Division 1"));
      System.out.println(mapper.updateClassNameById(2."Accounting Division 1 2"));
      System.out.println(End of update);
      // Call flush, which I wrote myself. The return value is List
      
        and there is no CORRESPONDING Xml for this flush method.
      
      // The database is actually updated when this method is called
      for (BatchResult flush : mapper.flush()) {
        // Update SQL
        System.out.println(flush.getSql());
        // Updated parameters
        System.out.println(flush.getParameterObjects());
         // Update the number of rows affected, including getParameterObjects above, which return values are all an array
         // This batch is in the statement dimension. Therefore, both of the above queries belong to the same statement, with different parameters
        So, getParameterObjects is an array, and getUpdateCounts must also be an array.System.out.println(flush.getUpdateCounts()); }}public interface ClassMapper {
  // This does not have, must not have
    @Flush
    List<BatchResult> flush(a);

  
   // The following two have XMl files
    List<ClassDo> listClassOwnerStudentByClassId(Integer orderId,@Param("array") String[] names);

   int updateClassNameById(@Param("id") Integer id,@Param("className") String className);

}

Copy the code

Looking at the results

1. The role of annotations

First look at the comments on the source code


/** * The maker annotation that invoke a flush statements via Mapper interface. * * 
 * public interface UserMapper {* & # 064; Flush * List< BatchResult> flush(); * } * 

*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Flush {
}

Copy the code

This annotation is a tag interface that is invoked to refresh statements through Mapper’s interface

2. How do annotations work?

On method invocation, the Invoke method of MapperProxy is called, where PlainMethodInvoker is built, and MapperMethod is wrapped inside PlainMethodInvoker. SqlCommand and MethodSignature are built in MapperMethod, and the first parse @flush operation builds the SqlCommand object. In this, if the current method is decorated with @flush and there is no Mapper, the query will be sqlCommandType.flush

  1. First, parse the @flush annotation.
   public SqlCommand(Configuration configuration, Class
        mapperInterface, Method method) {
      // The method name
      final String methodName = method.getName();
     // The class of method
      finalClass<? > declaringClass = method.getDeclaringClass();// Get the MappedStatement from Configuration with the fully qualified type name and method name.
      MappedStatement ms = resolveMappedStatement(mapperInterface, methodName, declaringClass,
          configuration);
     
     // if ms is null. To use Flush, do not write a MapperStatement.
      if (ms == null) {
        if(method.getAnnotation(Flush.class) ! =null) {
          
          // The query type is FLUSH.
          name = null;
          type = SqlCommandType.FLUSH;
        } else {
          throw new BindingException("Invalid bound statement (not found): "
              + mapperInterface.getName() + "."+ methodName); }}else {
        name = ms.getId();
        type = ms.getSqlCommandType();
        if (type == SqlCommandType.UNKNOWN) {
          throw new BindingException("Unknown execution method for: "+ name); }}}Copy the code
  1. use@FlushAnnotation.

The execute method of MapperMethod uses Type to determine the Type of the operation. The execute method is FLUSH.

  public Object execute(SqlSession sqlSession, Object[] args) {
    Object result; // It's called strategic mode
    
    // Type is FLUSH
    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);
          result = sqlSession.selectOne(command.getName(), param);
          if (method.returnsOptional()
              && (result == null || !method.getReturnType().equals(result.getClass()))) {
            result = Optional.ofNullable(result);
          }
        }
        break;
      case FLUSH:
        // The point is the 'flushStatements' method
        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;
  }
Copy the code

Continue to see the sqlSession. FlushStatements () went down after the above code has been point, there will be produce polymorphic. One is in the doFlushStatements method of BatchExecutor, and the other is in the doFlushStatements method of SimpleExecutor. The most important thing here is to take the first case. But I’m going to go ahead and look at both cases.

  1. BatchExecutor

    StatementList is an attribute of BatchExecutor. In this method, the Statement previously added to the statementList is executed, and the updated line number is encapsulated as an attribute of batchResult. This is called batch updating.

    How many questions?

    Why is statementList a List

    Because there can be multiple statements in an update operation, so here’s a List,

    Why is batchResult also a List and parameterObjects also a List?

    Because multiple statements correspond to multiple results, parameterObjects is because a Statement can be called multiple times with different parameter values.

    So where are the statementList and batchResult values added?

    @Override
      public List<BatchResult> doFlushStatements(boolean isRollback) throws SQLException {
        try {
          List<BatchResult> results = new ArrayList<>();
          if (isRollback) {
            return Collections.emptyList();
          }
          / / traverse the statement
          for (int i = 0, n = statementList.size(); i < n; i++) {
            // Set the query parameters
            Statement stmt = statementList.get(i);
            applyTransactionTimeout(stmt);
            
            BatchResult batchResult = batchResultList.get(i);
            try {
              // Statement executeBatch will be called to execute the previously prepared SQL in batches.
              // Where is statement added? Where was batchResult added?
              batchResult.setUpdateCounts(stmt.executeBatch());
              MappedStatement ms = batchResult.getMappedStatement();
              // Get the updated operation
              List<Object> parameterObjects = batchResult.getParameterObjects();
              
              // If KeyGenerator is used.
              KeyGenerator keyGenerator = ms.getKeyGenerator();
              if (Jdbc3KeyGenerator.class.equals(keyGenerator.getClass())) {
                Jdbc3KeyGenerator jdbc3KeyGenerator = (Jdbc3KeyGenerator) keyGenerator;
                jdbc3KeyGenerator.processBatch(ms, stmt, parameterObjects);
              } else if(! NoKeyGenerator.class.equals(keyGenerator.getClass())) {//issue #141
                for (Object parameter : parameterObjects) {
                  keyGenerator.processAfter(this, ms, stmt, parameter); }}/ / close the Statement
              closeStatement(stmt);
            } catch (BatchUpdateException e) {
              StringBuilder message = new StringBuilder();
              message.append(batchResult.getMappedStatement().getId())
                  .append(" (batch index #")
                  .append(i + 1)
                  .append(")")
                  .append(" failed.");
              if (i > 0) {
                message.append("")
                    .append(i)
                    .append(" prior sub executor(s) completed successfully, but will be rolled back.");
              }
              throw new BatchExecutorException(message.toString(), e, results, batchResult);
            }
            // Add the result to result.
            results.add(batchResult);
          }
          return results;
        } finally {
          for (Statement stmt : statementList) {
            closeStatement(stmt);
          }
          currentSql = null; statementList.clear(); batchResultList.clear(); }}Copy the code
  2. SimpleExecutor

    // Empty operation. Returns an empty list, which is nothing to look at.
     @Override
      public List<BatchResult> doFlushStatements(boolean isRollback) {
        return Collections.emptyList();
      }
    Copy the code

3. WhenstatementListandbatchResultListAdd value

The easiest way to do this is to click on it, find all the addresses it references, and look for the Add method, because it’s a List

There appears to be only one place where the Add method is called

The built Statement is added to the statementList during the update operation, and the BatchResult is also built and added to it. The PreparedStatement#addBatch() method is finally called. And returns a fixed value.

The BatchResult parameters are MapperStatement, SQL, and parameterObject

The problem?

  1. What is currentSql in the following code?

    This is when an updated Mapper is called multiple times to produce only one Statement and one BatchResult

    CurrentSql is null when the method is called for the first time. CurrentSql and currentStatement are assigned to the current assembly after being added to the first Statement. If the method is called a second time, the currentSql and currentStatement will go to if. The query parameters are added to the parameterObject property of the BatchResult

// BatchExecutor's doUpdate method
// The logic is basically the same as the previous query. Get Connection, process parameters, but instead of doing it, PreparedStatement#addBatch() is called;
  @Override
  public int doUpdate(MappedStatement ms, Object parameterObject) throws SQLException {
    final Configuration configuration = ms.getConfiguration();
    final StatementHandler handler = configuration.newStatementHandler(this, ms, parameterObject, RowBounds.DEFAULT, null.null);
    final BoundSql boundSql = handler.getBoundSql();
    final String sql = boundSql.getSql();
    final Statement stmt;
    if (sql.equals(currentSql) && ms.equals(currentStatement)) {
      int last = statementList.size() - 1;
      stmt = statementList.get(last);
      applyTransactionTimeout(stmt);
      handler.parameterize(stmt);// fix Issues 322
      BatchResult batchResult = batchResultList.get(last);
      batchResult.addParameterObject(parameterObject);
    } else {
      Connection connection = getConnection(ms.getStatementLog());
      stmt = handler.prepare(connection, transaction.getTimeout());
      handler.parameterize(stmt);    // fix Issues 322
      currentSql = sql;
      currentStatement = ms;
      statementList.add(stmt);
      batchResultList.add(new BatchResult(ms, sql, parameterObject));
    }
    handler.batch(stmt);
    // Return a fixed value of integer.min_value + 1002;
    return BATCH_UPDATE_RETURN_VALUE;
  }

Copy the code

added

Delete, insert and Update in Mybatis will use doUpdate method, the above is Update example. That is, the remaining three operations perform batch processing except for queries.

conclusion

Mybatis @Flush is prepared for batch operation. The executor type must be set to BatchExecutor (which can be set globally and locally when retrieving sessions), and the @flush method must not have an XML file. The return value is List, as shown below

  @Flush
    List<BatchResult> flush(a);
Copy the code

When a method is called, it can be updated by calling Mapper’s @flush modified method.