Spring-Data JPA:保存引用现有实体的新实体

Posted

技术标签:

【中文标题】Spring-Data JPA:保存引用现有实体的新实体【英文标题】:Spring-Data JPA: save new entity referencing existing one 【发布时间】:2013-05-09 16:36:24 【问题描述】:

问题与下面的基本相同:

JPA cascade persist and references to detached entities throws PersistentObjectException. Why?

我正在创建一个引用现有分离实体的新实体。现在,当我将此实体保存在我的 spring 数据存储库中时,会引发异常:

org.springframework.dao.InvalidDataAccessApiUsageException: detached entity passed to persist

如果我们查看 spring data JPA 源代码中的 save() 方法,我们会看到:

public <S extends T> S save(S entity) 

    if (entityInformation.isNew(entity)) 
        em.persist(entity);
        return entity;
     else 
        return em.merge(entity);
    

如果我们查看 AbstractEntityInformation 中的 isNew()

public boolean isNew(T entity) 

    return getId(entity) == null;

所以基本上如果我保存()一个新实体(id == null),spring data 将始终调用persist,因此这种情况将始终失败。

这似乎是向集合添加新项目时非常典型的用例。

我该如何解决这个问题?

编辑 1:

注意:

此问题与How to save a new entity that refers existing entity in Spring JPA? 没有直接关系。详细说明假设您收到通过 http 创建新实体的请求。然后,您从请求中提取信息并创建您的实体和现有的引用实体。因此,它们将永远是分离的。

【问题讨论】:

卡在相同的场景中.. 有什么解决方案吗? 不,不是真的...当然你可以处理异常,然后先创建(持久化)新对象,并在持久化新对象后添加引用。但这并不适用于所有情况...... 另一种选择是确定我们是否处于这种情况:entity.getId() == null &amp;&amp; entity.getReferencedEntity().getId() != null,如果为真,则从数据库中加载引用的实体。 这似乎是 Spring JPA 的基本失败。仅仅因为您保存的实体是新的,它不应该暗示 persist() 将适用于其所有引用的实体... 【参考方案1】:

我有一个类似的问题,我试图保存一个新的实体对象,其中包含一个已经保存的实体对象。

我所做的是实现了Persistable并相应地实现了isNew()。

public class MyEntity implements Persistable<Long> 

    public boolean isNew() 
        return null == getId() &&
            subEntity.getId() == null;
    

或者您当然可以使用AbstractPersistable 并覆盖那里的isNew。

我不知道这是否会被认为是处理这个问题的好方法,但它对我来说非常好,而且感觉很自然。

【讨论】:

如果子实体恰好是 List<...> 与 @OneToMany 映射【参考方案2】:

我想出的最好的是

public final T save(T containable) 
    // if entity containable.getCompound already exists, it
    // must first be reattached to the entity manager or else
    // an exception will occur (issue in Spring Data JPA ->
    // save() method internal calls persists instead of merge)
    if (containable.getId() == null
            && containable.getCompound().getId() != null)
        Compound compound = getCompoundService()
                .getById(containable.getCompound().getId());
        containable.setCompound(compound);   
    
    containable = getRepository().save(containable);
    return containable; 

我们检查我们是否处于有问题的情况,如果是,只需通过其 id 从数据库中重新加载现有实体,并将新实体的字段设置为这个新加载的实例。然后它会被附加。

这要求新实体的服务持有对被引用实体服务的引用。这应该不是问题,因为无论如何您都在使用 spring,因此可以将服务添加为新的 @Autowired 字段。

然而,另一个问题(在我的情况下,这种行为实际上是需要的)您不能在保存新实体的同时更改引用的现有实体。所有这些更改都将被忽略。

重要提示:

在许多情况下,可能是您的情况,这可能要简单得多。您可以将实体管理器的引用添加到您的服务中:

@PersistenceContext
private EntityManager entityManager;

及以上if()块使用

containable = entityManager.merge(containable);

而不是我的代码(如果有效,则未经测试)。

在我的例子中,类是抽象的,@ManyToOne 中的targetEntity 因此也是抽象的。直接调用 entityManager.merge(containable) 会导致异常。但是,如果您的课程都是具体的,这应该可以工作。

【讨论】:

而不是重新加载实体,您可以使用“打开”锁进行重新连接:entityManager.lock(containable.getCompound(),LockModeType.NONE); 这将使您的分离实体持久而无需锁定或重新加载它,并且您将能够按预期进行保存。【参考方案3】:

@EmbeddedId 和作为 id 一部分的业务数据存在同样的问题。 知道实体是否是新实体的唯一方法是执行(em.find(entity)==null)?em.persist(entity):em.merge(entity)

但是spring-data只提供save()方法,没有办法用find()方法填充Persistable.isNew()方法。

【讨论】:

以上是关于Spring-Data JPA:保存引用现有实体的新实体的主要内容,如果未能解决你的问题,请参考以下文章

Java JPA Hibernate保存不插入引用的实体

Spring-Boot,无法使用spring-data JPA在MySql中保存unicode字符串

Spring-Boot,无法使用 spring-data JPA 在 MySql 中保存 unicode 字符串

如何使用 Spring Data JPA 保存具有手动分配标识符的实体?

JPA ManyToMany 映射问题(无法将同一实体映射到另一个)

JPA。如何子类化现有实体并保留其 ID?