【问题标题】:JPA saveAll forces UPDATE call after INSERT INTO on OneToManyJPA saveAll 在 OneToMany 上的 INSERT INTO 之后强制 UPDATE 调用
【发布时间】: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


【解决方案1】:

两种可能更适合您的选择。

第一个选项

保持其他一切不变:

@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", insertable = false, updatable = false),
            @JoinColumn(name = "source", referencedColumnName = "source", insertable = false, updatable = false)
    })
    private List<Child> details;
}

这将确保 Child 表中的外键仅由它们各自的 @Id 映射控制。

第二个选项

或者,您可以反过来,让 JPA 使用所谓的派生 ID 完全为您设置子实体实例中的值:

@Entity
@IdClass(ChildPrimaryKey.class)
public class Child {
    @Id
    @JoinColumns({
            @JoinColumn(name = "product", referencedColumnName = "product"),
            @JoinColumn(name = "source", referencedColumnName = "source")
    })
    Parent parent;

    @Id
    @Column(name = "extra")
    private String extra;

    // the rest is identical...
}

public class ChildPrimaryKey {
    ParentPrimaryKey parent;
    String extra;
}

这将确保根据父实例的值在子表中设置源和产品列。然后,Parent 的详细信息映射可以删除连接列定义,并声明它是由 Child 的父关系映射的:

@OneToMany(mappedBy = "parent",cascade = {CascadeType.ALL}, orphanRemoval = true)
private List<Child> details;

【讨论】:

  • 太棒了!第一个选项就像一个魅力。
  • 后续问题:有没有办法在单个 INSERT 语句中执行此操作,而不是 2 ? (使用 JPQL、Native 甚至存储过程?)
猜你喜欢
  • 2017-02-21
  • 1970-01-01
  • 1970-01-01
  • 2015-11-17
  • 1970-01-01
  • 2011-01-29
  • 1970-01-01
  • 1970-01-01
  • 2017-09-25
相关资源
最近更新 更多