【问题标题】:Spring throwing PersistentObjectException when trying to insert new entity with existing field尝试使用现有字段插入新实体时 Spring 抛出 PersistentObjectException
【发布时间】:2018-07-05 17:40:53
【问题描述】:

这里是 Spring Boot/Hibernate/JPA/MySQL。我有以下两个 JPA 实体:

@MappedSuperclass
public abstract class BaseEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String refId;
}

@MappedSuperclass
public abstract class BaseLookup extends BaseEntity {
    @JsonIgnore
    @NotNull
    private String name;

    @NotNull
    private String label;

    @JsonIgnore
    @NotNull
    private String description;
}

@Entity
@Table(name = "device_systems")
@AttributeOverrides({
        @AttributeOverride(name = "id", column=@Column(name="device_system_id")),
        @AttributeOverride(name = "refId", column=@Column(name="device_system_ref_id")),
        @AttributeOverride(name = "name", column=@Column(name="device_system_name")),
        @AttributeOverride(name = "label", column=@Column(name="device_system_label")),
        @AttributeOverride(name = "description", column=@Column(name="device_system_description"))
})
public class DeviceSystem extends BaseLookup {
}

@Entity
@Table(name = "devices")
@AttributeOverrides({
        @AttributeOverride(name = "id", column=@Column(name="device_id")),
        @AttributeOverride(name = "refId", column=@Column(name="device_ref_id"))
})
@JsonDeserialize(using = DeviceDeserializer.class)
class Device extends BaseEntity {
    @Column(name = "device_app_version")
    private String appVersion;

    @OneToOne(fetch = FetchType.EAGER, cascade = [CascadeType.PERSIST, CascadeType.MERGE])
    @JoinColumn(name = "device_system_id", referencedColumnName = "device_system_id")
    @NotNull
    @Valid
    private DeviceSystem system;

    @Column(name = "device_system_version")
    private String systemVersion;

    @Column(name = "device_model")
    private String model;
}

下面是CrudRepository"s 为他们:

public interface DevicePersistor extends CrudRepository<Device,Long> {
}

public interface DeviceSystemPersistor extends CrudRepository<DeviceSystem,Long> {
    @Query("FROM DeviceSystem WHERE label = 'ANDROID'")
    public DeviceSystem android();

    @Query("FROM DeviceSystem WHERE label = 'iOS'")
    public DeviceSystem iOS();

    @Query("FROM DeviceSystem WHERE name = :name")
    public DeviceSystem findByName(@Param(value = "name") String name);
}

这是我运行select * from device_systems;(这是 MySQL)时的结果:

mysql> select * from device_systems;
+------------------+--------------------------------------+--------------------+---------------------+-------------------------------+
| device_system_id | device_system_ref_id                 | device_system_name | device_system_label | device_system_description     |
+------------------+--------------------------------------+--------------------+---------------------+-------------------------------+
|                1 | 5e2bc70b-d570-43c7-b420-9add794f7c76 | Android            | ANDROID             | Google/Android based devices. |
|                2 | 312d82fa-b0db-4c9a-a356-4e2610373f3f | iOS                | iOS                 | Apple/iOS based devices.      |

device_systems 表包含静态/查找/参考数据,因此该表中应该有两条且只有两条(曾经)记录。在我的应用中,用户可以使用以下 curl 命令创建新设备:


curl -k -i -H "Content-Type: application/json" -X POST \
    -d '{ "appVersion" : "0.0.1", "system" : "Android", "systemVersion" : "7.0", "model" : "Samsung Galaxy S7 Edge" }' \ 
   https://localhost:9200/v1/devices

DeviceDeserializer 看起来像:

public class DeviceDeserializer extends JsonDeserializer<Device> {
    @Autowired
    private DeviceSystemPersistor deviceSystemPersistor;

    @Override
    public Device deserialize(JsonParser jsonParser, DeserializationContext ctxt) throws IOException, JsonProcessingException {
        JsonNode deviceNode = jsonParser.readValueAsTree();

        String appVersion = deviceNode.get("appVersion").asText();

        // Lookup the DeviceSystem record/entity/instance by the name provided in the JSON.
        // This is because the user can only specify 'Android' or 'iOS'; no other values allowed!
        DeviceSystem deviceSystem = deviceSystemPersistor.findByName(deviceNode.get("system").asText());

        String systemVersion = deviceNode.get("systemVersion").asText();
        String model = deviceNode.get("model").asText();

        return new Device(appVersion, deviceSystem, systemVersion, model);
    }
}

处理该 POST 的 DeviceController 方法如下所示:

@PostMapping
public void saveDevice(@RequestBody Device device) {
    devicePersistor.save(device);
}

在运行时,当我运行该 curl 命令时,出现以下异常:

nested exception is org.hibernate.PersistentObjectException: detached entity passed to persist: com.myapp.entities.DeviceSystem
    at org.springframework.orm.jpa.vendor.HibernateJpaDialect.convertHibernateAccessException(HibernateJpaDialect.java:299)
    at org.springframework.orm.jpa.vendor.HibernateJpaDialect.translateExceptionIfPossible(HibernateJpaDialect.java:244)
    at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.translateExceptionIfPossible(AbstractEntityManagerFactoryBean.java:488)
    at org.springframework.dao.support.ChainedPersistenceExceptionTranslator.translateExceptionIfPossible(ChainedPersistenceExceptionTranslator.java:59)
    at org.springframework.dao.support.DataAccessUtils.translateIfNecessary(DataAccessUtils.java:213)
    at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:147)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
    at org.springframework.data.jpa.repository.support.CrudMethodMetadataPostProcessor$CrudMethodMetadataPopulatingMethodInterceptor.invoke(CrudMethodMetadataPostProcessor.java:133)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
    at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:92)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)

堆栈跟踪很大,但表明 devicePersistor.save(device) 调用是 PersistentObjectException 的来源...

听起来像 JPA/Hibernate 不喜欢这样一个事实,即我试图坚持一个 Device 与现有的 DeviceSystem 相关联。需要明确的是,我不是试图创建一个新的DeviceSystem 实例,我只是试图保存我的新Device 实例以与一个现有的关联DeviceSystem如何以正确的“JPA”方式执行此操作?

【问题讨论】:

    标签: mysql hibernate jpa spring-boot spring-data-jpa


    【解决方案1】:

    首先,删除CascadeType.PERSIST。您不想保留一个预先存在的、分离的实体。我也会删除CascadeType.MERGE,因为你说DeviceSystem 代表只读数据。对于多对一关联,级联操作很少是一个好主意(作为旁注,@OneToOne 应该真的是@ManyToOne)。

    这里的问题是 Spring Data 检测到Device 是一个新实体,并决定调用EntityManager.persist() 而不是EntityManager.merge()。这意味着PERSIST 操作被级联到Device.system,并且由于在这个阶段Device.system 指向一个分离的实体,所以会抛出一个异常。

    即使您禁用了级联,您也可能会不断收到异常,因为Device.system 仍在引用一个分离的实体。

    最安全的解决方案是将Device.system 的值替换为对托管实体的引用,如下所示:

    @Transactional
    public Device saveDevice(Device device) {
        device.setSystem(em.getReference(DeviceSystem.class, device.getSystem().getId()));
        if (device.getId() == null) {
            em.persist(device);
            return device;
        } else {
            return em.merge(device);
        }
    }
    

    您可以将上述方法添加到存储库/服务,或覆盖默认的 CrudRepository.save() 方法 - 请参阅 Spring Data: Override save method 以了解自定义 Spring 生成的存储库方法的方法。

    另一种方法是离开CascadeType.MERGE 并让Device 实现Persistable,这样isNew() 总是返回false(这样EntityManager.merge() 总是被调用),但这是一个相当hacky 解决方法。此外,您可能会意外覆盖DeviceSystem 的状态。

    【讨论】:

    • 谢谢@crizzis (+1) - 但只是好奇,你为什么认为Device 需要@ManyToOneDeviceSystem 的关系?每台设备都有 1-and-only-1 设备系统(设备是either 和 Android 或 iOS 手机,绝不是两者!)。
    • 我不认为我跟随。每个Device 都有一个DeviceSystem。同时,一个DeviceSystem可以分配给多个设备。这是一个多对一的关联,不是吗?
    • 再次感谢@crizzis,但不,这不是我需要/想要的数据模型,除非你告诉我我出于某种奇怪的原因使用它。 Device 是电话。 DeviceSystem 是一个表,其中存储了手机的所有不同可用操作系统。目前我的应用程序仅支持 2 种可能的系统:Android 和 iOS。因此,device_systems 表中只有 2 条记录。并且 Android 的 Devices(电话)将与 same Android DeviceSystem 记录相关联; iOS 同上,有意义吗?
    • 这正是我的意思:'(many) 设备将关联到相同的 (one) android 设备系统'。
    猜你喜欢
    • 2013-09-28
    • 2014-04-06
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-04-17
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多