Spring Data JPA:嵌套实体的批量插入

12

我有一个测试用例,需要将 100,000 个实体实例持久化到数据库中。我目前正在使用的代码可以实现这一点,但是直到所有数据都持久化到数据库中,需要最多 40 秒的时间。数据从一个大约为 15 MB 的 JSON 文件中读取。

之前我已经在另一个项目中的自定义存储库中实现了批量插入方法。然而,在那种情况下,我有很多顶级实体需要持久化,只有很少数量的嵌套实体。

在我的当前情况下,我有 5 个 Job 实体,它们包含约 30 个 JobDetail 实体的列表。一个 JobDetail 包含 850 到 1100 个 JobEnvelope 实体。

在写入数据库时,我使用默认的 save(Iterable<Job> jobs) 接口方法提交 Job 实体的列表。所有嵌套实体都具有 PERSIST 级联类型。每个实体都有自己的表。

启用批量插入的常规方式是实现一个自定义方法,例如 saveBatch,定期刷新缓存。但是,在这种情况下,我的问题是 JobEnvelope 实体。我没有使用 JobEnvelope 存储库使它们持久化,而是让 Job 实体的存储库处理它们。我使用 MariaDB 作为数据库服务器。

所以我的问题归结为以下几点:如何使 JobRepository 批量插入其嵌套实体?

这些是我关注的三个实体:

Job

@Entity
public class Job {
  @Id
  @GeneratedValue
  private int jobId;

  @OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.PERSIST, mappedBy = "job")
  @JsonManagedReference
  private Collection<JobDetail> jobDetails;
}

职位详情

@Entity
public class JobDetail {
  @Id
  @GeneratedValue
  private int jobDetailId;

  @ManyToOne(fetch = FetchType.EAGER, cascade = CascadeType.PERSIST)
  @JoinColumn(name = "jobId")
  @JsonBackReference
  private Job job;

  @OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.PERSIST, mappedBy = "jobDetail")
  @JsonManagedReference
  private List<JobEnvelope> jobEnvelopes;
}

就业信封

@Entity
public class JobEnvelope {
  @Id
  @GeneratedValue
  private int jobEnvelopeId;

  @ManyToOne(fetch = FetchType.EAGER, cascade = CascadeType.PERSIST)
  @JoinColumn(name = "jobDetailId")
  private JobDetail jobDetail;

  private double weight;
}
2个回答

17

确保正确配置Hibernate批处理相关的属性:

<property name="hibernate.jdbc.batch_size">100</property>
<property name="hibernate.order_inserts">true</property>
<property name="hibernate.order_updates">true</property>

重点是,如果连续的语句操纵相同的表,则可以将它们分批处理。如果出现向另一个表插入数据的语句,则必须在该语句之前中断并执行先前的批处理构造。通过设置 hibernate.order_inserts 属性,您授权 Hibernate 在构造批处理语句之前重新排序插入语句(hibernate.order_updates 对于更新语句具有相同的效果)。

jdbc.batch_size 是 Hibernate 使用的最大批量大小。尝试分析不同的值并选择显示用例中性能最佳的值。

请注意,如果使用 IDENTITY id 生成器,则禁用插入语句的批处理。更多详细信息请参见此处

对于 MySQL,您必须在连接 URL 中指定 rewriteBatchedStatements=true。为确保批处理按预期工作,请添加 profileSQL=true 以检查驱动程序发送到数据库的 SQL。更多细节请参见此处

如果您的实体已经版本化(用于乐观锁定),则为了利用批量更新(不影响插入),您还必须打开:

<property name="hibernate.jdbc.batch_versioned_data">true</property>

使用此属性可以告诉Hibernate,JDBC驱动程序能够在执行批量更新时返回受影响行的正确计数(用于执行版本检查)。您必须检查这是否适用于您的数据库/JDBC驱动程序。例如,在Oracle 11和早期版本中不起作用

您可能还希望在每个批处理后刷新和清除持久化上下文以释放内存,否则所有管理的对象将保留在持久化上下文中,直到关闭为止。

此外,您可能会发现这篇博客很有用,因为它很好地解释了Hibernate批处理机制的细节。


1
非常感谢您详细的回复。所以基本上不可能对使用“@GeneratedValue”注释的实体进行批量插入? - Ahatius
1
这是可能的,只是对于 IDENTITY ID 生成器不可行。适用于任何其他 ID 生成器。 - Dragan Bozanovic
啊,我明白了。它被设置为“AUTO”,MySQL不支持“SEQUENCE”,所以我正在研究“TABLE”生成。我猜自动模式选择了“IDENTITY”方法,因为没有序列表,而另一个方法也不被支持。等我有消息再回来汇报。 - Ahatius
很有可能,因为native是默认值,我认为如果你只指定@GeneratedValue,它会首先检查数据库是否支持IDENTITY - Dragan Bozanovic
4
天哪,非常感谢 - 这真的很奇妙。现在插入那些10万条目仅需5秒而不是40秒 :) - Ahatius

0
为了补充Dragan Bozanovic的先前回答,Hibernate有时会在构建批次之间的依赖关系图时遇到实体之间的循环关系,并且会在静默中停用批次执行顺序(请参见InsertActionSorter.sort(..)方法)。当发生这种情况时,让Hibernate跟踪此行为将是有趣的。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接