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”方式执行此操作?
答案 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
的状态。