2
votes

I have three jpa entities I’m trying to get working together. BoxProfile, BoxProfileItemAssignment and BoxItem all code listed below. BoxProfileItemAssignment has a @EmbeddedId using @MapId to map the composite key.

BoxProfile has a set of BoxProfileItemAssignments, the assignments are a BoxItem and quantity value. I want to be able to persist BoxProfileItemAssignments while persisting a new BoxProfile. Each BoxItem in a BoxProfileItemAssignments has already been persisted when a BoxProfile is being created.

I'm using spring data JpaRepository interfaces to persist my BoxProfile entities and accessing the repo through a service layer BoxProfileService.

When I attempt to persist a new BoxProfile entity I get a PersistenceException due to a detached entity. I understand that the BoxItem that is nested in the BoxProfileItemAssignment entity I'm passing in is detached but I'm not looking to make and changes or updates to said entity I just want to use it create the BoxProfileItemAssignment entry.

After much research I can't seem to find an example of Cascading persisted with a nested composite key entity which itself has a nested entity.

I would appreciate if someone could tell me what the correct combination of annotations are to achieve my goal.

javax.persistence.PersistenceException: org.hibernate.PersistentObjectException: detached entity passed to persist: com.quadrimular.fyfe.fulfillment.domain.BoxItem at org.hibernate.jpa.spi.AbstractEntityManagerImpl.convert(AbstractEntityManagerImpl.java:1763) at org.hibernate.jpa.spi.AbstractEntityManagerImpl.convert(AbstractEntityManagerImpl.java:1677) at org.hibernate.jpa.spi.AbstractEntityManagerImpl.convert(AbstractEntityManagerImpl.java:1683) at org.hibernate.jpa.spi.AbstractEntityManagerImpl.persist(AbstractEntityManagerImpl.java:1187) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:606) at org.springframework.orm.jpa.SharedEntityManagerCreator$SharedEntityManagerInvocationHandler.invoke(SharedEntityManagerCreator.java:291) at com.sun.proxy.$Proxy53.persist(Unknown Source) at org.springframework.data.jpa.repository.support.SimpleJpaRepository.save(SimpleJpaRepository.java:394) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:606) at org.springframework.data.repository.core.support.RepositoryFactorySupport$QueryExecutorMethodInterceptor.executeMethodOn(RepositoryFactorySupport.java:442) at org.springframework.data.repository.core.support.RepositoryFactorySupport$QueryExecutorMethodInterceptor.doInvoke(RepositoryFactorySupport.java:427) at org.springframework.data.repository.core.support.RepositoryFactorySupport$QueryExecutorMethodInterceptor.invoke(RepositoryFactorySupport.java:381) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:99) at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:267) at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:96) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:136) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) at org.springframework.data.jpa.repository.support.CrudMethodMetadataPostProcessor$CrudMethodMetadataPopulatingMethodIntercceptor.invoke(CrudMethodMetadataPostProcessor.java:122) 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) at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:207) at com.sun.proxy.$Proxy65.save(Unknown Source) at com.quadrimular.fyfe.fulfillment.service.BoxProfileServiceImpl.addBoxProfile(BoxProfileServiceImpl.java:48) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:606) at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:317) at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:190) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:157) at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:99) at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:267) at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:96) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:207) at com.sun.proxy.$Proxy66.addBoxProfile(Unknown Source) at com.quadrimular.fyfe.fulfillment.integration.ITBoxProfile.addBoxProfileDatabase(ITBoxProfile.java:96) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:606) at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:47) at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12) at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:44) at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17) at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:26) at org.springframework.test.context.junit4.statements.RunBeforeTestMethodCallbacks.evaluate(RunBeforeTestMethodCallbacks.java:73) at org.springframework.test.context.junit4.statements.RunAfterTestMethodCallbacks.evaluate(RunAfterTestMethodCallbacks.java:82) at org.springframework.test.context.junit4.statements.SpringRepeat.evaluate(SpringRepeat.java:73) at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:271) at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:217) at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:83) at org.junit.runners.ParentRunner$3.run(ParentRunner.java:238) at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:63) at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:236) at org.junit.runners.ParentRunner.access$000(ParentRunner.java:53) at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:229) at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61) at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:68) at org.junit.runners.ParentRunner.run(ParentRunner.java:309) at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:163) at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:50) at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38) at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:467) at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:683) at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:390) at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:197) Caused by: org.hibernate.PersistentObjectException: detached entity passed to persist: com.quadrimular.fyfe.fulfillment.domain.BoxItem at org.hibernate.event.internal.DefaultPersistEventListener.onPersist(DefaultPersistEventListener.java:139) at org.hibernate.internal.SessionImpl.firePersist(SessionImpl.java:801) at org.hibernate.internal.SessionImpl.persist(SessionImpl.java:794) at org.hibernate.jpa.event.internal.core.JpaPersistEventListener$1.cascade(JpaPersistEventListener.java:97) at org.hibernate.engine.internal.Cascade.cascadeToOne(Cascade.java:350) at org.hibernate.engine.internal.Cascade.cascadeAssociation(Cascade.java:293) at org.hibernate.engine.internal.Cascade.cascadeProperty(Cascade.java:161) at org.hibernate.engine.internal.Cascade.cascade(Cascade.java:118) at org.hibernate.event.internal.AbstractSaveEventListener.cascadeBeforeSave(AbstractSaveEventListener.java:432) at org.hibernate.event.internal.AbstractSaveEventListener.performSaveOrReplicate(AbstractSaveEventListener.java:265) at org.hibernate.event.internal.AbstractSaveEventListener.performSave(AbstractSaveEventListener.java:194) at org.hibernate.event.internal.AbstractSaveEventListener.saveWithGeneratedId(AbstractSaveEventListener.java:137) at org.hibernate.jpa.event.internal.core.JpaPersistEventListener.saveWithGeneratedId(JpaPersistEventListener.java:84) at org.hibernate.event.internal.DefaultPersistEventListener.entityIsTransient(DefaultPersistEventListener.java:206) at org.hibernate.event.internal.DefaultPersistEventListener.onPersist(DefaultPersistEventListener.java:149) at org.hibernate.internal.SessionImpl.firePersist(SessionImpl.java:801) at org.hibernate.internal.SessionImpl.persist(SessionImpl.java:794) at org.hibernate.jpa.event.internal.core.JpaPersistEventListener$1.cascade(JpaPersistEventListener.java:97) at org.hibernate.engine.internal.Cascade.cascadeToOne(Cascade.java:350) at org.hibernate.engine.internal.Cascade.cascadeAssociation(Cascade.java:293) at org.hibernate.engine.internal.Cascade.cascadeProperty(Cascade.java:161) at org.hibernate.engine.internal.Cascade.cascadeCollectionElements(Cascade.java:379) at org.hibernate.engine.internal.Cascade.cascadeCollection(Cascade.java:319) at org.hibernate.engine.internal.Cascade.cascadeAssociation(Cascade.java:296) at org.hibernate.engine.internal.Cascade.cascadeProperty(Cascade.java:161) at org.hibernate.engine.internal.Cascade.cascade(Cascade.java:118) at org.hibernate.event.internal.AbstractSaveEventListener.cascadeAfterSave(AbstractSaveEventListener.java:460) at org.hibernate.event.internal.AbstractSaveEventListener.performSaveOrReplicate(AbstractSaveEventListener.java:294) at org.hibernate.event.internal.AbstractSaveEventListener.performSave(AbstractSaveEventListener.java:194) at org.hibernate.event.internal.AbstractSaveEventListener.saveWithGeneratedId(AbstractSaveEventListener.java:125) at org.hibernate.jpa.event.internal.core.JpaPersistEventListener.saveWithGeneratedId(JpaPersistEventListener.java:84) at org.hibernate.event.internal.DefaultPersistEventListener.entityIsTransient(DefaultPersistEventListener.java:206) at org.hibernate.event.internal.DefaultPersistEventListener.onPersist(DefaultPersistEventListener.java:149) at org.hibernate.event.internal.DefaultPersistEventListener.onPersist(DefaultPersistEventListener.java:75) at org.hibernate.internal.SessionImpl.firePersist(SessionImpl.java:811) at org.hibernate.internal.SessionImpl.persist(SessionImpl.java:784) at org.hibernate.internal.SessionImpl.persist(SessionImpl.java:789) at org.hibernate.jpa.spi.AbstractEntityManagerImpl.persist(AbstractEntityManagerImpl.java:1181) ... 72 more

BoxProfile test method

@Test
    @ExpectedDatabase(value = "boxProfileData-add.xml", assertionMode = DatabaseAssertionMode.NON_STRICT)
    public void addBoxProfileDatabase() throws Exception {
        BoxProfileItemAssignment itemAssignment = new BoxProfileItemAssignment.Builder(BOX_ITEM_ONE, new BigDecimal("2.88")).build();
        BoxProfile original = new BoxProfile.Builder("example 3").itemAssignments((new HashSet(Arrays.asList(itemAssignment)))).sizes(new HashSet(Arrays.asList(BOX_SIZE))).selected(true).sequencer(3).build();

        BoxProfile returned = boxProfileService.addBoxProfile(original);

        assertNotNull(returned);
        assertThat(returned.getId(), instanceOf(Long.class));
        assertNotNull(returned.getId());
    }

BoxProfileRepository.java

public interface BoxProfileRepository extends JpaRepository<BoxProfile, Long> {

}

BoxProfileServiceImpl.java

@Service
@Transactional("mainTransactionManager")
public class BoxProfileServiceImpl implements BoxProfileService {

    private static final Logger LOG = LoggerFactory
            .getLogger(BoxProfileServiceImpl.class);

    private BoxProfileRepository repo;
    private BoxItemService boxItemService;

    @Autowired
    public BoxProfileServiceImpl(BoxProfileRepository repo, BoxItemService boxItemService) {
        this.repo = repo;
        this.boxItemService = boxItemService;
    }

    @Transactional("mainTransactionManager")
    public BoxProfile addBoxProfile(BoxProfile boxProfile) {
        LOG.debug("Adding boxProfile with information: " + boxProfile);
        BoxProfile toReturn = repo.save(boxProfile);
        LOG.debug("BoxProfile id: " + toReturn);
        return toReturn;
    }
}

BoxProfile.java

    @Entity
    @Table
    public class BoxProfile implements Serializable {

        private static final long serialVersionUID = 9091824819977165224L;

        @Id
        @GeneratedValue
        private Long id;
        private String description;
        private boolean selected;
        private int sequencer;

        @ManyToMany(fetch = FetchType.EAGER)
        @JoinTable(name = "boxProfileSizes", joinColumns = { @JoinColumn(name = "BOX_PROFILE_ID", referencedColumnName = "id") }, inverseJoinColumns = { @JoinColumn(name = "SIZE_ID", referencedColumnName = "id") })
        private Set<BoxSize> sizes;

        @OneToMany(mappedBy = "boxProfile", cascade={CascadeType.PERSIST, CascadeType.REMOVE}, fetch = FetchType.EAGER)
        private Set<BoxProfileItemAssignment> itemAssignments;

        // Modification times
        private Date creationTime;
        private Date modificationTime;

        @PreUpdate
        public void preUpdate() {
            setModificationTime(new Date());
        }

        @PrePersist
        public void prePersist() {
            Date now = new Date();
            setCreationTime(now);
            setModificationTime(now);
        }

        public BoxProfile() {
        }

        private BoxProfile(Builder b) {
            this.description = b.description;
            this.id = b.id;
            this.selected = b.selected;
            this.sequencer = b.sequencer;
            this.sizes = b.sizes;
        }

        public static class Builder {
            // Mandatory Fields
            private final String description;
            // Optional Fields
            private Long id = null;
            private boolean selected = false;
            private int sequencer = -1;

            private Set<BoxSize> sizes = new HashSet<BoxSize>();
            private Set<BoxProfileItemAssignment> itemAssignments = new HashSet<BoxProfileItemAssignment>();

            public Builder(String description) {
                this.description = description;
            }

            public Builder sequencer(int sequencer) {
                this.sequencer = sequencer;
                return this;
            }

            public Builder sizes(Set<BoxSize> sizes) {
                this.sizes = sizes;
                return this;
            }

            public Builder addSize(BoxSize size) {
                this.sizes.add(size);
                return this;
            }

            public Builder itemAssignments(
                    Set<BoxProfileItemAssignment> itemAssignments) {
                this.itemAssignments = itemAssignments;
                return this;
            }

            public Builder id(Long id) {
                this.id = id;
                return this;
            }

            public Builder selected(boolean selected) {
                this.selected = selected;
                return this;
            }

            public BoxProfile build() {
                BoxProfile boxProfile = new BoxProfile(this);
                // Add the new box profile to the box profile assigned fish.
                for (BoxProfileItemAssignment assignment : itemAssignments) {
                    assignment.setBoxProfile(boxProfile);
                }
                // Set the updated fish assignments on the box profile
                boxProfile.setItemAssignements(itemAssignments);

                return boxProfile;
            }
        }

        // Getters setters hashcode equals to string



    }

BoxProfileItemAssignment.java

@Entity
@Table(name = "BOX_PROFILE_ITEM")
public class BoxProfileItemAssignment implements Serializable{

    private static final long serialVersionUID = 3331165661732043732L;

    @EmbeddedId
    private BoxProfileItemAssignmentId id = new BoxProfileItemAssignmentId();

    @MapsId("boxProfileId")
    @ManyToOne(fetch=FetchType.LAZY)
    @JoinColumn(name = "BOX_PROFILE_ID", referencedColumnName = "id")
    private BoxProfile boxProfile;

    @MapsId("itemId")
    @ManyToOne(fetch = FetchType.EAGER)
    @JoinColumn(name = "ITEM_ID", referencedColumnName = "id")
    private BoxItem item;

    private BigDecimal quantity;

    // Modification times
    private Date creationTime;
    private Date modificationTime;

    @PreUpdate
    public void preUpdate() {
        setModificationTime(new Date());
    }

    @PrePersist
    public void prePersist() {
        Date now = new Date();
        setCreationTime(now);
        setModificationTime(now);
    }

    private BoxProfileItemAssignment(Builder b){
        this.boxProfile = b.boxProfile;
        this.item = b.item;
        this.quantity = b.quantity;
        this.id = b.id;
    }

    public BoxProfileItemAssignment(){}

    public static class Builder {

        private final BoxItem item;
        private final BigDecimal quantity;

        private BoxProfile boxProfile;
        private BoxProfileItemAssignmentId id = new BoxProfileItemAssignmentId();

        public Builder(BoxItem item, BigDecimal quantity){
            this.item = item;
            this.quantity = quantity;
        }
        public Builder boxProfile(BoxProfile boxProfile){
            this.boxProfile = boxProfile;
            return this;
        }

        public Builder id(BoxProfileItemAssignmentId id){
            this.id = id;
            return this;
        }

        public BoxProfileItemAssignment build(){
            return new BoxProfileItemAssignment(this);
        }
    }

    // Getters setters hashcode equals to string




}

BoxProfileItemAssignmentId

@Embeddable
public class BoxProfileItemAssignmentId implements Serializable{

    private static final long serialVersionUID = -7936926474216068447L;

    @Column(name = "BOX_PROFILE_ID")
    private Long boxProfileId;
    @Column(name = "ITEM_ID")
    private Long itemId;



    public BoxProfileItemAssignmentId(){}

    private BoxProfileItemAssignmentId(Builder b){
        this.boxProfileId = b.boxProfileId;
        this.itemId = b.itemId;
    }
    public static class Builder{
        private final Long boxProfileId;
        private final Long itemId;

        public Builder(Long boxProfileId, Long itemId){
            this.boxProfileId = boxProfileId;
            this.itemId = itemId;
        }

        public BoxProfileItemAssignmentId build(){
            return new BoxProfileItemAssignmentId(this);
        }
    }

    // Getters setters hashcode equals to string




}

BoxItem.java

@Entity
@Table
public class BoxItem implements Serializable {

    private static final long serialVersionUID = -6146188094809573420L;

    @Id
    @GeneratedValue
    private Long id;

    @NotNull
    private BoxItemType type;
    @NotNull
    private MeasurementUnit unit;
    @NotNull
    @Size(min=2, max=30)
    private String name;
    @NotNull
    private BigDecimal costPerUnit;

    // Modification times
    private Date creationTime;
    private Date modificationTime;

    @PreUpdate
    public void preUpdate() {
        modificationTime = new Date();
    }

    @PrePersist
    public void prePersist() {
        Date now = new Date();
        creationTime = now;
        modificationTime = now;
    }
    public BoxItem(){}

    private BoxItem(Builder b){
        this.type = b.type;
        this.name = b.name;
        this.costPerUnit = b.costPerUnit;
        this.id = b.id;
        this.unit = b.unit;
    }

    public static class Builder{
        private BoxItemType type;
        private MeasurementUnit unit;
        private String name;
        private BigDecimal costPerUnit;

        private Long id;

        public Builder(String name, BoxItemType type, MeasurementUnit unit, BigDecimal costPerUnit){
            this.name = name;
            this.type = type;
            this.unit = unit;
            this.costPerUnit = costPerUnit;
        }

        public Builder id(Long id){
            this.id = id;
            return this;
        }
        public BoxItem build(){
            return new BoxItem(this);
        }
    }

    // Getters setters hashcode equals to string


}
1

1 Answers

3
votes

The problem is not cause by your mapping but by the way you handle the 'existing' entities.

As you said the BOX_ITEM_ONE already exists, but the EntityManger you are using in your test method does not know about.

In your case, you probably persisted BOX_ITEM_ONE during testsetup or you fetched it with find but you using different EnittyManager than in your test method, so this object is still 'new' to your EnityManger, but at least EM recognizes that it is a JPA managed entity so you got your detached exception.

If you 'manually' created BOX_ITEM_ONE using the id and properties that existed in DB, you would get error 'cannot INSERT with same primary key (or something along those lines'), as the EM would try to persist 'new object' but with already set PrimaryKey.

Simply, you need to make EM aware of the BoxItem by adding it to EM context. THere is merge method for that, you just call BOX_ITME_ONE = EM.merge(BOX_ITEM_ONE), and then you add it to your new BoxProfile. Or even better, to prevent 'object has changed exception' if in the meantime the BoxItem was updated, find the object using your current EM, BOX_ITEM_ONE = em.find(BobItem.class,BOX_ITEM_ONE.getID()). It will not issue new sql statement, it will just grab the object from the JPA context so it is no an performance issue.

Last thing, you probably want to add orphanRemoval = true to your OneToMany anotation on itemAssignments in BoxProfile, as you would probably like the ItemsAssignments to be removed if you delete them from the collection as they don't have sense on their own.