尝试使用现有字段插入新实体时抛出PersistentObjectException的Spring

时间:2018-01-26 11:24:36

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

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的内容:

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”方式执行此操作?

1 个答案:

答案 0 :(得分: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生成的存储库方法的方法,请参阅Spring Data: Override save method

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