【发布时间】:2022-01-04 19:39:03
【问题描述】:
上下文
我们的PUT 端点本质上是通过首先在所有实体上调用DELETE,然后调用INSERT INTO 来执行UPSERT。
但是,在调试时,我看到 JPA 也发出了一些 UPDATE 调用,出于性能原因,我试图避免这种情况。我希望只有 2 个INSERT 调用:一个用于Parent,一个用于所有Child。
这似乎只发生在 @OneToMany 实体上。
如果有帮助,我们使用 EclipseLink 作为我们的 JPA 提供程序,并激活 batch-writing。该应用程序是使用 Spring Data 构建的。
型号
@Entity
@Table
@Data // org.lombok
@IdClass(ParentPrimaryKey.class)
public class Parent {
@Id
@Column(name = "product", updatable = false)
private String product;
@Id
@Column(name = "source", updatable = false)
private String source;
@Column
private String whatever;
@OneToMany(cascade = {CascadeType.ALL}, orphanRemoval = true)
@JoinColumns({
@JoinColumn(name = "product", referencedColumnName = "product"),
@JoinColumn(name = "source", referencedColumnName = "source")
})
private List<Child> details;
}
@Entity
@Table
@Data // org.lombok
@IdClass(ChildPrimaryKey.class)
public class Child {
@Id
@Column(name = "product", updatable = false)
private String product;
@Id
@Column(name = "source", updatable = false)
private String source;
@Id
@Column(name = "extra", updatable = false)
private String extra;
@Column
private String foo;
@Column
private String bar;
}
关联的当前日志
INSERT INTO PARENT (product, source, whatever) VALUES (?, ?, ?)
bind => [A, A, A]
bind => [B, B, A]
INSERT INTO CHILD (product, source, extra, foo, bar) VALUES (?, ?, ?, ?, ?)
bind => [A, A, 1, B, B]
bind => [A, A, 2, C, C]
bind => [A, A, 3, D, D]
bind => [B, B, 1, B, B]
bind => [B, B, 2, C, C]
bind => [B, B, 3, D, D]
UPDATE CHILD SET product = ?, source = ? WHERE ((product = ?) AND (source = ?) AND (extra = ?))
bind => [A, A, A, A, 1]
bind => [A, A, A, A, 2]
bind => [A, A, A, A, 3]
bind => [B, B, B, B, 1]
bind => [B, B, B, B, 2]
bind => [B, B, B, B, 3]
尝试修复
基于this answer,我尝试添加nullable=false:
public class Child {
@Id
@Column(name = "product", updatable = false, nullable = false)
private String product;
@Id
@Column(name = "source", updatable = false, nullable = false)
private String source;
@Id
@Column(name = "extra", updatable = false, nullable = false)
private String extra;
// the rest is identical...
}
结果日志
可以看出,这确实删除了UPDATE 调用,但它增加了将向我们的数据库发出的总查询量,因此意味着性能实际上会更差(它将从总共3 个请求,到 2N):
INSERT INTO PARENT (product, source, whatever) VALUES (?, ?, ?)
bind => [A, A, A]
INSERT INTO CHILD (product, source, extra, foo, bar) VALUES (?, ?, ?, ?, ?)
bind => [A, A, 1, B, B]
bind => [A, A, 2, C, C]
bind => [A, A, 3, D, D]
INSERT INTO PARENT (product, source, whatever) VALUES (?, ?, ?)
bind => [B, B, A]
INSERT INTO CHILD (product, source, extra, foo, bar) VALUES (?, ?, ?, ?, ?)
bind => [B, B, 1, B, B]
bind => [B, B, 2, C, C]
bind => [B, B, 3, D, D]
【问题讨论】:
-
您有一个来自 Parent->Child 的单向映射,并且已经映射它以便父拥有/控制子表中存在的外键值。不幸的是,这意味着 EclipseLink 必须在一个语句中插入子行(与子实体相关联),并在稍后单独处理父实体时对其进行更新。如果您希望在一个语句中完成,则需要将列完全绑定到子实体。最简单的方法是使 OneToMany 成为双向映射,以便 Child 知道其父级 - 有很多示例说明如何做到这一点
-
另外-我认为您可能弄错了映射,因为您有控制 Child.source 和 Child.product 列的重复映射-父通过 parent.details 映射写入它们,而子确实通过源/产品基本 id 映射。这不应该被允许。如果您将 OneToMany 标记为可插入/可更新=false,您的问题就会消失。只需将其添加到连接列定义中,使该映射只读。无论如何,您必须已经直接在每个子实体中设置基本列,所以没有意义
-
行为改变的原因,批量写入性能,再次是由于 oneToMany 是单向的。 EclipseLink 必须处理它管理的所有对象,并根据关系进行粗略的排序,以便它可以适当地查找和设置所有外键。这就是为什么您看到插入然后更新子记录是分开的。当您使该字段不可为空时,EclipseLink 检测到它并具有将 fk 处理组合到子插入中的特殊处理 - 但它是作为错误修复添加的,而不是具有批量写入支持的完整功能;
标签: spring-boot jpa spring-data-jpa spring-data eclipselink