Spring Boot/Hibernate/JPA/MySQL here. I have the following two JPA entities:
@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;
}
And the following to CrudRepository
"s for them:
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);
}
Here"s the results when I run select * from device_systems;
(this is 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. |
The device_systems
table contains static/lookup/reference data, and so there should be two and only two (ever) records in that table. In my app, a user can create a new device with the following curl command:
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
The DeviceDeserializer
looks like:
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);
}
}
And the DeviceController
method that handles that POST looks like:
@PostMapping
public void saveDevice(@RequestBody Device device) {
devicePersistor.save(device);
}
At runtime, when I run that curl command, I get the following exception:
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)
The stacktrace is huge but indicates the devicePersistor.save(device)
call as the source of the PersistentObjectException
...
It sounds like JPA/Hibernate doesn"t like the fact that I"m trying to persist a Device
that has an existing DeviceSystem
associated with it. To be clear, I"m not trying to create a new DeviceSystem
instance, I"m just trying to save my new Device
instance to be associated with an existing DeviceSystem
. How do I do this the correct "JPA" way?