Java数据批量处理

1,156 阅读3分钟

在日常工作中,我们会遇到需要一次性保存多条数据到数据库的情况。在数据量不大的情况下,我们还可以使用单条数据的插入或更新来完成,但一旦数据量过大,处理单条数据的方式效率就会急剧下降。所以在这里介绍一下常用的数据批量处理的方式。

分析下原因,单条数据插入或更新不适用于大量数据是因为每次插入或更新都是一个事务,而事务的开启和关闭相较于数据插入是比较耗费时间的,所以当数据量大的时候,就需要频繁开启关闭事务,进而导致效率下降。

一、MyBatis配合<foreach>标签

-- mysql
    <insert id="saveUsers">
        insert into user (id, username, password)
        values
        <foreach collection="list" item="user" separator=",">
            (#{user.id}, #{user.username}, #{user.password})
        </foreach>
    </insert>

-- oracle第一种
    <insert id="saveUsers">
        INSERT ALL
        <foreach collection="list" item="user" separator=" " close="SELECT * FROM dual" index="index">
            INTO USER (id, username, password)
            VALUES (#{user.id}, #{user.username}, #{user.password})
        </foreach>
    </insert>

-- oracle第二种
    <insert id="saveUsers" parameterType="list">
        <foreach collection ="list" item="user" separator =";" open="begin" close = ";end;">
            INSERT INTO USER (id, username, password ) VALUES (#{user.id}, #{user.username}, #{user.password})
        </foreach>
    </insert>

二、jdbcTemplate的batchUpdate方法

public int[] batchUpdate(String sql, final BatchPreparedStatementSetter pss) throws DataAccessException {
    // do something
}

jdbcTemplate提供了batchUpdate方法,需要两个参数,一个是sql语句,另一个是一个BatchPreparedStatementSetter对象,这个对象不需要我们在外部实现,而是和Runable一样在方法内部实现,例子如下:

// 批量插入
public void batchUpdate(final List<User> list) {
    String sql = "INSERT INTO USER " +
            "(ID, " +
            "USERNAME, " +
            "PASSWORD)" +
            "VALUES " +
            "(?, ?, ?)";
    jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() {

        @Override
        public void setValues(PreparedStatement ps, int i) throws SQLException {
            User user = list.get(i);
            ps.setString(1, user.getId());
            ps.setString(2, user.getUsername());
            ps.setString(3, user.getPassword());
        }

        @Override
        public int getBatchSize() {
            return list.size();
        }
    });
}

// 批量更新
public void batchUpdate(final List<User> list) {
    String sql = "UPDATE USER SET " +
            "USERNAME = ?, " +
            "PASSWORD = ?" +
            "WHERE ID = ?";
    jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() {

        @Override
        public void setValues(PreparedStatement ps, int i) throws SQLException {
            User user = list.get(i);
            ps.setString(1, user.getUsername());
            ps.setString(2, user.getPassword());
            ps.setString(3, user.getId());
        }

        @Override
        public int getBatchSize() {
            return list.size();
        }
    });
}

不过这种方法在处理一些宽表的时候代码过于长,在此我对这个方法做了进一步抽象,考虑到反射会影响性能,所以字段使用Map来存储。

public interface JdbcBatchService<T> {

    /**
     * 批量更新
     *
     * @author fyh
     * @param tableName: 目标表名
     * @param columnMap: 更新字段名,驼峰命名
     * @param conditionColumns: 更新条件字段名
     * @param dataList: 更新源数据
     * @return void
     * @date 2024/5/14 9:40
     **/
    void batchUpdate(String tableName, Map<String, String> columnMap, Map<String, String> conditionColumns, List<T> dataList);
}


@Service
public class JdbcBatchServiceImpl<T> implements JdbcBatchService<T> {

    /**
     * JDBC服务接口
     */
    @Resource
    private JdbcTemplate jdbcTemplate;

    /**
     * 批量更新默认方法,特殊情况请自行实现接口
     *
     * @author fyh
     * @param tableName: 目标表名
     * @param columnMap: 更新字段名,<驼峰命名, 数据类型>
     * @param dataList: 更新源数据
     * @param conditionColumns: 更新条件字段名
     * @return void
     * @date 2024/5/14 9:40
     **/
    @Override
    public void batchUpdate(String tableName, Map<String, String> columnMap, Map<String, String> conditionColumns, List<T> dataList) {
        // 1、拼接sql
        StringBuilder sql = new StringBuilder("UPDATE ").append(tableName).append(" SET ");
        String updateColumnSql = columnMap.keySet().stream()
                .map(s -> StringUtils.camelToUnderline(s) + " = ?")
                .reduce((a, b) -> a + ", " + b)
                .orElse("");
        sql.append(updateColumnSql);
        if (!MapUtils.isEmpty(conditionColumns)) {
            sql.append(" WHERE ");
            String conditionSql = conditionColumns.keySet().stream()
                .map(s -> StringUtils.camelToUnderline(s) + " = ?")
                .reduce((a, b) -> a + " AND " + b)
                .orElse("");
            sql.append(conditionSql);
        }

        // 2、设置占位符内容
        jdbcTemplate.batchUpdate(sql.toString(), new BatchPreparedStatementSetter() {

            @Override
            public void setValues(PreparedStatement ps, int i) throws SQLException {
                T sourceData = dataList.get(i);
                TypeReference<Map<String, String>> type = new TypeReference<Map<String, String>>() {};
                Map<String, String> sourceDataMap = new ObjectMapper().convertValue(sourceData, type);

                int idx = 1;
                for (Map.Entry<String, String> entry : columnMap.entrySet()) {
                    ps.setString(idx++, MapUtils.getString(sourceDataMap, entry.getKey(), ""));
                }
                if (!MapUtils.isEmpty(conditionColumns)) {
                    for (Map.Entry<String, String> conditionColumn : conditionColumns.entrySet()) {
                        ps.setString(idx++, MapUtils.getString(sourceDataMap, conditionColumn.getKey(), ""));
                    }
                }
            }

            @Override
            public int getBatchSize() {
                return dataList.size();
            }
        });
    }
}

使用方式:

List<User> list = UserDao.getUserNeedUpdate();
// 1000条记录为一组进行批量插入,提高效率
List<List<User>> partitionList = Lists.partition(list, 1000);
Map<String, String> columnMap = new HashMap<String, String>() {
    {
        put("id", "String");
        put("userName", "String");
        put("password", "String");
    }
};

Map<String, String> conditionMap = new HashMap<String, String>() {
    {
        put("id", "String");
    }
};

for (List<User> user : partitionList) {
    jdbcBatchService.batchUpdate("USER", columnMap, conditionMap, user);
}

这里本来打算使用Map<String, String>,用数据库字段作为key,对应类型作为value,在setValues()方法中根据类型判断插入什么类型的数据,但因为时间和工作上暂时没有这个需求,所以没实现,后续继续完善。

三、这两种方法比较

<foreach>和jdbcTemplate的batchUpdate的效率作者本人没有去实际比较过,但是我在实际使用中更倾向于使用jdbcTemplate,主要原因是Java代码比xml里的sql代码更容易维护,具体使用哪个根据自己实际情况选择就行。
在使用时我们可以借助Lists.partition方法对数组进行切分,以合适的数量去进行批量处理操作。
当然,有更好的框架直接提供了批处理能力,也可以选择这些。

推荐阅读
segmentfault.com/a/119000002…
blog.csdn.net/qq_38737586…