带有额外列的 Spring Data JPA 多对多

Posted

技术标签:

【中文标题】带有额外列的 Spring Data JPA 多对多【英文标题】:Spring Data JPA Many to Many with extra column 【发布时间】:2017-10-17 12:14:00 【问题描述】:

我正在尝试在两个表之间建立多对多关系。该关系包含额外信息(日期)。我正在尝试使用 Spring Data JPA 来实现这一目标,并且正在使用单元测试进行测试。测试失败并给出错误:

org.springframework.orm.jpa.JpaObjectRetrievalFailureException: Unable to find ....JoinEntity with id ....JoinEntity@5934ca1e; nested exception is javax.persistence.EntityNotFoundException: Unable to find ....JoinEntity with id ....JoinEntity@5934ca1e

这是我的代码:

实体 A:

import java.util.Set;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.OneToMany;
import javax.persistence.Table;

@Entity
@Table(name= "tableA")
public class EntityA

   /** The primary key. */
   @Id
   private Integer id;

   private String name;

   @OneToMany(mappedBy="a")
   private Set<JoinEntity> bs;

   //... getters and setters

实体 B:

import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;

@Entity
@Table(name= "tableB")
public class EntityB

   /** The primary key. */
   @Id
   private Integer id;

   private String name;

   //... getters and setters

加入实体:

import java.io.Serializable;
import java.util.Date;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;

@Entity
@Table(name="joinTableAB")
@IdClass(ABId.class)
public class JoinEntity implements Serializable


   @Column(name = "join_date")
   private Date date;

   @Id
   @ManyToOne
   @JoinColumn(name = "a_id")
   private EntityA a;

   @Id
   @ManyToOne
   @JoinColumn(name = "b_id")
   private EntityB b;

   //... getters and setters

A 的存储库:

import org.springframework.data.repository.CrudRepository;

public interface ARepository extends CrudRepository<EntityA,Integer>


B 的存储库:

import org.springframework.data.repository.CrudRepository;

public interface BRepository extends CrudRepository<EntityB,Integer>


简单的测试类:

import java.util.Date;
import java.util.HashSet;
import java.util.Set;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

/**
 * Testing for AtmosphericConditionsRepository.
 */
@DataJpaTest
@RunWith(SpringRunner.class)
@SpringBootTest(classes = ARepositoryTest.class)
public class ARepositoryTest

   @Autowired
   ARepository aRepository;

   @Autowired
   BRepository bRepository;

   @Test
   public void test()
   
      EntityA a0 = new EntityA();
      a0.setId(0);
      a0.setName("a0");
      EntityA a1 = new EntityA();
      a1.setId(1);
      a1.setName("a1");

      a0 = aRepository.save(a0);
      a1 = aRepository.save(a1);

      EntityB b0 = new EntityB();
      b0.setId(0);
      b0.setName("b0");
      EntityB b1 = new EntityB();
      b1.setId(1);
      b1.setName("b1");

      b0 = bRepository.save(b0);
      b1 = bRepository.save(b1);

      Set<JoinEntity> joinEntities = new HashSet<>();
      JoinEntity je = new JoinEntity();
      je.setDate(new Date());
      je.setA(a0);
      je.setB(b0);
      joinEntities.add(je);
      a0.setBs(joinEntities);

      aRepository.save(a0);
   


@IdClass 中指定的类:

import java.io.Serializable;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;

public class ABId implements Serializable

   private EntityA a;

   private EntityB b;

   public ABId()
   
   

   public ABId(EntityA pA, EntityB pB)
   
      a = pA;
      b = pB;
   

   public EntityA getA()
   
      return a;
   

   public EntityB getB()
   
      return b;
   

   @Override
   public boolean equals(Object pO)
   
      if (this == pO)
      
         return true;
      
      if (pO == null || getClass() != pO.getClass())
      
         return false;
      

      ABId abId = (ABId) pO;

      if (a != null ? !a.equals(abId.a) : abId.a != null)
      
         return false;
      
      return b != null ? b.equals(abId.b) : abId.b == null;
   

   @Override
   public int hashCode()
   
      int result = a != null ? a.hashCode() : 0;
      result = 31 * result + (b != null ? b.hashCode() : 0);
      return result;
   

扩展堆栈跟踪:

2017-10-18 08:30:48.696  INFO 6460 --- [           main] o.s.t.c.transaction.TransactionContext   : Began transaction (1) for test context [DefaultTestContext@de3a06f testClass = ARepositoryTest, testInstance = ....ARepositoryTest@58a90037, testMethod = test@ARepositoryTest, testException = [null], mergedContextConfiguration = [MergedContextConfiguration@76b10754 testClass = ARepositoryTest, locations = '', classes = 'interface ....ARepository, interface ....BRepository, class ....JoinEntity, class ....ABId, interface ....ARepository, interface ....BRepository, class ....JoinEntity, class ....ABId', contextInitializerClasses = '[]', activeProfiles = '', propertySourceLocations = '', propertySourceProperties = 'org.springframework.boot.test.context.SpringBootTestContextBootstrapper=true', contextCustomizers = set[org.springframework.boot.test.autoconfigure.OverrideAutoConfigurationContextCustomizerFactory$DisableAutoConfigurationContextCustomizer@4493d195, org.springframework.boot.test.autoconfigure.filter.TypeExcludeFiltersContextCustomizer@4e1d422d, org.springframework.boot.test.autoconfigure.properties.PropertyMappingContextCustomizer@5c8ff52f, org.springframework.boot.test.autoconfigure.web.servlet.WebDriverContextCustomizerFactory$Customizer@3cb1ffe6, org.springframework.boot.test.context.ImportsContextCustomizer@274bc460, org.springframework.boot.test.context.SpringBootTestContextCustomizer@2c039ac6, org.springframework.boot.test.context.filter.ExcludeFilterContextCustomizer@6b57696f, org.springframework.boot.test.mock.mockito.MockitoContextCustomizer@0], contextLoader = 'org.springframework.boot.test.context.SpringBootContextLoader', parent = [null]]]; transaction manager [org.springframework.orm.jpa.JpaTransactionManager@5e13f156]; rollback [true]
Hibernate: select entitya0_.id as id1_6_0_, entitya0_.name as name2_6_0_ from tablea entitya0_ where entitya0_.id=?
Hibernate: select entitya0_.id as id1_6_0_, entitya0_.name as name2_6_0_ from tablea entitya0_ where entitya0_.id=?
Hibernate: select entityb0_.id as id1_7_0_, entityb0_.name as name2_7_0_ from tableb entityb0_ where entityb0_.id=?
Hibernate: select entityb0_.id as id1_7_0_, entityb0_.name as name2_7_0_ from tableb entityb0_ where entityb0_.id=?
Hibernate: select joinentity0_.a_id as a_id2_2_0_, joinentity0_.b_id as b_id3_2_0_, joinentity0_.join_date as join_dat1_2_0_, entitya1_.id as id1_6_1_, entitya1_.name as name2_6_1_, entityb2_.id as id1_7_2_, entityb2_.name as name2_7_2_ from join_tableab joinentity0_ inner join tablea entitya1_ on joinentity0_.a_id=entitya1_.id inner join tableb entityb2_ on joinentity0_.b_id=entityb2_.id where joinentity0_.a_id=? and joinentity0_.b_id=?
2017-10-18 08:30:49.035  INFO 6460 --- [           main] o.s.t.c.transaction.TransactionContext   : Rolled back transaction for test context [DefaultTestContext@de3a06f testClass = ARepositoryTest, testInstance = ....ARepositoryTest@58a90037, testMethod = test@ARepositoryTest, testException = org.springframework.orm.jpa.JpaObjectRetrievalFailureException: Unable to find ....JoinEntity with id ....ABId@5a9ba131; nested exception is javax.persistence.EntityNotFoundException: Unable to find ....JoinEntity with id ....ABId@5a9ba131, mergedContextConfiguration = [MergedContextConfiguration@76b10754 testClass = ARepositoryTest, locations = '', classes = 'interface ....ARepository, interface ....BRepository, class ....JoinEntity, class ....ABId, interface ....ARepository, interface ....BRepository, class ....JoinEntity, class ....ABId', contextInitializerClasses = '[]', activeProfiles = '', propertySourceLocations = '', propertySourceProperties = 'org.springframework.boot.test.context.SpringBootTestContextBootstrapper=true', contextCustomizers = set[org.springframework.boot.test.autoconfigure.OverrideAutoConfigurationContextCustomizerFactory$DisableAutoConfigurationContextCustomizer@4493d195, org.springframework.boot.test.autoconfigure.filter.TypeExcludeFiltersContextCustomizer@4e1d422d, org.springframework.boot.test.autoconfigure.properties.PropertyMappingContextCustomizer@5c8ff52f, org.springframework.boot.test.autoconfigure.web.servlet.WebDriverContextCustomizerFactory$Customizer@3cb1ffe6, org.springframework.boot.test.context.ImportsContextCustomizer@274bc460, org.springframework.boot.test.context.SpringBootTestContextCustomizer@2c039ac6, org.springframework.boot.test.context.filter.ExcludeFilterContextCustomizer@6b57696f, org.springframework.boot.test.mock.mockito.MockitoContextCustomizer@0], contextLoader = 'org.springframework.boot.test.context.SpringBootContextLoader', parent = [null]]].

org.springframework.orm.jpa.JpaObjectRetrievalFailureException: Unable to find ....JoinEntity with id ....ABId@d3513f0a; nested exception is javax.persistence.EntityNotFoundException: Unable to find ....JoinEntity with id ....ABId@d3513f0a

    at org.springframework.orm.jpa.EntityManagerFactoryUtils.convertJpaAccessExceptionIfPossible(EntityManagerFactoryUtils.java:389)
    at org.springframework.orm.jpa.vendor.HibernateJpaDialect.translateExceptionIfPossible(HibernateJpaDialect.java:246)
    at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.translateExceptionIfPossible(AbstractEntityManagerFactoryBean.java:491)
    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)
    at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:213)
    at com.sun.proxy.$Proxy93.save(Unknown Source)
    at ....ARepositoryTest.test(ARepositoryTest.java:60)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
    at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
    at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
    at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
    at org.springframework.test.context.junit4.statements.RunBeforeTestMethodCallbacks.evaluate(RunBeforeTestMethodCallbacks.java:75)
    at org.springframework.test.context.junit4.statements.RunAfterTestMethodCallbacks.evaluate(RunAfterTestMethodCallbacks.java:86)
    at org.springframework.test.context.junit4.statements.SpringRepeat.evaluate(SpringRepeat.java:84)
    at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:252)
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:94)
    at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
    at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
    at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
    at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
    at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
    at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61)
    at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:70)
    at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:191)
    at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
    at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
    at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:51)
    at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:237)
    at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at com.intellij.rt.execution.application.AppMain.main(AppMain.java:147)
Caused by: javax.persistence.EntityNotFoundException: Unable to find ....JoinEntity with id ....ABId@d3513f0a
    at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl$JpaEntityNotFoundDelegate.handleEntityNotFound(EntityManagerFactoryBuilderImpl.java:144)
    at org.hibernate.event.internal.DefaultLoadEventListener.load(DefaultLoadEventListener.java:227)
    at org.hibernate.event.internal.DefaultLoadEventListener.proxyOrLoad(DefaultLoadEventListener.java:278)
    at org.hibernate.event.internal.DefaultLoadEventListener.doOnLoad(DefaultLoadEventListener.java:121)
    at org.hibernate.event.internal.DefaultLoadEventListener.onLoad(DefaultLoadEventListener.java:89)
    at org.hibernate.internal.SessionImpl.fireLoad(SessionImpl.java:1129)
    at org.hibernate.internal.SessionImpl.internalLoad(SessionImpl.java:1022)
    at org.hibernate.type.EntityType.resolveIdentifier(EntityType.java:639)
    at org.hibernate.type.EntityType.resolve(EntityType.java:431)
    at org.hibernate.type.EntityType.replace(EntityType.java:330)
    at org.hibernate.type.CollectionType.replaceElements(CollectionType.java:518)
    at org.hibernate.type.CollectionType.replace(CollectionType.java:663)
    at org.hibernate.type.TypeHelper.replace(TypeHelper.java:177)
    at org.hibernate.event.internal.DefaultMergeEventListener.copyValues(DefaultMergeEventListener.java:401)
    at org.hibernate.event.internal.DefaultMergeEventListener.entityIsPersistent(DefaultMergeEventListener.java:203)
    at org.hibernate.event.internal.DefaultMergeEventListener.onMerge(DefaultMergeEventListener.java:176)
    at org.hibernate.event.internal.DefaultMergeEventListener.onMerge(DefaultMergeEventListener.java:69)
    at org.hibernate.internal.SessionImpl.fireMerge(SessionImpl.java:840)
    at org.hibernate.internal.SessionImpl.merge(SessionImpl.java:822)
    at org.hibernate.internal.SessionImpl.merge(SessionImpl.java:827)
    at org.hibernate.jpa.spi.AbstractEntityManagerImpl.merge(AbstractEntityManagerImpl.java:1161)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at org.springframework.orm.jpa.SharedEntityManagerCreator$SharedEntityManagerInvocationHandler.invoke(SharedEntityManagerCreator.java:298)
    at com.sun.proxy.$Proxy85.merge(Unknown Source)
    at org.springframework.data.jpa.repository.support.SimpleJpaRepository.save(SimpleJpaRepository.java:509)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at org.springframework.data.repository.core.support.RepositoryFactorySupport$QueryExecutorMethodInterceptor.executeMethodOn(RepositoryFactorySupport.java:503)
    at org.springframework.data.repository.core.support.RepositoryFactorySupport$QueryExecutorMethodInterceptor.doInvoke(RepositoryFactorySupport.java:488)
    at org.springframework.data.repository.core.support.RepositoryFactorySupport$QueryExecutorMethodInterceptor.invoke(RepositoryFactorySupport.java:460)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
    at org.springframework.data.projection.DefaultMethodInvokingMethodInterceptor.invoke(DefaultMethodInvokingMethodInterceptor.java:61)
    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:282)
    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)
    ... 41 more

【问题讨论】:

“JoinEntity”的@IdClass 在哪里?因为你有不止 1 个@Id,所以你需要一个 你确定它是强制性的吗?我正在使用 @Id 两次创建复合键。 我用@IdClass试过了,结果是一样的,只是消息稍有不同(当指出没有找到哪个id时)。还有其他想法吗? 你能显示更多的堆栈跟踪吗,请? (只是一个猜测:为 JoinEntity 写一个 Dao(不贵)并在你的测试中使用它。) 当然是必须的。如果你有一个复合键,那么如果你没有定义类型,你将没有任何东西可以输入到em.find(...) 【参考方案1】:

我错过了集合上的 Cascade 类型:/。

添加后:

@OneToMany(mappedBy="a",cascade = CascadeType.ALL)
private Set<JoinEntity> bs;

现在可以正常使用了。

【讨论】:

以上是关于带有额外列的 Spring Data JPA 多对多的主要内容,如果未能解决你的问题,请参考以下文章

带有额外列的 JPA 2.0 多对多 - 更新集合

openjpa:多对多,带有额外的列

JPA 2:通过不在带有额外字段的多对多中工作来订购

Spring boot JPA - 使用额外的列查询多对多

如何使用带有额外列的多对多映射保存对象?

带有额外列的多对多自引用原则