I have a service that copies values from one entity to another. The operation is multilevel in that it copies some values from entity A to entity B and then attempts to overwrite the set of child entities © from entity A to Entity B.
First i delete all of the C entities in B. and then add cloned versions of the C entities from the A parent instance.
I’ve tried various implementations where I tried to do this in one transaction but all have failed.
My current implementation works but is performed in two separate transactions. The first copies the fields from A to B and deletes the C records from B.
The second copies the C records from A to B.
For the transaction I’m using a Utility
Here Is the service implementation
package com.yieldmo.ymcuba.services.adbuilder;
import com.haulmont.cuba.core.EntityManager;
import com.haulmont.cuba.core.Persistence;
import com.haulmont.cuba.core.global.LoadContext;
import com.yieldmo.ymcuba.adbuilder.AdBuilderCreativeCopyService;
import com.yieldmo.ymcuba.entity.Creative;
import com.yieldmo.ymcuba.entity.CreativeConfig;
import com.yieldmo.ymcuba.services.store.YmStoreManager;
import com.yieldmo.ymcuba.util.EntityLoader;
import org.springframework.stereotype.Service;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import static com.haulmont.cuba.core.global.LoadContext.createQuery;
import static org.apache.commons.collections4.CollectionUtils.isNotEmpty;
@Service(AdBuilderCreativeCopyService.NAME)
public class AdBuilderCreativeCopier implements AdBuilderCreativeCopyService {
private static final String ID = "adBuilderCreativeId";
public static final String CREATIVE_FOR_CLONING_VIEW = "creative-manager-creative";
public static final String CREATIVE_BY_ADBUILDER_QUERY = "select c from ymcuba$Creative c" +
" where c.adBuilderCreativeId = :" + ID;
private Creative sourceCreative;
private EntityLoader entityLoader;
private YmStoreManager storeManager;
private Persistence persistence;
public AdBuilderCreativeCopier(EntityLoader entityLoader, YmStoreManager storeManager, Persistence persistence){
this.entityLoader = entityLoader;
this.storeManager = storeManager;
this.persistence = persistence;
}
@Override
public void copyToTargetCreatives(Set<Creative> targets, String adBuilderCreativeId){
this.sourceCreative = loadCreative(adBuilderCreativeId);
for(Creative target: targets){
dupeTo(target);
}
}
private Creative loadCreative(String adBuilderCreativeId) {
LoadContext.Query query = createQuery(CREATIVE_BY_ADBUILDER_QUERY).setParameter(ID, adBuilderCreativeId);
return entityLoader.loadEntity(Creative.class, CREATIVE_FOR_CLONING_VIEW, query);
}
private Creative dupeTo(Creative target){
target.setSharingVisibility(sourceCreative.getSharingVisibility());
target.setFormatGroup(sourceCreative.getFormatGroup());
target.setStatus(sourceCreative.getStatus());
persistCreative(target);
return target;
}
private void persistCreative(Creative target) {
// FIRST TRANSACTION
storeManager.transact((EntityManager em) -> {
em.merge(target);
clearConfigs(target, em);
});
// SECOND TRANSACTION
storeManager.transact((EntityManager em) -> {
cloneConfigs(target,em);
});
}
private void cloneConfigs(Creative target, EntityManager em) {
Set<CreativeConfig> configs= Optional.ofNullable(sourceCreative.getConfigs())
.map(this::cloneConfigs).get();
setConfigs(configs, target, em);
}
private Set<CreativeConfig> cloneConfigs(Set<CreativeConfig> configs) {
return configs.stream()
.map(CreativeConfig::clone)
.collect(Collectors.toSet());
}
public void clearConfigs(Creative target, EntityManager em){
target.getConfigs().forEach(config -> em.remove(config));
}
private void setConfigs(Set<CreativeConfig> clonedConfigs, Creative target, EntityManager em) {
if (isNotEmpty(clonedConfigs)) {
clonedConfigs.forEach((config) -> {
config.setCreative(target);
em.persist(config);
});
}
}
}
Here is the implementation of the YmStoreManager referenced in the service above
package com.yieldmo.ymcuba.services.store;
import javax.inject.Inject;
import org.springframework.stereotype.Component;
import com.haulmont.cuba.core.Persistence;
import com.haulmont.cuba.core.Transaction;
import com.yieldmo.ymcuba.ExcludeFromCodeCoverage;
@Component
public class YmStoreManager {
public static final String STORE_NAME = "yieldmo";
private final Persistence persistence;
@Inject
public YmStoreManager(Persistence persistence) {
this.persistence = persistence;
}
@ExcludeFromCodeCoverage
public <T> T transactReturning(Transaction.Callable<T> action) {
Transaction tx = persistence.createTransaction(STORE_NAME);
return tx.execute(STORE_NAME, action);
}
@ExcludeFromCodeCoverage
public void transact(Transaction.Runnable action) {
Transaction tx = persistence.createTransaction(STORE_NAME);
tx.execute(STORE_NAME, action);
}
}
When I tried to everything within the same transaction I got a variety of errors depending on what I tried. Here is an example
com.haulmont.cuba.core.global.RemoteException:
---
org.springframework.transaction.TransactionSystemException: Could not commit JPA transaction; nested exception is javax.persistence.RollbackException: Exception [EclipseLink-4002] (Eclipse Persistence Services - 2.6.2.cuba23): org.eclipse.persistence.exceptions.DatabaseException
Internal Exception: com.mysql.jdbc.exceptions.jdbc4.MySQLIntegrityConstraintViolationException: Duplicate entry '1967330851960070600-cta' for key 'crtv_name_unq'
Error Code: 1062
Call: INSERT INTO creative_config (group_element, grouping, name, value, creative_id) VALUES (?, ?, ?, ?, ?)
bind => [null, null, cta, none, 1967330851960070600]
Query: InsertObjectQuery(com.yieldmo.ymcuba.entity.CreativeConfig-8388692119038344336 [managed])
---
javax.persistence.RollbackException: Exception [EclipseLink-4002] (Eclipse Persistence Services - 2.6.2.cuba23): org.eclipse.persistence.exceptions.DatabaseException
Internal Exception: com.mysql.jdbc.exceptions.jdbc4.MySQLIntegrityConstraintViolationException: Duplicate entry '1967330851960070600-cta' for key 'crtv_name_unq'
Error Code: 1062
Call: INSERT INTO creative_config (group_element, grouping, name, value, creative_id) VALUES (?, ?, ?, ?, ?)
bind => [null, null, cta, none, 1967330851960070600]
Query: InsertObjectQuery(com.yieldmo.ymcuba.entity.CreativeConfig-8388692119038344336 [managed])
---
org.eclipse.persistence.exceptions.DatabaseException:
Internal Exception: com.mysql.jdbc.exceptions.jdbc4.MySQLIntegrityConstraintViolationException: Duplicate entry '1967330851960070600-cta' for key 'crtv_name_unq'
Error Code: 1062
Call: INSERT INTO creative_config (group_element, grouping, name, value, creative_id) VALUES (?, ?, ?, ?, ?)
bind => [null, null, cta, none, 1967330851960070600]
Query: InsertObjectQuery(com.yieldmo.ymcuba.entity.CreativeConfig-8388692119038344336 [detached])
---
com.mysql.jdbc.exceptions.jdbc4.MySQLIntegrityConstraintViolationException: Duplicate entry '1967330851960070600-cta' for key 'crtv_name_unq'
at com.haulmont.cuba.core.sys.ServiceInterceptor.aroundInvoke(ServiceInterceptor.java:129)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethodWithGivenArgs(AbstractAspectJAdvice.java:629)
at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethod(AbstractAspectJAdvice.java:618)
at org.springframework.aop.aspectj.AspectJAroundAdvice.invoke(AspectJAroundAdvice.java:70)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:168)
at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:92)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:213)
at com.yieldmo.ymcuba.adbuilder.AdBuilderCreativeCopyServiceUnMockedTest.testCloneCreatives(AdBuilderCreativeCopyServiceUnMockedTest.java:38)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.lang.reflect.Method.invoke(Method.java:498)
at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:47)
at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:242)
at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70)