Search code examples
springspring-bootspring-data-jpaspring-data-restbidirectional

Unable to setup @ManyToMany and @OneToOne relationship correctly for bi directional Spring Data JPA and Spring REST


I am using Spring Boot with Spring Data JPA and Spring Data REST.

I have a class called TestExection, it is a class setup to have multiple TestResults. When a TestExecution is created there are no results created to set for it.

Later when the TestExecution is run TestResult objects are created.

These TestResult objects have a @OneToOne relation to the TestExecution and prior to saving a TestResult the TestExecution object is set on the TestResult and the save(TestResult) is called.

  // Create RESULT object
  TestResult testResult = new TestResult(someTestExection......);
  save(testResult)

If I make a call via REST to see the testResults/1/testExecution I can see the testExecution associated with the testResult. If I make the same call to testExecutions/1/testResults it returns a empty [].

2 questions:

1) Do I need to expclicitly set the newly created TestResult object into the TestExecutions's TestResult Set?

  testExecution.getTestResults().add(testResult);
  save(testExecution)
  • When I try this it causes the attached stack trace.

    Hibernate: update test_execution set description=?, owner=?, version=? where id=? and version=?
    [WARNING]
    java.lang.reflect.InvocationTargetException
        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:497)
        at org.springframework.boot.maven.AbstractRunMojo$LaunchRunner.run(AbstractRunMojo.java:478)
        at java.lang.Thread.run(Thread.java:745)
    Caused by: java.lang.StackOverflowError
        at com.miw.mcb.server.model.TestExecution.hashCode(TestExecution.java:32)
        at com.miw.mcb.server.model.TestResult.hashCode(TestResult.java:30)
        at java.util.AbstractSet.hashCode(AbstractSet.java:126)
        at org.hibernate.collection.internal.PersistentSet.hashCode(PersistentSet.java:448)
        at com.miw.mcb.server.model.TestExecution.hashCode(TestExecution.java:32)
        at com.miw.mcb.server.model.TestResult.hashCode(TestResult.java:30)
        at java.util.AbstractSet.hashCode(AbstractSet.java:126)
        at org.hibernate.collection.internal.PersistentSet.hashCode(PersistentSet.java:448)
    

OR

2) Is there some way to have it setup with a relationship so that when TestResult sets a TestExecution it can be linked and added to its list?

TestExecution

  @Data
  @Entity
  @Table(name = "test_execution")
  //@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id")
  public class TestExecution {

      private @Id @GeneratedValue Long id;

      // TODO set back to lazy
      @ManyToMany(fetch = FetchType.EAGER)
      //@JsonBackReference
      private Set<TestResult> testResults;

      // TODO set back to lazy
      @ManyToMany(fetch = FetchType.EAGER)
      private Set<TestSuite> testSuites;

TestResult

  @Data
  @Entity
  @Table(name = "test_result")
  //@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id")
  public class TestResult {

      private @Id @GeneratedValue Long id;

      // TODO switch back to lazy
      @OneToOne(fetch = FetchType.EAGER)
      //@JsonManagedReference
      private TestExecution testExecution;

      // TODO switch back to lazy
      @OneToOne(fetch = FetchType.EAGER)
      private TestSuite testSuite;

Both Repositories are setup the same

TestExecutionRepository

@Repository
@Transactional
public interface TestExecutionRepository extends PagingAndSortingRepository<TestExecution, Long> {

TestResultRepository

@Repository
@Transactional
public interface TestResultRepository extends PagingAndSortingRepository<TestResult, Long> {

AppConfig

@Configuration
@ComponentScan({ "com.miw.mcb.server.dao.repository", "com.miw.mcb.server.model" })
public class AppConfig {

}

SpringBootApp (ReactAndSpringDataRestApplication)

@SpringBootApplication
public class ReactAndSpringDataRestApplication {

public static void main(String[] args) {
    SpringApplication.run(ReactAndSpringDataRestApplication.class, args);
}
}

UPDATE: I made the updates for @ManyToOne and @OneToMany but there is still the problem of how the TestResults are propagated to TestExecution

Thank your for the explanation on ManyToOne and OneToMany. I thought I had the mapping incorrect.

After that I got the following stack trace when running:

 testExecution.getTestResults().add(testResult); 
 save(testExecution)

** Update for result.setTestExecution(execution); execution.getResults().add(result); repo.save(execution); without saving result first **

java.lang.StackOverflowError: null
  at com.miw.mcb.server.model.TestExecution.hashCode(TestExecution.java:26)
  at com.miw.mcb.server.model.TestResult.hashCode(TestResult.java:35)
  at java.util.AbstractSet.hashCode(AbstractSet.java:126)
  at org.hibernate.collection.internal.PersistentSet.hashCode(PersistentSet.java:448)
  at com.miw.mcb.server.model.TestExecution.hashCode(TestExecution.java:26)
  at com.miw.mcb.server.model.TestResult.hashCode(TestResult.java:35)
  at java.util.AbstractSet.hashCode(AbstractSet.java:126)
  at org.hibernate.collection.internal.PersistentSet.hashCode(PersistentSet.java:448)
  at com.miw.mcb.server.model.TestExecution.hashCode(TestExecution.java:26)
  at com.miw.mcb.server.model.TestResult.hashCode(TestResult.java:35)
  at java.util.AbstractSet.hashCode(AbstractSet.java:126)...

** To fix this I had to implement the hasCode() function for test result with Lombok **

 @EqualsAndHashCode(exclude={"id", "testExecution", "device", "testCase"})

Solution

  • I think you're making wrong choices for the JPA annotations. Moreover, I don't understand how this even works.

    In fact, a @ManyToMany relationship represents a kind of relationship in which every element of an entity can be related with anyone from the other entity. You need an intermediate table for that. This means a single TestResult could be linked with many executions.

    On the other side, you're providing a @OneToOne mapping, which is just the other way, provide only one entity instance from the other side for this object.

    The way you manage it, looks like what you want to have is a @OneToMany, which will allow TestExecution to control the testResults collection, meaning a test execution can have more than one result, but each result belongs only to one execution.

    TestExecution:

    @OneToMany(fetch=FetchType.EAGER, cascade = CascadeType.ALL, mappedBy="testExecution")
    private Set<TestResult> testResults;
    

    TestResult:

    @ManyToOne(fetch=FetchType.EAGER)
    @JoinColumn(name="id_test_result")
    private TestExecution testExecution;
    

    This way you let TextExecution manage its TestResult collection (using CascadeType.ALL propagates the changes into the collection when saving the TestExecution object).

    See also: