MyBatis批量操作


源码基于MyBatis 3.4.6

如何使用

MyBatis内部提供了批量执行SQL的功能,当然这也只是对JDBC的一个包装。在介绍MyBatis中如何使用批量功能前,先来段原生的JDBC代码,看看如何执行一个批量SQL。大多数使用批量执行功能时,大多数都是对同一条SQL语句反复执行插入、更新、删除,只是传递的参数不一致。在接下来的代码中我将向MySQL中批量插入100000条数据。

/**
 * 实体类
 */
@Getter
@Setter
public class Example {

    private Integer id;

    private String name;

    private Integer age;
}

/**
 * 测试类
 */
public class ExampleMapperTest {

	// 准备100000条数据
    private List prepareData() {
        List examples = new ArrayList<>();
        Random random = new Random();
        for(int i = 1; i <= 100000; i++) {
            Example example = new Example();
            example.setId(i);
            example.setName("example-" + i);
            example.setAge(random.nextInt(101));
            examples.add(example);
        }
        return examples;
    }

    // 执行批量插入
    @Test
    public void testJDBCBatch() throws SQLException {
        String url = "jdbc:mysql://127.0.0.1:3306/test";
        long start = System.currentTimeMillis();
        Connection conn = DriverManager.getConnection(url, "root", "123456");
        List examples = prepareData();
        try {
            // 开启事务
            conn.setAutoCommit(false);
            PreparedStatement ps = conn.prepareStatement("INSERT INTO example(id, name, age) VALUES (?, ?, ?)");
            for(Example example : examples) {
                // 设置参数
                ps.setInt(1, example.getId());
                ps.setString(2, example.getName());
                ps.setInt(3, example.getAge());
                // 添加到批量语句集合中
                ps.addBatch();
            }
            // 执行批量操作
            int[] updateCounts = ps.executeBatch();
            // 提交事务
            conn.commit();
            long end = System.currentTimeMillis();
            System.out.println("批量执行耗时: " + (end - start) / 1000.0 + "s");
            for(int updateCount : updateCounts) {
                Assert.assertEquals(1, updateCount);
            }
        } catch (SQLException e) {
            conn.rollback();
            throw e;
        } finally {
            conn.close();
        }
    }
}

我对上述批量插入操作进行了一个简单的测试,并且统计了执行时间,在MySQL5.6中,批量插入100000条数据大约需要10秒左右。

接下来看看如何在MyBatis中如何执行批量操作,上面用到的实体类就不贴了。

/**
 * 获取SqlSession的工具类
 */
public class MyBatisUtils {

    private final static SqlSessionFactory sqlSessionFactory;

    static{
        try {
            Reader reader = Resources.getResourceAsReader("mybatis-config.xml");
            sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
        } catch (IOException e) {
            throw new RuntimeException("sqlSessionFactory init fail.", e);
        }
    }

    public static SqlSession getSqlSession(ExecutorType executorType) {
        return sqlSessionFactory.openSession(executorType);
    }
}

/**
 * Mapper接口,为了方便直接使用注解的方式。
 */
public interface ExampleMapper {

    @Insert(value = "INSERT INTO example(id, name, age) VALUES (#{id}, #{name}, #{age})")
    int insert(Example example);
}

/**
 * 测试类
 */
public class ExampleMapperTest {

    @Test
    public void testBatch() {
        // 准备数据,上面已经贴过了。
        List examples = prepareData();
        // 关键,如果使用批量功能,需要使用BatchExecutor而不是默认的SimpleExecutor
        try (SqlSession session = MyBatisUtils.getSqlSession(ExecutorType.BATCH)) {
            ExampleMapper exampleMapper = session.getMapper(ExampleMapper.class);
            for(Example example : examples) {
                // 因为使用的Executor是BatchExecutor, 并不会真的执行
                // 内部调用的是statement.addBatch()
                exampleMapper.insert(example);
            }
            // 执行批量操作, 内部调用的是statement.executeBatch()
            // 如果不需要返回值可以不用显示调用,commit方法内部会调用此方法
            List results = session.flushStatements();
            session.commit();
            Assert.assertEquals(1, results.size());
            BatchResult result = results.get(0);
            for(int updateCount : result.getUpdateCounts()) {
                Assert.assertEquals(1, updateCount);
            }
        }
    }
}

这里可能需要说明的是flushStatements方法了,此方法定义在SqlSession接口中,签名如下

List flushStatements();

此方法的作用就是将前面所有执行过的INSERT、UPDATE、DELETE语句真正刷新到数据库中。底层调用了JDBC的statement.executeBatch方法。这里有疑惑的肯定是这个方法的返回值了。通俗的说如果执行的是同一个方法并且执行的是同一条SQL,注意这里的SQL还没有设置参数,也就是说SQL里的占位符'?'还没有被处理成真正的参数,那么每次执行的结果共用一个BatchResult,真正的结果可以通过BatchResult中的getUpdateCounts方法获取。

按照上面例子,执行的是ExampleMapper中的insert方法,执行的SQL语句是INSERT INTO example(id, name, age) VALUES (?, ?, ?)。这100000万次执行的都是同一个方法同一条SQL语句。那么返回值中只有一个BatchResultgetUpdateCounts返回的数组大小是100000,代表着每一次执行的结果。

源码分析

SqlSession这个接口操作数据库的功能都是Executor接口实现的,而批量功能正是由上面提到过的BatchExecutor实现。SqlSession接口中的updateinsertdelete最终都会调用Executorupdate方法。

// 位于父类BaseExecutor中,BatchExecutor继承了BaseExecutor
@Override
public int update(MappedStatement ms, Object parameter) throws SQLException {
    ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
    if (closed) {
        throw new ExecutorException("Executor was closed.");
    }
    // 清空本地缓存
    clearLocalCache();
    // 对数据库做INSERT、UPDATE、DELETE操作,由子类实现
    return doUpdate(ms, parameter);
}

接下来看BatchExecutor

public class BatchExecutor extends BaseExecutor {

  public static final int BATCH_UPDATE_RETURN_VALUE = Integer.MIN_VALUE + 1002;
  /**
   * Statement集合,如果执行的方法不一样或者SQL语句不同
   * 都会创建一个Statement
   */
  private final List statementList = new ArrayList();
  /**
   * 与上面一一对应,用来保存每一个statement的执行结果。
   */
  private final List batchResultList = new ArrayList();
  // 当前的SQL语句
  private String currentSql;
  // XML中的每一个