Spring 具有嵌套复合主键实体的数据 JPA 持久实体,其本身是一个在持久时间分离的嵌套实体

Spring Data JPA Persisting Entity with a nested Composite Primary Key entity with itself a nested entity which is detached at persist time

我尝试让三个 jpa 实体一起工作。 BoxProfile、BoxProfileItemAssignment 和 BoxItem 所有代码都列在下面。 BoxProfileItemAssignment 有一个 @EmbeddedId 使用 @MapId 来映射复合键。

BoxProfile 有一组BoxProfileItemAssignments,赋值是一个BoxItem 和数量值。我希望能够在保留新的 BoxProfile 的同时保留 BoxProfileItemAssignments。在创建 BoxProfile 时,BoxProfileItemAssignments 中的每个 BoxItem 都已保留。

我正在使用 spring 数据 JpaRepository 接口来保存我的 BoxProfile 实体并通过服务层 BoxProfileService 访问存储库。

当我尝试保留一个新的 BoxProfile 实体时,由于分离的实体,我得到了 PersistenceException。我知道嵌套在我传入的 BoxProfileItemAssignment 实体中的 BoxItem 是分离的,但我不想对所述实体进行更改或更新,我只想使用它来创建 BoxProfileItemAssignment 条目。

经过大量研究,我似乎无法找到一个级联的例子,它与本身具有嵌套实体的嵌套复合键实体保持一致。

如果有人能告诉我正确的注释组合是什么来实现我的目标,我将不胜感激。

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.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.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.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.run(ParentRunner.java:238) at org.junit.runners.ParentRunner.schedule(ParentRunner.java:63) at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:236) at org.junit.runners.ParentRunner.access[=18=]0(ParentRunner.java:53) at org.junit.runners.ParentRunner.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.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.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
    @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


}

问题不是由您的映射引起的,而是由您处理 'existing' 实体的方式引起的。

如您所说,BOX_ITEM_ONE 已经存在,但您在测试方法中使用的 EntityManger 并不知道。

在你的情况下,你可能在测试设置期间坚持 BOX_ITEM_ONE 或者你使用 find 获取它但你使用的 EnittyManager 与你的测试方法不同,所以这个对象仍然是 'new' 你的 EnityManger,但至少 EM 认识到它是一个 JPA 托管实体,所以你得到了你的分离异常。

如果您 'manually' 使用 DB 中存在的 ID 和属性创建 BOX_ITEM_ONE,您将收到错误 'cannot INSERT with same primary key (or something along those lines'),因为 EM 会尝试保留 'new object' 但已经设置了 PrimaryKey。

简单地说,您需要通过将 BoxItem 添加到 EM 上下文来让 EM 知道 BoxItem。这是合并方法,您只需调用 BOX_ITME_ONE = EM.merge(BOX_ITEM_ONE),然后将其添加到新的 BoxProfile 中。 或者甚至更好,为了防止 'object has changed exception' 如果在此期间更新了 BoxItem,请使用您当前的 EM 查找对象,BOX_ITEM_ONE = em.find(BobItem.class,BOX_ITEM_ONE.getID()).它不会发出新的 sql 语句,它只会从 JPA 上下文中获取对象,因此这不是性能问题。

最后一件事,您可能想将 orphanRemoval = true 添加到 BoxProfile 中 itemAssignments 的 OneToMany 注释中,因为如果您从集合中删除 ItemsAssignments,您可能希望删除它们,因为它们对他们自己的。