使用带有 Hibernate 的 Spring data jpa 的具有相同标识符的不同对象

Posted

技术标签:

【中文标题】使用带有 Hibernate 的 Spring data jpa 的具有相同标识符的不同对象【英文标题】:A different object with the same identifier using Spring data jpa with Hibernate 【发布时间】:2019-10-06 15:42:45 【问题描述】:

我检查了不同的来源,但没有一个能解决我的问题,例如: 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> 


Parent 和 Child 具有一对多关系。 在我的主要方法中:

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();

【问题讨论】:

【参考方案1】:

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

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

【讨论】:

如果我只是保存父实例(与子实例没有任何关系)。即使我再次重新运行该应用程序,它也没有显示具有相同 ID 的错误消息。我认为hibernate已经处理了这个问题。在我的问题中编辑了这个 parent.pid 是您数据库中的主键吗?您是否允许具有相同 id 的多个记录? Java 代码中的 @Id 注释不会自动使其成为数据库中的主要注释。如果不是,它将解释您所描述的内容。 是的,我已将 pid 设置为我的数据库中的主键【参考方案2】:

在完整解释之前,请注意:尝试发布实际编译和工作的代码。

您的main() 无法编译, 您没有在父母和孩子之间建立完整的关系。 还尝试在发布的示例中明确划分事务。

您的代码是如何工作的

您正在对存储库调用 save。在下面,此方法调用entityManager.merge(),因为您自己设置了一个 id。 Merge 调用 SQL Select 来验证对象是否存在,然后为对象调用 SQL insert 或 update。 (建议用db中存在id的对象保存是错误的)

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

您插入父项 合并是级联的,你插入孩子(我们称之为childA

第二轮

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

解决它

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

这也解释了为什么当您将 Child 的 PK 更改为 long 时代码可以工作 - 没有办法搞砸并有一个不完整的 PK。

注意

上面的解决方案会弄乱孤儿。

我仍然认为原始解决方案更好,因为孤儿被删除了。 此外,将更新的解决方案添加到原始解决方案是值得的更新。 删除整个列表并重新插入它不太可能在负载下表现良好。 不幸的是,它会在父级的第一次合并时删除列表,并在父级的第二次合并时重新添加它们。 (这就是为什么不需要 clear 的原因)

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

更好的是,尝试查看解决方案并仅添加/替换父项的特定子项,而不是查看父项及其子项。这可能是最高效的。

原始解决方案

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

@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();
    

【讨论】:

你能解释一下为什么先保存parent,然后清除一个空数组(我的意思是parent.children.clear())吗? 更新了答案以解决您更新后的映射。 and then clear an empty array。那不是真的。在父节点上合并后,该数组是数据库中所有子节点的惰性代理。 非常感谢,详细的答案和很好的解释。还有2个问题。我使用了原始解决方案而没有清除数组。有用。它实际上创建删除 sql 以清除 db 中的子表。为什么会这样?如何检查数组中的实际元素(所有子代的代理) 抱歉大量更新,但我真的需要一个调试器来检查它。再次更新,原来删除是在第一次合并时发出的(父级有空子级,让我们删除孤儿),并且没有机会使用这种方法将旧父级的列表与新父级的列表合并。找到老父母会给你这样的机会。

以上是关于使用带有 Hibernate 的 Spring data jpa 的具有相同标识符的不同对象的主要内容,如果未能解决你的问题,请参考以下文章

带有 Hibernate 和 Ehcache 的 Spring 数据 JPA 不起作用

Spring 3,带有通用 DAO 的 Hibernate 4 AutoWired sessionFactory

Spring Boot / Thymeleaf / Hibernate:带有 Java 注解的 Sessionfactory Bean

带有注释和(动态)AbstractRoutingDataSource 的 Spring 3.1.3 + Hibernate 配置

使用带有 Hibernate 的 Spring data jpa 的具有相同标识符的不同对象

Hibernate 和 Spring Data JPA 无法处理带有特殊字符的 Oracle 表名?