MyBatisBatchItemWriter.java

  1. /*
  2.  * Copyright 2010-2024 the original author or authors.
  3.  *
  4.  * Licensed under the Apache License, Version 2.0 (the "License");
  5.  * you may not use this file except in compliance with the License.
  6.  * You may obtain a copy of the License at
  7.  *
  8.  *    https://www.apache.org/licenses/LICENSE-2.0
  9.  *
  10.  * Unless required by applicable law or agreed to in writing, software
  11.  * distributed under the License is distributed on an "AS IS" BASIS,
  12.  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13.  * See the License for the specific language governing permissions and
  14.  * limitations under the License.
  15.  */
  16. package org.mybatis.spring.batch;

  17. import static org.springframework.util.Assert.isTrue;
  18. import static org.springframework.util.Assert.notNull;

  19. import org.apache.ibatis.session.ExecutorType;
  20. import org.apache.ibatis.session.SqlSession;
  21. import org.apache.ibatis.session.SqlSessionFactory;
  22. import org.mybatis.logging.Logger;
  23. import org.mybatis.logging.LoggerFactory;
  24. import org.mybatis.spring.SqlSessionTemplate;
  25. import org.springframework.batch.item.Chunk;
  26. import org.springframework.batch.item.ItemWriter;
  27. import org.springframework.beans.factory.InitializingBean;
  28. import org.springframework.core.convert.converter.Converter;
  29. import org.springframework.dao.EmptyResultDataAccessException;
  30. import org.springframework.dao.InvalidDataAccessResourceUsageException;

  31. /**
  32.  * {@code ItemWriter} that uses the batching features from {@code SqlSessionTemplate} to execute a batch of statements
  33.  * for all items provided.
  34.  * <p>
  35.  * Provided to facilitate the migration from Spring-Batch iBATIS 2 writers to MyBatis 3.
  36.  * <p>
  37.  * The user must provide a MyBatis statement id that points to the SQL statement defined in the MyBatis.
  38.  * <p>
  39.  * It is expected that {@link #write(Chunk)} is called inside a transaction. If it is not each statement call will be
  40.  * autocommitted and flushStatements will return no results.
  41.  * <p>
  42.  * The writer is thread safe after its properties are set (normal singleton behavior), so it can be used to write in
  43.  * multiple concurrent transactions.
  44.  *
  45.  * @author Eduardo Macarron
  46.  *
  47.  * @since 1.1.0
  48.  */
  49. public class MyBatisBatchItemWriter<T> implements ItemWriter<T>, InitializingBean {

  50.   private static final Logger LOGGER = LoggerFactory.getLogger(MyBatisBatchItemWriter.class);

  51.   private SqlSessionTemplate sqlSessionTemplate;

  52.   private String statementId;

  53.   private boolean assertUpdates = true;

  54.   private Converter<T, ?> itemToParameterConverter = new PassThroughConverter<>();

  55.   /**
  56.    * Public setter for the flag that determines whether an assertion is made that number of BatchResult objects returned
  57.    * is one and all items cause at least one row to be updated.
  58.    *
  59.    * @param assertUpdates
  60.    *          the flag to set. Defaults to true;
  61.    */
  62.   public void setAssertUpdates(boolean assertUpdates) {
  63.     this.assertUpdates = assertUpdates;
  64.   }

  65.   /**
  66.    * Public setter for {@link SqlSessionFactory} for injection purposes.
  67.    *
  68.    * @param sqlSessionFactory
  69.    *          a factory object for the {@link SqlSession}.
  70.    */
  71.   public void setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) {
  72.     if (sqlSessionTemplate == null) {
  73.       this.sqlSessionTemplate = new SqlSessionTemplate(sqlSessionFactory, ExecutorType.BATCH);
  74.     }
  75.   }

  76.   /**
  77.    * Public setter for the {@link SqlSessionTemplate}.
  78.    *
  79.    * @param sqlSessionTemplate
  80.    *          a template object for use the {@link SqlSession} on the Spring managed transaction
  81.    */
  82.   public void setSqlSessionTemplate(SqlSessionTemplate sqlSessionTemplate) {
  83.     this.sqlSessionTemplate = sqlSessionTemplate;
  84.   }

  85.   /**
  86.    * Public setter for the statement id identifying the statement in the SqlMap configuration file.
  87.    *
  88.    * @param statementId
  89.    *          the id for the statement
  90.    */
  91.   public void setStatementId(String statementId) {
  92.     this.statementId = statementId;
  93.   }

  94.   /**
  95.    * Public setter for a converter that converting item to parameter object.
  96.    * <p>
  97.    * By default implementation, an item does not convert.
  98.    *
  99.    * @param itemToParameterConverter
  100.    *          a converter that converting item to parameter object
  101.    *
  102.    * @since 2.0.0
  103.    */
  104.   public void setItemToParameterConverter(Converter<T, ?> itemToParameterConverter) {
  105.     this.itemToParameterConverter = itemToParameterConverter;
  106.   }

  107.   /**
  108.    * Check mandatory properties - there must be an SqlSession and a statementId.
  109.    */
  110.   @Override
  111.   public void afterPropertiesSet() {
  112.     notNull(sqlSessionTemplate, "A SqlSessionFactory or a SqlSessionTemplate is required.");
  113.     isTrue(ExecutorType.BATCH == sqlSessionTemplate.getExecutorType(),
  114.         "SqlSessionTemplate's executor type must be BATCH");
  115.     notNull(statementId, "A statementId is required.");
  116.     notNull(itemToParameterConverter, "A itemToParameterConverter is required.");
  117.   }

  118.   @Override
  119.   public void write(final Chunk<? extends T> items) {

  120.     if (!items.isEmpty()) {
  121.       LOGGER.debug(() -> "Executing batch with " + items.size() + " items.");

  122.       for (T item : items) {
  123.         sqlSessionTemplate.update(statementId, itemToParameterConverter.convert(item));
  124.       }

  125.       var results = sqlSessionTemplate.flushStatements();

  126.       if (assertUpdates) {
  127.         if (results.size() != 1) {
  128.           throw new InvalidDataAccessResourceUsageException("Batch execution returned invalid results. "
  129.               + "Expected 1 but number of BatchResult objects returned was " + results.size());
  130.         }

  131.         var updateCounts = results.get(0).getUpdateCounts();

  132.         for (var i = 0; i < updateCounts.length; i++) {
  133.           var value = updateCounts[i];
  134.           if (value == 0) {
  135.             throw new EmptyResultDataAccessException("Item " + i + " of " + updateCounts.length
  136.                 + " did not update any rows: [" + items.getItems().get(i) + "]", 1);
  137.           }
  138.         }
  139.       }
  140.     }
  141.   }

  142.   private static class PassThroughConverter<T> implements Converter<T, T> {

  143.     @Override
  144.     public T convert(T source) {
  145.       return source;
  146.     }

  147.   }

  148. }