When looking at the source code, I found the @ Flush annotation. I haven't used it before, so I have this article
Note: the type of actuator here must be BatchExecutor
Let's start with an example
@Test public void testShouldSelectClassUsingMapperClass(){ try( //Specify that the type of executor for this query is BATCH. The corresponding type is BatchExecutor SqlSession session = sqlMapper.openSession(ExecutorType.BATCH) ){ ClassMapper mapper = session.getMapper(ClassMapper.class); long start = System.currentTimeMillis(); // This query is the normal query method List<ClassDo> classDos = mapper.listClassOwnerStudentByClassId(1, new String[]{"son of a bitch"}); System.out.println(classDos); System.out.println("Start update"); // Here are two update operations System.out.println(mapper.updateClassNameById(1, "Class 1 of planning section 1")); System.out.println(mapper.updateClassNameById(2, "Class 1 of planning section 2")); System.out.println("End update"); // Call the flush method, which I wrote myself. The return value is list < batchresult >, and the flush method has no corresponding Xml. // Only when this method is called can the database be really updated for (BatchResult flush : mapper.flush()) { //Updated sql System.out.println(flush.getSql()); //Updated parameters System.out.println(flush.getParameterObjects()); //The number of rows affected by the update, including the above getParameterObjects, and the return values are an array // This batch is in the dimension of statement. Therefore, the above two queries belong to one statement, but the parameters are different // Therefore, getParameterObjects is an array, and getUpdateCounts must also be an array. System.out.println(flush.getUpdateCounts()); } } public interface ClassMapper { // This is not, must not @Flush List<BatchResult> flush(); //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); }
Looking at the results
1. Role of notes
First look at the comments on the source code
/** * The maker annotation that invoke a flush statements via Mapper interface. * * <pre> * public interface UserMapper { * @Flush * List<BatchResult> flush(); * } * </pre> */ @Documented @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface Flush { }
This annotation is a tag interface that calls refresh statements through Mapper's interface
2. How does annotation work?
When the method is called, the invoke method of MapperProxy will be called, where PlainMethodInvoker will be built, and MapperMethod will be wrapped in PlainMethodInvoker. Build SqlCommand and MethodSignature in MapperMethod. The first @ Flush operation is to build SqlCommand object. If the current method is modified by @ Flush and there is no corresponding Mapper, the query will be changed to SqlCommandType.FLUSH
- The first step is to parse the @ Flush annotation.
public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) { // Method name final String methodName = method.getName(); // method class final Class<?> declaringClass = method.getDeclaringClass(); // Get MappedStatement from Configuration by fully qualified type name and method name. MappedStatement ms = resolveMappedStatement(mapperInterface, methodName, declaringClass, configuration); // If ms is null. The premise of using Flush is not to write MapperStatement. if (ms == null) { if (method.getAnnotation(Flush.class) != null) { //The type of this query 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); } } }
- Use @ Flush annotation.
In the execute method of MapperMethod, the Type of this operation will be determined by Type. It has been determined that its Type is FLUSH.
public Object execute(SqlSession sqlSession, Object[] args) { Object result; //Call it strategic model // The 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 key 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; }
Continue to sqlSession.flushStatements()
Follow the above code and click on it. Polymorphism will occur here. One is to the doFlushStatements method of BatchExecutor, and the other is the doFlushStatements method of simpleexecution. The most important thing here is to take the first case. But I will continue to analyze both cases.
-
BatchExecutor
statementList is the attribute of BatchExecutor. In this method, the Statement previously added to statementList will be actually executed, and then the updated rows will be encapsulated as an attribute of batchResult. This is batch update.
How many questions?
Why is statementList a List
Because there may be multiple statements in an update operation. Here is a List,
Why is batchResult a List and parameterObjects a List,
Because multiple statements correspond to multiple results, parameterObjects is because a Statement may be called multiple times and the parameter values are different.
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(); } //Traversal statement for (int i = 0, n = statementList.size(); i < n; i++) { //Set query parameters Statement stmt = statementList.get(i); applyTransactionTimeout(stmt); BatchResult batchResult = batchResultList.get(i); try { // Here, the executeBatch of the statement will be called to batch execute the previously prepared sql. // So, here's the question. Where was the statement added? Where is batchResult added? batchResult.setUpdateCounts(stmt.executeBatch()); MappedStatement ms = batchResult.getMappedStatement(); //Get updated action 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 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 the result and return it. results.add(batchResult); } return results; } finally { for (Statement stmt : statementList) { closeStatement(stmt); } currentSql = null; statementList.clear(); batchResultList.clear(); } }
-
SimpleExecutor
// Empty operation. Returns an empty list, which has nothing to see. @Override public List<BatchResult> doFlushStatements(boolean isRollback) { return Collections.emptyList(); }
3. When to add values to statementList and batchResultList
The simplest way is to click, find all the referenced addresses, and find the add method, because it is a List
It seems that there is only one place where the add method is called
During the update operation, the built Statement will be added to the statementList. At the same time, BatchResult will be built and added to it. The last call is the PreparedStatement#addBatch() method. And returns a fixed value.
You can see several parameters of BatchResult, namely MapperStatement, sql and parameterObject parameters
Question?
-
What is currentSql in the following code?
This is an updated Mapper. Multiple calls will only generate a Statement and a BatchResult
When this method is called for the first time, currentSql is null. After it is added to the first Statement, currentSql and currentStatement will be assigned as currently assembled. If this method is called for the second time, it will go to if and add the parameters of this query to the parameterObject property of the previous BatchResult
// doUpdate method of BatchExecutor // The logic here is basically the same as the previous query. They all get the Connection and process parameters, but they are not executed here. Instead, 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); //You can see that the returned value is a fixed value of Integer.MIN_VALUE + 1002; return BATCH_UPDATE_RETURN_VALUE; }
summary
Mybatis @Flush is prepared for batch updates. The type of executor must be set to BatchExecutor (it can be set globally and locally when obtaining Session), and the method marked with @ Flush cannot have a corresponding xml file. The return value is List, as shown below
@Flush List<BatchResult> flush();
When the method is called, it can be updated by calling Mapper's @ Flush modified method.