【问题标题】:Why is JPA double-inserting child entities?为什么 JPA 双插入子实体?
【发布时间】:2015-08-10 20:10:28
【问题描述】:

我有一对实体,称它们为“Ticket”和“TicketComment”。

Ticket 有这个属性:

@OneToMany(mappedBy="ticket", cascade = CascadeType.ALL)
@OrderBy("date")
private List<TicketComment> comments = new ArrayList<>();

而且,TicketComment 有这个:

@ManyToOne
@JoinColumn(name="TKT_ID")
@NotNull
private Ticket ticket;

我认为,我正在使用的代码应该添加一个新注释。但是,似乎两次将新评论添加到数据库中:

Ticket ticket = ticketRepo.findOne(id);

TicketComment newComment = new TicketComment();
// ...
newComment.setTicket(ticket);
ticket.getComments().add(newComment);

ticketRepo.save(ticket);

我认为在没有重复子实体的情况下,我之前已经能够使用类似实体成功地做到这一点......我可能会遗漏什么?


更新:一种解决方法似乎是:

  1. 为“TicketComment”创建另一个 JpaRepository
  2. 在不使用ticketRepo 创建/添加评论的情况下将任何其他更新保存到票证。
  3. 创建新评论,与工单 (newComment.setTicket(ticket);) 关联,并使用 ticketCommentRepo 保存。

所以,有两种不同的保存,一种只应该影响父母,另一种应该只影响孩子。

我想将其简化为一次保存,但我不确定这是否可行?


更新 2:澄清观察到的症状。

如果我尝试简单地保存我的父“Ticket”实体并添加新的“TicketComment”,我会看到在数据库中创建了两条不同的记录,它们具有不同的主键,但具有相同的注释文本,并且相同,或几乎如此,时间戳。

例如,假设我发表了“看起来不错”之类的评论。即使我在调试器中确认在我的ticket.getComments() 中只显示一个“看起来不错”...在我再次加载工单的详细信息页面后,我看到如下内容:

我@2015-09-11 23:16:58:看起来不错

我@2015-09-11 23:16:59:看起来不错

当我查看正在准备的 SQL 语句的一些调试日志时,我看到我的 TicketComments 表调用了两个看起来相同的 INSERT 语句,然后是我的 Tickets 表的单个 UPDATE...

【问题讨论】:

  • 你能分享你的ticketRepo findOne 和保存方法吗?
  • 那些是由 Spring Data JPA 生成的。
  • 那之后你会保存 newComment 吗?
  • 您是否使用 Hibernate 作为 JPA 提供程序?
  • 那么,这是an earlier post 的副本。您被 Hibernate 错误击中。链接的帖子有多种解决方法,包括在将子实体添加到父实体之前保存子实体、使用Set 而不是List 等。

标签: java jpa spring-data-jpa


【解决方案1】:

我真的不明白你的意思 “将新评论添加到数据库两次”。您是否看到两个具有相同 PK 的注册表?

如果您希望每条评论在工单集合中出现一次,我认为您应该使用Set 接口而不是List。 无论如何,我将按部分解释我所知道的关于该 sn-p 代码的预期行为。

Ticket ticket = ticketRepo.findOne(id);
TicketComment newComment = new TicketComment();

票已经被保留了?没关系,因为你会save()它。然后创建一条新评论。

newComment.setTicket(ticket);

这是必要的,因为关联的所有者方位于 TicketComment 实体中,并保持对象模型一致(在某些情况下可能不需要)。

ticket.getComments().add(newComment);

当您在ticket.comments 集合上设置cascade=ALL 时,在ticket 上触发的所有操作(保存、更新等)都将传播到集合中的所有实体对象。

ticketRepo.save(ticket);

当您将ticket 传递给save() 方法时,该操作将传播到您刚刚添加的newComment 实体。 newComment 将被传递给 saveOrUpdate(),然后与 ticket 的关系保持不变。

【讨论】:

  • 从概念上讲,一个 Set 对我来说没有多大意义......首先,决定 equals() 的实现很奇怪......从技术上讲,有人/可以/两次发布相同的评论.我让 Spring Data JPA 为我填充评论时间。另外,这些有一个逻辑顺序来应用(发表评论的时间),JPA 可以为 Sets 做到这一点吗? (如,它是否支持使用SortedSet 而不是常规的Set
  • 如果我理解你所说的话。为什么 Set 有意义?首先是因为这是@manish 评论的错误(hibernate.atlassian.net/browse/HHH-6776)的解决方法(你正在遭受它)。其次,由于您表示与TicketComment 表中的字段/列的关联,每个TicketComment 可以属于一个Ticket.comments 集合并且在其中只出现一次(就像一个集合)。列表在概念上允许重复对象的外观,但您不能用您的表示保存这种状态。需要明确的是,TicketComment 的每一行都是一个唯一的评论(有一个 id
  • ...要清楚,表格的每一行都是唯一的注释。它具有标识它的唯一主键,如果另一行具有相同的 comment text 无关紧要,它们是不同的。您知道现实和适用的内容,但相同的文本并不意味着它是相同的评论。例如,注意 stackoverflow 对我的下一条评论的作用
  • ...要清楚,表格的每一行都是唯一的注释。它具有标识它的唯一主键,如果另一行具有相同的 comment text 无关紧要,它们是不同的。您知道现实和适用的内容,但相同的文本并不意味着它是相同的评论。例如,注意 stackoverflow 对我的下一条评论的作用
  • 是的,但是这些 ID 是由数据库生成的,因此在它们被持久化之前,它们并不存在于子对象中。关键还在于,您可能两次提交了相同的文本。但是,我只提交过一次。我同意这是 HHH-6776 的一个实例,但我不同意在我的情况下使用 Set 是正确的解决方法。我想我只需要为我的子元素创建一些琐碎的JpaRepository 接口。
【解决方案2】:

您无需在newComment 中指定ticket 只需删除以下行

newComment.setTicket(ticket);

这会起作用,因为cascade 会自动将ticket 分配给newComment,反之亦然 你实际上是双重分配,这可能是重复的原因。

【讨论】:

  • 关联的 owner 方位于 TicketComment (@ManyToOne) 中,因此无法删除该行以保持关系。级联只是传播保存操作。
  • 是的,我知道 TicketComment 是所有者,但这也使它成为该关系上的孩子,因此当您坚持时,如果 CascadeType.PERSISTCascadeType.ALL 应用于孩子,则父母孩子将保持不变
  • 您说"cascade 会自动将ticket 分配给newComment" 但cascade 不会这样做,因为它是双向关联。并删除该行违反 JPA 规范。
猜你喜欢
  • 2021-06-09
  • 2012-04-04
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-10-10
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多