JPA 坚持变得越来越慢
JPA persist becomes slower and slower
这个场景使用一个简单的 oneToMany 关系,在两个方向上都保持级联。
多个:
@javax.persistence.Entity(name="Many")
public class Many {
@javax.persistence.ManyToOne(cascade = CascadeType.PERSIST)
protected One one;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long primaryKey;
public void setM(One one) {
this.one = one;
// comment out this line and performance becomes stable
this.one.getMany().add(this);
}
// other setters, getters, etc...
}
一个:
@javax.persistence.Entity(name="One")
public class One {
@javax.persistence.OneToMany(mappedBy="m", cascade = CascadeType.PERSIST)
protected java.util.Set<Many> many = com.google.common.collect.Sets.newHashSet();
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long primaryKey;
private String name;
// setters, getters, etc...
}
测试:
public static void main(String[] args) {
while(true) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("test-pu");
EntityManager em = emf.createEntityManager();
for (int i = 0; i < 100; i++) {
sw.reset();
sw.start();
persistMVs(emf, em);
System.err.println("Elapsed: " + sw.elapsed(TimeUnit.MILLISECONDS) + " ms");
}
em.close();
emf.close();
}
}
private static void persistMVs(EntityManagerFactory emf, EntityManager em) {
em.getTransaction().begin();
One one = getOrCreateOne(em);
for (int i = 0; i < 200; i++) {
Many many = new Many();
many.setM(one);
em.persist(many);
}
em.getTransaction().commit();
}
测试是一个无限循环,它试图插入与单个 One
实体关联的 20000 Many
个实体。每个循环都以创建一个新的 EntityManagerFactory
开始,以显示不断增加的数据库对性能的负面影响。
预期的行为是,实体的插入时间不会急剧增加,但是在每个 WHILE CYCLE 之后都会增加一个数量级。
备注:
- 我已经尝试过 eclipseLink、Hibernate、OpenJPA 并且都遇到了这种减速问题。
- 如果我不更新 One 的 Many 集合,则不会出现退化(请参阅 Many 的注释行)。
- 如果我不创建一个新的 EntityManagerFactory,那么即使在 50 万个实体之后也不会降级。
- 慢的部分是
em.persist(many);
(我测的)
- 查看 https://github.com/kupsef/OneToMany 并开始测试
gradle start
.
为什么数据库的初始大小在这种情况下很重要?我应该将此行为视为错误吗?
我认为问题出在this.one.getMany()
,因为在每次迭代中,越来越多的实体需要从这个关系中加载。
@OneToMany
关系在默认情况下是惰性的,因此当您调用 getMany()
JPA 提供者必须从集合中初始化每个实体,随着它的大小的增长,这需要更多的时间。
如果您不在每次迭代中创建新的 EntityManagerFactory
,上次迭代中的实体将保留在缓存中,因此执行的查询会少很多。
只是为了扩展 Predrag 的回答 - 遍历 1:M 关系不仅有引入实体的成本,而且任何扩展对象图,但这些实体仍然在持久单元内管理。因为您的测试为重复事务重复使用相同的 EntityManager,所以托管实体的缓存会随着每次迭代而持续增长。每次上下文与数据库同步时,都必须遍历并检查托管实体的缓存是否有更改——这发生在刷新、事务提交甚至查询时。
如果您必须引入大型对象图,可以采取的缓解措施是为每个事务边界释放和获取新的 EntityManager,或者偶尔刷新并清除 EntityManager。任一选项都允许它释放一些托管实体,因此它不需要在每次提交时检查所有实体是否有更改。
编辑>
您的 "Many" class 已覆盖 hashCode 方法,并使用其引用的 "One" 的哈希码及其主键来构建其哈希码。这会导致您在循环中坚持的每个 "Many" 都具有相同的哈希码,因为 GenerationType.IDENTITY 只能在插入语句发生时分配序列 - 这发生在同步期间 (flush/commit)。此方法可能会导致缓存查找(由于级联持久调用而导致提供程序在每次持久调用上遍历不断增长的对象模型时发生)花费越来越长的时间。
这个场景使用一个简单的 oneToMany 关系,在两个方向上都保持级联。
多个:
@javax.persistence.Entity(name="Many")
public class Many {
@javax.persistence.ManyToOne(cascade = CascadeType.PERSIST)
protected One one;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long primaryKey;
public void setM(One one) {
this.one = one;
// comment out this line and performance becomes stable
this.one.getMany().add(this);
}
// other setters, getters, etc...
}
一个:
@javax.persistence.Entity(name="One")
public class One {
@javax.persistence.OneToMany(mappedBy="m", cascade = CascadeType.PERSIST)
protected java.util.Set<Many> many = com.google.common.collect.Sets.newHashSet();
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long primaryKey;
private String name;
// setters, getters, etc...
}
测试:
public static void main(String[] args) {
while(true) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("test-pu");
EntityManager em = emf.createEntityManager();
for (int i = 0; i < 100; i++) {
sw.reset();
sw.start();
persistMVs(emf, em);
System.err.println("Elapsed: " + sw.elapsed(TimeUnit.MILLISECONDS) + " ms");
}
em.close();
emf.close();
}
}
private static void persistMVs(EntityManagerFactory emf, EntityManager em) {
em.getTransaction().begin();
One one = getOrCreateOne(em);
for (int i = 0; i < 200; i++) {
Many many = new Many();
many.setM(one);
em.persist(many);
}
em.getTransaction().commit();
}
测试是一个无限循环,它试图插入与单个 One
实体关联的 20000 Many
个实体。每个循环都以创建一个新的 EntityManagerFactory
开始,以显示不断增加的数据库对性能的负面影响。
预期的行为是,实体的插入时间不会急剧增加,但是在每个 WHILE CYCLE 之后都会增加一个数量级。
备注:
- 我已经尝试过 eclipseLink、Hibernate、OpenJPA 并且都遇到了这种减速问题。
- 如果我不更新 One 的 Many 集合,则不会出现退化(请参阅 Many 的注释行)。
- 如果我不创建一个新的 EntityManagerFactory,那么即使在 50 万个实体之后也不会降级。
- 慢的部分是
em.persist(many);
(我测的) - 查看 https://github.com/kupsef/OneToMany 并开始测试
gradle start
.
为什么数据库的初始大小在这种情况下很重要?我应该将此行为视为错误吗?
我认为问题出在this.one.getMany()
,因为在每次迭代中,越来越多的实体需要从这个关系中加载。
@OneToMany
关系在默认情况下是惰性的,因此当您调用 getMany()
JPA 提供者必须从集合中初始化每个实体,随着它的大小的增长,这需要更多的时间。
如果您不在每次迭代中创建新的 EntityManagerFactory
,上次迭代中的实体将保留在缓存中,因此执行的查询会少很多。
只是为了扩展 Predrag 的回答 - 遍历 1:M 关系不仅有引入实体的成本,而且任何扩展对象图,但这些实体仍然在持久单元内管理。因为您的测试为重复事务重复使用相同的 EntityManager,所以托管实体的缓存会随着每次迭代而持续增长。每次上下文与数据库同步时,都必须遍历并检查托管实体的缓存是否有更改——这发生在刷新、事务提交甚至查询时。
如果您必须引入大型对象图,可以采取的缓解措施是为每个事务边界释放和获取新的 EntityManager,或者偶尔刷新并清除 EntityManager。任一选项都允许它释放一些托管实体,因此它不需要在每次提交时检查所有实体是否有更改。
编辑> 您的 "Many" class 已覆盖 hashCode 方法,并使用其引用的 "One" 的哈希码及其主键来构建其哈希码。这会导致您在循环中坚持的每个 "Many" 都具有相同的哈希码,因为 GenerationType.IDENTITY 只能在插入语句发生时分配序列 - 这发生在同步期间 (flush/commit)。此方法可能会导致缓存查找(由于级联持久调用而导致提供程序在每次持久调用上遍历不断增长的对象模型时发生)花费越来越长的时间。