Обзор:
Я создаю подпружиненное приложение, которое частично извлекает некоторые сущности из внешней службы REST и сравнивает их с предыдущими версиями сущностей, хранящихся локальнов базе данных.
Я ввожу EntityManager
с @PersistenceContext
и использую это для работы с базой данных, так как существует много типов сущностей, и тип изначально неизвестен модулю.Я мог бы получить JpaRepository
от фабрики, но число различных типов сущностей может возрасти, и я бы предпочел не полагаться на это, если это вообще возможно.
Проблема:
Когда модуль извлекает объект, который не хранится в базе данных, он выполняет некоторую бизнес-логику и затем пытается сохранить новый объект.
Класс Person
, которыйявляется одним из рассматриваемых объектов, содержит три поля типа Site
, которые часто содержат одни и те же объекты.
Когда я пытаюсь сохранить новый Person
, который имеет один и тот же объект Site
в несколькихполя с CascadeType.PERSIST
, я получаю EntityExistsException
(см. stacktrace (1)).
Когда я удаляю CascadeType.PERSIST из полей Site
и пытаюсь сохранить новый Person
, которыйимеет один и тот же объект Site
в нескольких полях, я получаю TransientPropertyValueException
(см. stacktrace (2)).
Мне кажется, я понимаю причины возникновения обоих исключений:
В первом случае это потому, что после первого поля сайта находится каскадДля второго поля это невозможно.
Второй случай, который я считаю, заключается в том, что аннотация @Transactional
пытается очистить транзакцию без экземпляров сайта.
Я пытался удалить аннотацию @Transactional
, начать и зафиксировать EntityTransaction самостоятельно, но я получаю IllegalStateException
(см. stacktrace (3)), хотя я думаю,это ожидается, так как весна должна обрабатывать транзакции сама.
Я посмотрел ответы на похожие вопросы (например, это , это ), но все они предлагают некоторые вариантыоб изменении CascadeType.
В другом вопросе кто-то предложил удостовериться, что рассматриваемые объекты были правильно оценены методом equals()
, поэтому я проверил в отладчике, и ((Person)newEntity).currentSite.equals(((Person)newEntity).homeSite)
оценивается как true.
Как я могу последовательно сохранять / объединять сущности с одним и тем же объектом в нескольких полях?
Редактировать: Я также тРазличные комбинации каскадных типов использовали с fetch = FetchType.EAGER
, но это не вызывает изменений в их соответствующих исключениях.
Редактировать 2: Я попытался использовать JpaRepository
вместо этогоиспользования EntityManager
, и эффективно получить тот же набор исключений в зависимости от того, какие типы каскадов я использую.
Если я использую PERSIST
, но не MERGE
, я получаю EntityNotFoundException
(см. stacktrace (4)), и если я использую оба PERSIST
и MERGE
, я получаю InvalidDataAccessApiUsageException` (см. stacktrace (5)).
Person:
@EqualsAndHashCode(callSuper = true)
@javax.persistence.Entity
@XmlDiscriminatorValue("person")
@XmlRootElement(name = "person")
@XmlAccessorType(XmlAccessType.FIELD)
@XmlSeeAlso({Subscriber.class})
public class Person extends MobileResource implements Serializable {
private static final Logger LOG = LogManager.getLogger(Person.class);
private String firstName;
private String surname;
public Person() {
super();
}
public Person(Long id) {
super(id);
}
public Person(Person that) {
super(that);
this.firstName = that.firstName;
this.surname = that.surname;
}
// getters && setters
}
MobileResource:
@EqualsAndHashCode(callSuper = true)
@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
@XmlRootElement(name = "resource")
@XmlDiscriminatorNode("@type")
@XmlAccessorType(XmlAccessType.FIELD)
@XmlSeeAlso({Vehicle.class, Person.class})
public abstract class MobileResource extends Resource implements Serializable {
private static final Logger LOG = LogManager.getLogger(MobileResource.class);
@ManyToOne(cascade = CascadeType.ALL)
private MobileResourceStatus status;
private Long incidentId;
@ManyToOne(fetch = FetchType.LAZY, cascade = {CascadeType.PERSIST, CascadeType.REMOVE, CascadeType.DETACH})
private Site homeSite;
@ManyToOne(fetch = FetchType.LAZY, cascade = {CascadeType.PERSIST, CascadeType.REMOVE, CascadeType.DETACH})
private Site currentSite;
@ManyToOne(fetch = FetchType.LAZY, cascade = {CascadeType.PERSIST, CascadeType.REMOVE, CascadeType.DETACH})
private Site relocationSite;
public MobileResource() {
super();
}
public MobileResource(Long id) {
super(id);
}
public MobileResource(MobileResource that) {
super(that);
this.status = that.status;
this.incidentId = that.incidentId;
this.homeSite = that.homeSite;
this.currentSite = that.currentSite;
this.relocationSite = that.relocationSite;
}
// getters && setters
}
Сайт:
@EqualsAndHashCode(callSuper = true)
@javax.persistence.Entity
public class Site extends Resource implements Serializable {
private static final Logger LOG = LogManager.getLogger(Site.class);
private String location;
public Site() {
super();
}
public Site(Long id) {
super(id);
}
public Site(Site that) {
super(that);
this.location = that.location;
}
}
Ресурс:
@EqualsAndHashCode
@MappedSuperclass
@XmlRootElement
@XmlSeeAlso({MobileResource.class})
public abstract class Resource implements Entity, Serializable {
private static final Logger LOG = LogManager.getLogger(Resource.class);
@Id
private Long id;
private String callSign;
@XmlPath(".")
private LatLon latLon;
private Long brigadeId;
private Long batchId;
@ManyToMany(cascade = CascadeType.ALL)
private List<Attribute> attributes;
@ManyToOne(fetch = FetchType.LAZY, cascade = {CascadeType.PERSIST, CascadeType.REMOVE, CascadeType.DETACH})
private ResourceType type;
public Resource() {
}
public Resource(Long id) {
this.id = id;
}
public Resource(Resource that) {
this.id = that.id;
this.callSign = that.callSign;
this.latLon = that.latLon;
this.attributes = that.attributes;
this.batchId = that.batchId;
this.brigadeId = that.brigadeId;
this.type = that.type;
}
// getters && setters
}
DefaultEntityMessageHandler:
@Component
public class DefaultEntityMessageHandler implements EntityMessageHandler {
@PersistenceContext
private EntityManager entityManager;
@Override
@Transactional
public void handleEntityMessage(EntityMessageData data, Message message) {
// business logic
if (newEntity != null) {
if (oldEntity != null)
entityManager.merge(newEntity);
else
entityManager.persist(newEntity);
}
}
}
Stacktrace (1):
2018-06-06 12:05:15,975 ERROR ActiveMQMessageConsumer - ID:cpt-9225-1528283097161-1:1:1:1 Exception while processing message: ID:cpt-8919-1528281875592-1:1:1:1:4
javax.persistence.EntityExistsException: A different object with the same identifier value was already associated with the session : [my.class.path.entity.resource.site.Site#738]
at org.hibernate.internal.ExceptionConverterImpl.convert(ExceptionConverterImpl.java:118)
at org.hibernate.internal.ExceptionConverterImpl.convert(ExceptionConverterImpl.java:157)
at org.hibernate.internal.ExceptionConverterImpl.convert(ExceptionConverterImpl.java:164)
at org.hibernate.internal.SessionImpl.firePersist(SessionImpl.java:813)
at org.hibernate.internal.SessionImpl.persist(SessionImpl.java:773)
at org.hibernate.jpa.event.internal.core.JpaPersistEventListener$1.cascade(JpaPersistEventListener.java:80)
at org.hibernate.engine.internal.Cascade.cascadeToOne(Cascade.java:467)
at org.hibernate.engine.internal.Cascade.cascadeAssociation(Cascade.java:392)
at org.hibernate.engine.internal.Cascade.cascadeProperty(Cascade.java:193)
at org.hibernate.engine.internal.Cascade.cascade(Cascade.java:126)
at org.hibernate.event.internal.AbstractSaveEventListener.cascadeBeforeSave(AbstractSaveEventListener.java:414)
at org.hibernate.event.internal.AbstractSaveEventListener.performSaveOrReplicate(AbstractSaveEventListener.java:252)
at org.hibernate.event.internal.AbstractSaveEventListener.performSave(AbstractSaveEventListener.java:182)
at org.hibernate.event.internal.AbstractSaveEventListener.saveWithGeneratedId(AbstractSaveEventListener.java:125)
at org.hibernate.jpa.event.internal.core.JpaPersistEventListener.saveWithGeneratedId(JpaPersistEventListener.java:67)
at org.hibernate.event.internal.DefaultPersistEventListener.entityIsTransient(DefaultPersistEventListener.java:189)
at org.hibernate.event.internal.DefaultPersistEventListener.onPersist(DefaultPersistEventListener.java:132)
...
Изменение каскадавведите MobileResource:
@ManyToOne(fetch = FetchType.LAZY, cascade = {CascadeType.REMOVE, CascadeType.DETACH})
private Site homeSite;
@ManyToOne(fetch = FetchType.LAZY, cascade = {CascadeType.REMOVE, CascadeType.DETACH})
private Site currentSite;
@ManyToOne(fetch = FetchType.LAZY, cascade = {CascadeType.REMOVE, CascadeType.DETACH})
Stacktrace (2):
2018-06-06 12:19:24,084 ERROR ExceptionMapperStandardImpl - HHH000346: Error during managed flush [org.hibernate.TransientPropertyValueException: object references an unsaved transient instance - save the transient instance before flushing : my.class.path.entity.resource.mobile_resource.person.Person.currentSite -> my.class.path.entity.resource.site.Site]
2018-06-06 12:19:24,093 ERROR ActiveMQMessageConsumer - ID:cpt-9436-1528283955454-1:1:1:1 Exception while processing message: ID:cpt-8919-1528281875592-1:1:1:1:8
org.springframework.dao.InvalidDataAccessApiUsageException: org.hibernate.TransientPropertyValueException: object references an unsaved transient instance - save the transient instance before flushing : my.class.path.entity.resource.mobile_resource.person.Person.currentSite -> my.class.path.entity.resource.site.Site; nested exception is java.lang.IllegalStateException: org.hibernate.TransientPropertyValueException: object references an unsaved transient instance - save the transient instance before flushing : my.class.path.entity.resource.mobile_resource.person.Person.currentSite -> my.class.path.entity.resource.site.Site
at org.springframework.orm.jpa.EntityManagerFactoryUtils.convertJpaAccessExceptionIfPossible(EntityManagerFactoryUtils.java:365)
at org.springframework.orm.jpa.vendor.HibernateJpaDialect.translateExceptionIfPossible(HibernateJpaDialect.java:227)
at org.springframework.orm.jpa.JpaTransactionManager.doCommit(JpaTransactionManager.java:540)
at org.springframework.transaction.support.AbstractPlatformTransactionManager.processCommit(AbstractPlatformTransactionManager.java:746)
at org.springframework.transaction.support.AbstractPlatformTransactionManager.commit(AbstractPlatformTransactionManager.java:714)
...
Stacktrace (3):
2018-06-06 13:29:35,594 ERROR ActiveMQMessageConsumer - ID:cpt-9864-1528288166188-1:1:1:1 Exception while processing message: ID:cpt-8919-1528281875592-1:1:1:1:9
java.lang.IllegalStateException: Not allowed to create transaction on shared EntityManager - use Spring transactions or EJB CMT instead
at org.springframework.orm.jpa.SharedEntityManagerCreator$SharedEntityManagerInvocationHandler.invoke(SharedEntityManagerCreator.java:254)
at com.sun.proxy.$Proxy114.getTransaction(Unknown Source)
at my.class.path.entity_controller.DefaultEntityMessageHandler.handleEntityMessage(DefaultEntityMessageHandler.java:60)
at my.class.path.entity_listener.listeners.IdExtractorMessageListener.onMessage(IdExtractorMessageListener.java:41)
...
Stacktrace (4)
Error starting ApplicationContext. To display the conditions report re-run your application with 'debug' enabled.
2018-06-06 15:26:36,143 ERROR SpringApplication - Application run failed
java.lang.IllegalStateException: Failed to execute CommandLineRunner
at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:793)
at org.springframework.boot.SpringApplication.callRunners(SpringApplication.java:774)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:335)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1246)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1234)
at my.class.path.OfficerSubscription.main(OfficerSubscription.java:44)
Caused by: org.springframework.orm.jpa.JpaObjectRetrievalFailureException: Unable to find my.class.path.entity.resource.site.Site with id 738; nested exception is javax.persistence.EntityNotFoundException: Unable to find my.class.path.entity.resource.site.Site with id 738
at org.springframework.orm.jpa.EntityManagerFactoryUtils.convertJpaAccessExceptionIfPossible(EntityManagerFactoryUtils.java:373)
at org.springframework.orm.jpa.vendor.HibernateJpaDialect.translateExceptionIfPossible(HibernateJpaDialect.java:227)
at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.translateExceptionIfPossible(AbstractEntityManagerFactoryBean.java:507)
at org.springframework.dao.support.ChainedPersistenceExceptionTranslator.translateExceptionIfPossible(ChainedPersistenceExceptionTranslator.java:61)
at org.springframework.dao.support.DataAccessUtils.translateIfNecessary(DataAccessUtils.java:242)
at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:153)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:185)
...
Stacktrace(5)
Error starting ApplicationContext. To display the conditions report re-run your application with 'debug' enabled.
2018-06-06 15:31:54,840 ERROR SpringApplication - Application run failed
java.lang.IllegalStateException: Failed to execute CommandLineRunner
at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:793)
at org.springframework.boot.SpringApplication.callRunners(SpringApplication.java:774)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:335)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1246)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1234)
at my.class.path.OfficerSubscription.main(OfficerSubscription.java:44)
Caused by: org.springframework.dao.InvalidDataAccessApiUsageException: Multiple representations of the same entity [my.class.path.entity.resource.site.Site#738] are being merged. Detached: [FJE84 - Uckfield]; Detached: [FJE84 - Uckfield]; nested exception is java.lang.IllegalStateException: Multiple representations of the same entity [my.class.path.entity.resource.site.Site#738] are being merged. Detached: [FJE84 - Uckfield]; Detached: [FJE84 - Uckfield]
at org.springframework.orm.jpa.EntityManagerFactoryUtils.convertJpaAccessExceptionIfPossible(EntityManagerFactoryUtils.java:365)
at org.springframework.orm.jpa.vendor.HibernateJpaDialect.translateExceptionIfPossible(HibernateJpaDialect.java:227)
at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.translateExceptionIfPossible(AbstractEntityManagerFactoryBean.java:507)
at org.springframework.dao.support.ChainedPersistenceExceptionTranslator.translateExceptionIfPossible(ChainedPersistenceExceptionTranslator.java:61)
at org.springframework.dao.support.DataAccessUtils.translateIfNecessary(DataAccessUtils.java:242)
at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:153)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:185)
at org.springframework.data.jpa.repository.support.CrudMethodMetadataPostProcessor$CrudMethodMetadataPopulatingMethodInterceptor.invoke(CrudMethodMetadataPostProcessor.java:135)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:185)
at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:92)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:185)
at org.springframework.data.repository.core.support.SurroundingTransactionDetectorMethodInterceptor.invoke(SurroundingTransactionDetectorMethodInterceptor.java:61)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:185)
at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:212)
at com.sun.proxy.$Proxy122.save(Unknown Source)
at my.class.path.OfficerSubscription.run(OfficerSubscription.java:81)
at my.class.path.OfficerSubscription$$FastClassBySpringCGLIB$$705870eb.invoke(<generated>)
...