JPA批量插入优化 2023-04-19 11:27 ### 背景 项目中会有用到批量插入数据的场景,jpa的批量插入是saveAll()方法: ```java @Transactional public <S extends T> List<S> saveAll(Iterable<S> entities) { Assert.notNull(entities, "Entities must not be null!"); List<S> result = new ArrayList(); Iterator var3 = entities.iterator(); while(var3.hasNext()) { S entity = var3.next(); result.add(this.save(entity)); } return result; } ``` 可以看到方法实现还是调用了save(),所以Jpa的批量插入底层其实是一条条插入的。 ### 现状分析 如果我们要批量插入200条数据,调用saveAll()把数据集合传进去,那么就会依次执行200次save()。执行save()时,要在java服务里调用MySQL,调用外部服务本身就是服务中的耗时操作;而且每次执行sql都会自动开启事务执行完之后关闭事务,200次执行需要200次事务的开启和关闭。因此使用saveAll()做批量插入会非常耗时。 ### 优化方向 如何优化呢?我们可以采用拼接sql的方式,将批量插入做成一个sql,一次执行,这样只会和mysql交互一次,一次事务的开启关闭,省掉了很多时间。 Mysql中的批量插入语法: ```sql insert into table_name (column1, column2, column3, column4) values ("", "", "", ""), ("", "", "", ""), ("", "", "", ""); ``` 要在java中实现这样的批量插入,我们需要: 1、先拼接处sql 2、设置插入的值 3、执行 ### 实践 dao: ```java /** * 批量插入 * @param properties 属性名(javabean的哪些属性要存入db) * @param dataList 数据集合(javabean对象集合) * @return 成功执行条数 * @throws Exception */ public int batchInsert(String[] properties, List<ENTITY> dataList) throws Exception { if (CollectionUtil.isEmpty(dataList)) { return 0; } //获取表名 Class<?> entityClass = dataList.get(0).getClass(); Table annotation = entityClass.getAnnotation(Table.class); String tableName = annotation.name(); //转为数据库字段集合 String[] columns = new String[properties.length]; for (int i = 0; i < columns.length; i++) { columns[i] = StrUtil.toUnderlineCase(properties[i]); } //生成sql String sql = AssembleSqlUtil.generateInsertSql(tableName, columns, dataList.size()); Query query = entityManager.createNativeQuery(sql); //缓存field Map<String, Field> fieldMap = new HashMap<>(16); for (Field field : entityClass.getDeclaredFields()) { field.setAccessible(true); fieldMap.put(field.getName(), field); } //预编译sql for (int i = 0; i < dataList.size(); i++) { ENTITY entity = dataList.get(i); for (int j = 0; j < properties.length; j++) { Field field = fieldMap.get(properties[j]); query.setParameter((i * properties.length) + j + 1, field.get(entity)); } } //执行sql return query.executeUpdate(); } ``` util: ```java public class AssembleSqlUtil { /** * 生成插入sql * @param tableName 表名 * @param columns 列名 * @param dataSize 数据量(行数) * @return 类似'insert into tableName (column1,column2,column3) values (?,?,?),(?,?,?);' */ public static String generateInsertSql(String tableName, String[] columns, int dataSize) { StringBuilder sb = new StringBuilder(); sb.append("insert into ").append(tableName).append(" ("); for (int i = 0; i < columns.length; i++) { sb.append(columns[i]).append(","); if (i == columns.length - 1) { sb.deleteCharAt(sb.length() - 1).append(")").append(" values "); } } String placeholder = generatePlaceholder(columns.length); for (int i = 0; i < dataSize; i++) { sb.append(placeholder).append(","); if (i == dataSize - 1) { sb.deleteCharAt(sb.length() - 1).append(";"); } } return sb.toString(); } /** * 生成 ? 占位符, 如传入5,则返回(?,?,?,?,?) * @param n * @return */ private static String generatePlaceholder(int n) { StringBuilder sb = new StringBuilder("("); for (int i = 0; i < n; i++) { sb.append("?,"); if (i == n - 1) { sb.deleteCharAt(sb.length() - 1).append(")"); } } return sb.toString(); } ``` 用到的工具类依赖: ```xml <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.8.3</version> </dependency> ``` ### 使用示例 ```java @Test @Transactional(rollbackFor = Exception.class) @Rollback(value = false) public void test() { int n = 200; String[] properties = {"orderId", "icon", "name", "goodsId", "discountAmount", "refundAmount"}; List<MemberOrderDetail> dataList = new ArrayList<>(); int loop = n / 10; for (int i = 0; i < loop; i++) { String orderId = "orderId" + i; for (int j = 0; j < 10; j++) { MemberOrderDetail detail = new MemberOrderDetail(); detail.setOrderId(orderId); detail.setIcon("xxx"); detail.setName("name" + i); detail.setGoodsId("goodsId"); detail.setCount(1); detail.setDiscountAmount(1L); detail.setRefundAmount(0L); dataList.add(detail); } } int i = 0; int count = 3; try { while (count > 0) { long startTime = System.currentTimeMillis(); i = memberOrderDetailDao.batchInsert(properties, dataList); // i = memberOrderDetailDao.saveAll(dataList).size(); long endTime = System.currentTimeMillis(); System.out.println(i); System.out.println("耗时:" + (endTime - startTime)); count--; } } catch (Exception e) { throw new RuntimeException(e); } } ``` 其中MemberOrderDetail是一个domain: ```java import lombok.Data; import lombok.EqualsAndHashCode; import org.springframework.data.jpa.domain.support.AuditingEntityListener; import javax.persistence.*; import java.io.Serializable; @Entity @Table ( name ="member_order_detail") @Data @EntityListeners({AuditingEntityListener.class}) @EqualsAndHashCode(callSuper = true) public class MemberOrderDetail { @Id @GeneratedValue(strategy=GenerationType.IDENTITY) @Column(name = "id" ) private Long id; @Column(name = "order_id" ) private String orderId; @Column(name = "icon" ) private String icon; @Column(name = "name" ) private String name; @Column(name = "goods_id" ) private String goodsId; @Column(name = "count" ) private Integer count; @Column(name = "discount_amount" ) private Long discountAmount; @Column(name = "refund_amount" ) private Long refundAmount; } ``` ### 性能对比 #### 单元测试中执行耗时对比: | 数据量 | jpa saveAll() 耗时(ms) | batchInsert() 耗时(ms) | 平均相对提升 | 最差相对提升 | 最好相对提升 | | ------ | ---------------------- | ---------------------- | ------------ | ------------ | ------------ | | 100 | 553、549、565 | 433、12、11 | 73% | 23% | 98% | | 200 | 692、693、653 | 389、18、13 | 79% | 44% | 98% | | 500 | 1087、1033、1078 | 443、44、33 | 84% | 56% | 97% | | 1000 | 1855、1595、1702 | 490、69、55 | 88% | 74% | 97% | #### web服务中执行耗时对比: (服务中初次执行db操作所需的初始化时间不计算在内) | 数据量 | jpa saveAll() 耗时(ms) | batchInsert() 耗时(ms) | 平均相对提升 | | ------ | ------------------------------------- | --------------------------- | ------------ | | 100 | 397、127、123、80、90、94、64、66、70 | 8、5、5、10、74、7、9、6、8 | 92% | | 200 | 188、163、161、127、135、240 | 16、13、14、14、14、12 | 91% | | 500 | 613、396、366、316、326、324 | 37、28、31、32、27、22 | 92% | | 1000 | 782、1023、807、814、714、827 | 96、51、50、51、58、56 | 93% | >【平均相对提升】为去掉最长和最短耗时后的平均数。 可以看到,优化后,批量插入效率提升了92%左右。 以上就是jpa中批量插入的优化,其他持久层框架批量插入优化步骤也类似。 --END--
发表评论