使用带有Hibernate的Spring数据jpa的具有相同标识符的其他对象

时间:2019-05-20 09:33:32

标签: java spring hibernate spring-data-jpa

我检查了不同的来源,但没有一个能解决我的问题,例如: https://coderanch.com/t/671882/databases/Updating-child-DTO-object-MapsId

Spring + Hibernate : a different object with the same identifier value was already associated with the session

我的情况:我创建了2个类,如下所示是1个存储库:

@Entity
public class Parent{
  @Id
  public long pid;

  public String name;

  @OneToMany(mappedBy = "parent", cascade = CascadeType.ALL)
  public List<Child> children;
}

-------------------------------------------------------------------

@Entity
public class Child{
  @EmbeddedId
  public PK childPK = new PK();

  public String name;

  @ManyToOne
  @MapsId("parentPk")
  @JoinColumn(name = "foreignKeyFromParent")
  public Parent parent;

  @Embeddable
  @EqualsAndHashCode
  static class PK implements Serializable {
      public long parentPk;
      public long cid;
  }
}
------------------------------------------------------------------------

public interface ParentRepository extends JpaRepository<AmazonTest, Long> {
}

父母与子女之间存在一对多关系。 在我的主要方法中:

public static void main(String[] args) {
    @Autowired
    private ParentRepository parentRepository;

    Parent parent = new Parent();
    parent.pid = 1;
    parent.name = "Parent 1";

    Child child = new Child();
    List<Child> childList = new ArrayList<>();

    child.childPK.cid = 1;
    child.name = "Child 1";
    childList.add(child);

    parent.children= childList;

    parentRepository.save(parent);
    parentRepository.flush();
}


当我第一次运行该应用程序时,数据可以成功保存到数据库中。但是,如果我再次运行它,则会出现错误“异常:org.springframework.dao.DataIntegrityViolationException:具有相同标识符值的另一个对象已与该会话关联”。
我期望如果数据是新的,它将更新我的数据库,如果数据相同,什么也不会发生。我的代码有什么问题。

如果我让父母独立(与孩子没有任何关系)。即使我重新运行该应用程序,也不会出现任何错误。

编辑:但是,如果我在子实体中使用以下带有简单主键的实现,它将按预期工作。我可以重新运行该应用程序而不会出现错误。我还可以更改该值,例如child.name,它将反映在数据库中。

@Entity
public class Parent{
   @Id
   public long pid;

   public String name;

   @OneToMany(mappedBy = "parent", cascade = CascadeType.ALL)
   public List<Child> children;
}

-------------------------------------------------------------------

@Entity
public class Child{
   @Id
   public long cid;


   public String name;

   @ManyToOne
   @JoinColumn(name = "foreignKeyFromParent")
   public Parent parent;

}
------------------------------------------------------------------------

public interface ParentRepository extends JpaRepository<AmazonTest, Long> {
}

-------------------------------------------------------------------------
public static void main(String[] args) {
   @Autowired
   private ParentRepository parentRepository;

   Parent parent = new Parent();
   parent.pid = 1;
   parent.name = "Parent 1";

   Child child = new Child();
   List<Child> childList = new ArrayList<>();

   child.cid = 1;
   child.name = "Child 1";
   childList.add(child);

   parent.children= childList;

   parentRepository.save(parent);
   parentRepository.flush();
}

2 个答案:

答案 0 :(得分:1)

好吧,parent.pid是您的数据库主键。您只能将一个记录集保存到ID = 1的数据库中。这是预期的行为。

也许让自己熟悉@GeneratedValue以避免自己设置ID。

答案 1 :(得分:0)

在进行全面解释之前,请注意以下几点:尝试发布实际上可以编译并按宣传方式工作的代码。

  • 您的main()无法编译,
  • 您没有在父母与子女之间建立完整的关系。
  • 还尝试在发布的示例中明确划分交易范围。

代码的工作方式

您正在调用存储库上的save。在下面,此方法将在您自己设置ID时调用entityManager.merge()。合并调用SQL Select以验证对象是否存在,然后调用该对象的SQL插入或更新。 (使用db中存在的具有ID的对象保存的建议是错误的)

  • 在第一次运行中,对象不存在。

    • 您插入父母
    • 合并是级联的,您将插入子级(将其称为childA
  • 第二次运行

    • 合并选择父项(与childA
    • 我们比较会话中是否已有新的父母。 这是在SessionImpl.getEntityUsingInterceptor
    • 中完成的
    • 找到父母
    • 合并被级联到孩子
    • 再次,我们检查对象是否已在会话中。
    • 现在区别来了:
    • 取决于您如何设置子代与父代之间的关系,子代可能具有不完整的PK(并依赖于从与父代的关系中填充@MapsId的PK)。不幸的是,通过不完整的PK在会话中找不到该实体,但是稍后,保存时PK已完成,现在,您有2个具有相同密钥的冲突对象。

要解决

Child child = new Child();
child.parent = parent;
child.childPK.cid = 1;
child.childPK.parentPk = 1;

这也解释了为什么将Child的PK更改为很长时代码可以工作-没有办法搞砸它并拥有不完整的PK。

注意

以上解决方案使孤儿陷入困境。

我仍然认为,如果将孤儿移走,原始的解决方案会更好。 另外,将更新的版本添加到原始解决方案中也是值得的。 在负载下,删除整个列表并重新插入它的效果不太好。 不幸的是,它会在父项的第一次合并中删除列表,并在父项的第二次合并中重新添加列表。 (这就是为什么不需要清除的原因)

更好的是,只需找到父实体并对其进行更新(如其他答案所示)。

更好的是,尝试查看解决方案,仅添加/替换父级的特定子级,而不是查看/查看父级及其子级的倾斜。这可能是最出色的表现。

原始解决方案

我提出以下建议(请注意,不允许完全替换儿童列表,因为它是休眠代理)。

@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL, orphanRemoval = true)
public List<Child> children = new ArrayList<>();
@SpringBootTest
public class ParentOrphanRepositoryTest {

    @Autowired
    private ParentOrphanRepository parentOrphanRepository;

    @Test
    public void testDoubleAdd() {
        addEntity();
        addEntity();
    }

    @Transactional
    public void addEntity() {
        Parent parent = new Parent();
        parent.pid = 1;
        parent.name = "Parent 1";

        parent = parentOrphanRepository.save(parent);


        Child child = new Child();
        List<Child> childList = new ArrayList<>();

        child.parent = parent;
        child.childPK.cid = 1;
        child.name = "Child 1";
        childList.add(child);

        // parent.children.clear(); Not needed.
        parent.children.addAll(childList);
        parentOrphanRepository.save(parent);
        parentOrphanRepository.flush();
    }
}