hjoeren
hjoeren

Reputation: 589

How to PATCH repositoryless collections of item resources with Spring Data Rest

Short: Is it possible to edit value entity collections ("replace collections") of an item resource?

Let's say there is the following model:

+---------+            +------------------+            +--------+
| Student | <-1----n-> | CourseMembership | <-m----1-> | Course |
+---------+            +------------------+            +--------+

Student and Course are exported via Spring Data Rest and the corresponding repositories (Students and Courses, both JpaRepository) exists. CourseMembership should not be exported (there shouldn't exist an endpoint for it) and so, also a corresponding repository doesn't exist/is not necessary.

Here are the three classes for the three entities/tables:

Entity
@Table(name = "students")
@Getter
@Setter
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class Course extends AbstractPersistable<Long> {
    
    @Column(name = "name", nullable = false)
    private String name;

}
@Entity
@Table(name = "course_memberships")
@Getter
@Setter
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class CourseMembership extends AbstractPersistable<Long> {

  @ManyToOne
  @JoinColumn(name = "student")
  private Student student;
  
  @ManyToOne
  @JoinColumn(name = "course")
  private Course course;
  
}
@Entity
@Table(name = "students")
@Getter
@Setter
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class Student extends AbstractPersistable<Long> {

  @Column(name = "name", nullable = false)
  private String name;

  @OneToMany(mappedBy = "student", cascade = { CascadeType.ALL })
  // @OneToMany(mappedBy = "student", cascade = { CascadeType.ALL }, orphanRemoval = true)
  @Setter(AccessLevel.NONE)
  private List<CourseMembership> courseMemberships = new ArrayList<>();

  public void setCourseMemberships(List<CourseMembership> courseMemberships) {
    this.courseMemberships.clear();
    this.courseMemberships.addAll(courseMemberships);
    this.courseMemberships.forEach(courseMembership -> courseMembership.setStudent(this));
  }

}

When the course memberships are PATCHED the first time, everthing is fine, the elements are inserted:

~$  curl -X PATCH -H "Content-Type: application/json" "http://localhost:8080/students/3" -d '{"courseMemberships":[{"course":"/courses/1"},{"course":"/courses/2"}]}'
{
  "name" : "John Doe",
  "courseMemberships" : [ {
    "new" : false,
    "_links" : {
      "student" : {
        "href" : "http://localhost:8080/students/3"
      },
      "course" : {
        "href" : "http://localhost:8080/courses/1"
      }
    }
  }, {
    "new" : false,
    "_links" : {
      "student" : {
        "href" : "http://localhost:8080/students/3"
      },
      "course" : {
        "href" : "http://localhost:8080/courses/2"
      }
    }
  } ],
  "new" : false,
  "_links" : {
    "self" : {
      "href" : "http://localhost:8080/students/3"
    },
    "student" : {
      "href" : "http://localhost:8080/students/3"
    }
  }
}

But with the next PATCH operations, I struggle:

Problem/Question:

When I try to replace the list (e. g. with an empty list or another list consisting of other elements), either nothing happens (without orphanRemoval = true) or exceptions occurs:

without orphanRemoval = true:

In the PATCH response, the collection looks modified, but with a GET after PATCH the response shows the origin collection.

(PRESUMTION: there where the two elements)

~$  curl  -X PATCH -H "Content-Type: application/json" "http://localhost:8080/students/3" -d '{"courseMemberships":[]}'
{
  "name" : "John Doe",
  "courseMemberships" : [ ],
  "new" : false,
  "_links" : {
    "self" : {
      "href" : "http://localhost:8080/students/3"
    },
    "student" : {
      "href" : "http://localhost:8080/students/3"
    }
  }
}

~$ curl -X GET "http://localhost:8080/students/3"
{
  "name" : "John Doe",
  "courseMemberships" : [ {
    "new" : false,
    "_links" : {
      "student" : {
        "href" : "http://localhost:8080/students/3"
      },
      "course" : {
        "href" : "http://localhost:8080/courses/1"
      }
    }
  }, {
    "new" : false,
    "_links" : {
      "student" : {
        "href" : "http://localhost:8080/students/3"
      },
      "course" : {
        "href" : "http://localhost:8080/courses/2"
      }
    }
  } ],
  "new" : false,
  "_links" : {
    "self" : {
      "href" : "http://localhost:8080/students/3"
    },
    "student" : {
      "href" : "http://localhost:8080/students/3"
    }
  }
}

with orphanRemoval = true:

Clearing an existing list is possible but "replace with another list" leads to an exception.

(PRESUMTION: there was one element)

~$ curl  -X PATCH -H "Content-Type: application/json" "http://localhost:8080/students/3" -d '{"courseMemberships":[{"course":"/courses/1"},{"course":"/courses/2"}]}'
{"cause":{"cause":null,"message":"not-null property references a null or transient value : io.gitlab.hjoeren.foo.CourseMembership.student"},"message":"not-null property references a null or transient value : io.gitlab.hjoeren.foo.CourseMembership.student; nested exception is org.hibernate.PropertyValueException: not-null property references a null or transient value : io.gitlab.hjoeren.foo.CourseMembership.student"}

2021-05-01 20:15:55.344 ERROR 23606 --- [nio-8080-exec-4] o.s.d.r.w.RepositoryRestExceptionHandler : not-null property references a null or transient value : io.gitlab.hjoeren.foo.CourseMembership.student; nested exception is org.hibernate.PropertyValueException: not-null property references a null or transient value : io.gitlab.hjoeren.foo.CourseMembership.student

org.springframework.dao.DataIntegrityViolationException: not-null property references a null or transient value : io.gitlab.hjoeren.foo.CourseMembership.student; nested exception is org.hibernate.PropertyValueException: not-null property references a null or transient value : io.gitlab.hjoeren.foo.CourseMembership.student
    at org.springframework.orm.jpa.vendor.HibernateJpaDialect.convertHibernateAccessException(HibernateJpaDialect.java:294) ~[spring-orm-5.3.6.jar:5.3.6]
    at org.springframework.orm.jpa.vendor.HibernateJpaDialect.translateExceptionIfPossible(HibernateJpaDialect.java:233) ~[spring-orm-5.3.6.jar:5.3.6]
    at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.translateExceptionIfPossible(AbstractEntityManagerFactoryBean.java:551) ~[spring-orm-5.3.6.jar:5.3.6]
    at org.springframework.dao.support.ChainedPersistenceExceptionTranslator.translateExceptionIfPossible(ChainedPersistenceExceptionTranslator.java:61) ~[spring-tx-5.3.6.jar:5.3.6]
    at org.springframework.dao.support.DataAccessUtils.translateIfNecessary(DataAccessUtils.java:242) ~[spring-tx-5.3.6.jar:5.3.6]
    at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:152) ~[spring-tx-5.3.6.jar:5.3.6]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[spring-aop-5.3.6.jar:5.3.6]
    at org.springframework.data.jpa.repository.support.CrudMethodMetadataPostProcessor$CrudMethodMetadataPopulatingMethodInterceptor.invoke(CrudMethodMetadataPostProcessor.java:174) ~[spring-data-jpa-2.4.8.jar:2.4.8]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[spring-aop-5.3.6.jar:5.3.6]
    at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:97) ~[spring-aop-5.3.6.jar:5.3.6]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[spring-aop-5.3.6.jar:5.3.6]
    at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:215) ~[spring-aop-5.3.6.jar:5.3.6]
    at com.sun.proxy.$Proxy97.save(Unknown Source) ~[na:na]
    at org.springframework.data.repository.support.CrudRepositoryInvoker.invokeSave(CrudRepositoryInvoker.java:101) ~[spring-data-commons-2.4.8.jar:2.4.8]
    at org.springframework.data.rest.core.support.UnwrappingRepositoryInvokerFactory$UnwrappingRepositoryInvoker.invokeSave(UnwrappingRepositoryInvokerFactory.java:181) ~[spring-data-rest-core-3.4.8.jar:3.4.8]
    at org.springframework.data.rest.webmvc.RepositoryEntityController.saveAndReturn(RepositoryEntityController.java:446) ~[spring-data-rest-webmvc-3.4.8.jar:3.4.8]
    at org.springframework.data.rest.webmvc.RepositoryEntityController.patchItemResource(RepositoryEntityController.java:395) ~[spring-data-rest-webmvc-3.4.8.jar:3.4.8]
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:na]
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
    at java.base/java.lang.reflect.Method.invoke(Method.java:566) ~[na:na]
    at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:197) ~[spring-web-5.3.6.jar:5.3.6]
    at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:141) ~[spring-web-5.3.6.jar:5.3.6]
    at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:106) ~[spring-webmvc-5.3.6.jar:5.3.6]
    at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:894) ~[spring-webmvc-5.3.6.jar:5.3.6]
    at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:808) ~[spring-webmvc-5.3.6.jar:5.3.6]
    at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87) ~[spring-webmvc-5.3.6.jar:5.3.6]
    at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1060) ~[spring-webmvc-5.3.6.jar:5.3.6]
    at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:962) ~[spring-webmvc-5.3.6.jar:5.3.6]
    at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006) ~[spring-webmvc-5.3.6.jar:5.3.6]
    at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:880) ~[spring-webmvc-5.3.6.jar:5.3.6]
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:733) ~[tomcat-embed-core-9.0.45.jar:4.0.FR]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:227) ~[tomcat-embed-core-9.0.45.jar:9.0.45]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[tomcat-embed-core-9.0.45.jar:9.0.45]
    at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53) ~[tomcat-embed-websocket-9.0.45.jar:9.0.45]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) ~[tomcat-embed-core-9.0.45.jar:9.0.45]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[tomcat-embed-core-9.0.45.jar:9.0.45]
    at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100) ~[spring-web-5.3.6.jar:5.3.6]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) ~[spring-web-5.3.6.jar:5.3.6]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) ~[tomcat-embed-core-9.0.45.jar:9.0.45]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[tomcat-embed-core-9.0.45.jar:9.0.45]
    at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93) ~[spring-web-5.3.6.jar:5.3.6]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) ~[spring-web-5.3.6.jar:5.3.6]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) ~[tomcat-embed-core-9.0.45.jar:9.0.45]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[tomcat-embed-core-9.0.45.jar:9.0.45]
    at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201) ~[spring-web-5.3.6.jar:5.3.6]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) ~[spring-web-5.3.6.jar:5.3.6]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) ~[tomcat-embed-core-9.0.45.jar:9.0.45]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[tomcat-embed-core-9.0.45.jar:9.0.45]
    at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:202) ~[tomcat-embed-core-9.0.45.jar:9.0.45]
    at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:97) ~[tomcat-embed-core-9.0.45.jar:9.0.45]
    at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:542) ~[tomcat-embed-core-9.0.45.jar:9.0.45]
    at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:143) ~[tomcat-embed-core-9.0.45.jar:9.0.45]
    at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92) ~[tomcat-embed-core-9.0.45.jar:9.0.45]
    at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:78) ~[tomcat-embed-core-9.0.45.jar:9.0.45]
    at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:357) ~[tomcat-embed-core-9.0.45.jar:9.0.45]
    at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:374) ~[tomcat-embed-core-9.0.45.jar:9.0.45]
    at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65) ~[tomcat-embed-core-9.0.45.jar:9.0.45]
    at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:893) ~[tomcat-embed-core-9.0.45.jar:9.0.45]
    at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1707) ~[tomcat-embed-core-9.0.45.jar:9.0.45]
    at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49) ~[tomcat-embed-core-9.0.45.jar:9.0.45]
    at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128) ~[na:na]
    at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628) ~[na:na]
    at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) ~[tomcat-embed-core-9.0.45.jar:9.0.45]
    at java.base/java.lang.Thread.run(Thread.java:829) ~[na:na]
Caused by: org.hibernate.PropertyValueException: not-null property references a null or transient value : io.gitlab.hjoeren.foo.CourseMembership.student
    at org.hibernate.engine.internal.Nullability.checkNullability(Nullability.java:111) ~[hibernate-core-5.4.30.Final.jar:5.4.30.Final]
    at org.hibernate.engine.internal.Nullability.checkNullability(Nullability.java:55) ~[hibernate-core-5.4.30.Final.jar:5.4.30.Final]
    at org.hibernate.action.internal.AbstractEntityInsertAction.nullifyTransientReferencesIfNotAlready(AbstractEntityInsertAction.java:116) ~[hibernate-core-5.4.30.Final.jar:5.4.30.Final]
    at org.hibernate.action.internal.AbstractEntityInsertAction.makeEntityManaged(AbstractEntityInsertAction.java:125) ~[hibernate-core-5.4.30.Final.jar:5.4.30.Final]
    at org.hibernate.engine.spi.ActionQueue.addResolvedEntityInsertAction(ActionQueue.java:289) ~[hibernate-core-5.4.30.Final.jar:5.4.30.Final]
    at org.hibernate.engine.spi.ActionQueue.addInsertAction(ActionQueue.java:263) ~[hibernate-core-5.4.30.Final.jar:5.4.30.Final]
    at org.hibernate.engine.spi.ActionQueue.addAction(ActionQueue.java:250) ~[hibernate-core-5.4.30.Final.jar:5.4.30.Final]
    at org.hibernate.event.internal.AbstractSaveEventListener.addInsertAction(AbstractSaveEventListener.java:338) ~[hibernate-core-5.4.30.Final.jar:5.4.30.Final]
    at org.hibernate.event.internal.AbstractSaveEventListener.performSaveOrReplicate(AbstractSaveEventListener.java:287) ~[hibernate-core-5.4.30.Final.jar:5.4.30.Final]
    at org.hibernate.event.internal.AbstractSaveEventListener.performSave(AbstractSaveEventListener.java:193) ~[hibernate-core-5.4.30.Final.jar:5.4.30.Final]
    at org.hibernate.event.internal.AbstractSaveEventListener.saveWithGeneratedId(AbstractSaveEventListener.java:135) ~[hibernate-core-5.4.30.Final.jar:5.4.30.Final]
    at org.hibernate.event.internal.DefaultMergeEventListener.saveTransientEntity(DefaultMergeEventListener.java:271) ~[hibernate-core-5.4.30.Final.jar:5.4.30.Final]
    at org.hibernate.event.internal.DefaultMergeEventListener.entityIsTransient(DefaultMergeEventListener.java:243) ~[hibernate-core-5.4.30.Final.jar:5.4.30.Final]
    at org.hibernate.event.internal.DefaultMergeEventListener.onMerge(DefaultMergeEventListener.java:175) ~[hibernate-core-5.4.30.Final.jar:5.4.30.Final]
    at org.hibernate.event.service.internal.EventListenerGroupImpl.fireEventOnEachListener(EventListenerGroupImpl.java:104) ~[hibernate-core-5.4.30.Final.jar:5.4.30.Final]
    at org.hibernate.internal.SessionImpl.fireMerge(SessionImpl.java:813) ~[hibernate-core-5.4.30.Final.jar:5.4.30.Final]
    at org.hibernate.internal.SessionImpl.merge(SessionImpl.java:786) ~[hibernate-core-5.4.30.Final.jar:5.4.30.Final]
    at org.hibernate.engine.spi.CascadingActions$6.cascade(CascadingActions.java:261) ~[hibernate-core-5.4.30.Final.jar:5.4.30.Final]
    at org.hibernate.engine.internal.Cascade.cascadeToOne(Cascade.java:499) ~[hibernate-core-5.4.30.Final.jar:5.4.30.Final]
    at org.hibernate.engine.internal.Cascade.cascadeAssociation(Cascade.java:423) ~[hibernate-core-5.4.30.Final.jar:5.4.30.Final]
    at org.hibernate.engine.internal.Cascade.cascadeProperty(Cascade.java:220) ~[hibernate-core-5.4.30.Final.jar:5.4.30.Final]
    at org.hibernate.engine.internal.Cascade.cascadeCollectionElements(Cascade.java:532) ~[hibernate-core-5.4.30.Final.jar:5.4.30.Final]
    at org.hibernate.engine.internal.Cascade.cascadeCollection(Cascade.java:463) ~[hibernate-core-5.4.30.Final.jar:5.4.30.Final]
    at org.hibernate.engine.internal.Cascade.cascadeAssociation(Cascade.java:426) ~[hibernate-core-5.4.30.Final.jar:5.4.30.Final]
    at org.hibernate.engine.internal.Cascade.cascadeProperty(Cascade.java:220) ~[hibernate-core-5.4.30.Final.jar:5.4.30.Final]
    at org.hibernate.engine.internal.Cascade.cascade(Cascade.java:153) ~[hibernate-core-5.4.30.Final.jar:5.4.30.Final]
    at org.hibernate.event.internal.DefaultMergeEventListener.cascadeOnMerge(DefaultMergeEventListener.java:519) ~[hibernate-core-5.4.30.Final.jar:5.4.30.Final]
    at org.hibernate.event.internal.DefaultMergeEventListener.entityIsPersistent(DefaultMergeEventListener.java:204) ~[hibernate-core-5.4.30.Final.jar:5.4.30.Final]
    at org.hibernate.event.internal.DefaultMergeEventListener.onMerge(DefaultMergeEventListener.java:178) ~[hibernate-core-5.4.30.Final.jar:5.4.30.Final]
    at org.hibernate.event.internal.DefaultMergeEventListener.onMerge(DefaultMergeEventListener.java:70) ~[hibernate-core-5.4.30.Final.jar:5.4.30.Final]
    at org.hibernate.event.service.internal.EventListenerGroupImpl.fireEventOnEachListener(EventListenerGroupImpl.java:93) ~[hibernate-core-5.4.30.Final.jar:5.4.30.Final]
    at org.hibernate.internal.SessionImpl.fireMerge(SessionImpl.java:793) ~[hibernate-core-5.4.30.Final.jar:5.4.30.Final]
    at org.hibernate.internal.SessionImpl.merge(SessionImpl.java:780) ~[hibernate-core-5.4.30.Final.jar:5.4.30.Final]
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:na]
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
    at java.base/java.lang.reflect.Method.invoke(Method.java:566) ~[na:na]
    at org.springframework.orm.jpa.ExtendedEntityManagerCreator$ExtendedEntityManagerInvocationHandler.invoke(ExtendedEntityManagerCreator.java:362) ~[spring-orm-5.3.6.jar:5.3.6]
    at com.sun.proxy.$Proxy90.merge(Unknown Source) ~[na:na]
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:na]
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
    at java.base/java.lang.reflect.Method.invoke(Method.java:566) ~[na:na]
    at org.springframework.orm.jpa.SharedEntityManagerCreator$SharedEntityManagerInvocationHandler.invoke(SharedEntityManagerCreator.java:311) ~[spring-orm-5.3.6.jar:5.3.6]
    at com.sun.proxy.$Proxy90.merge(Unknown Source) ~[na:na]
    at org.springframework.data.jpa.repository.support.SimpleJpaRepository.save(SimpleJpaRepository.java:560) ~[spring-data-jpa-2.4.8.jar:2.4.8]
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:na]
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
    at java.base/java.lang.reflect.Method.invoke(Method.java:566) ~[na:na]
    at org.springframework.data.repository.core.support.RepositoryMethodInvoker$RepositoryFragmentMethodInvoker.lambda$new$0(RepositoryMethodInvoker.java:289) ~[spring-data-commons-2.4.8.jar:2.4.8]
    at org.springframework.data.repository.core.support.RepositoryMethodInvoker.doInvoke(RepositoryMethodInvoker.java:137) ~[spring-data-commons-2.4.8.jar:2.4.8]
    at org.springframework.data.repository.core.support.RepositoryMethodInvoker.invoke(RepositoryMethodInvoker.java:121) ~[spring-data-commons-2.4.8.jar:2.4.8]
    at org.springframework.data.repository.core.support.RepositoryComposition$RepositoryFragments.invoke(RepositoryComposition.java:524) ~[spring-data-commons-2.4.8.jar:2.4.8]
    at org.springframework.data.repository.core.support.RepositoryComposition.invoke(RepositoryComposition.java:285) ~[spring-data-commons-2.4.8.jar:2.4.8]
    at org.springframework.data.repository.core.support.RepositoryFactorySupport$ImplementationMethodExecutionInterceptor.invoke(RepositoryFactorySupport.java:531) ~[spring-data-commons-2.4.8.jar:2.4.8]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[spring-aop-5.3.6.jar:5.3.6]
    at org.springframework.data.repository.core.support.QueryExecutorMethodInterceptor.doInvoke(QueryExecutorMethodInterceptor.java:156) ~[spring-data-commons-2.4.8.jar:2.4.8]
    at org.springframework.data.repository.core.support.QueryExecutorMethodInterceptor.invoke(QueryExecutorMethodInterceptor.java:131) ~[spring-data-commons-2.4.8.jar:2.4.8]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[spring-aop-5.3.6.jar:5.3.6]
    at org.springframework.data.projection.DefaultMethodInvokingMethodInterceptor.invoke(DefaultMethodInvokingMethodInterceptor.java:80) ~[spring-data-commons-2.4.8.jar:2.4.8]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[spring-aop-5.3.6.jar:5.3.6]
    at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:123) ~[spring-tx-5.3.6.jar:5.3.6]
    at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:388) ~[spring-tx-5.3.6.jar:5.3.6]
    at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:119) ~[spring-tx-5.3.6.jar:5.3.6]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[spring-aop-5.3.6.jar:5.3.6]
    at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:137) ~[spring-tx-5.3.6.jar:5.3.6]
    ... 59 common frames omitted

2021-05-01 20:15:55.350  WARN 23606 --- [nio-8080-exec-4] .m.m.a.ExceptionHandlerExceptionResolver : Resolved [org.springframework.dao.DataIntegrityViolationException: not-null property references a null or transient value : io.gitlab.hjoeren.foo.CourseMembership.student; nested exception is org.hibernate.PropertyValueException: not-null property references a null or transient value : io.gitlab.hjoeren.foo.CourseMembership.student]


Maybe a useful note: When initially there is a single element in the collection, and a PATCH with a collection consisting of another element, the element is updated - instead of deleted and insert:

(PRESUMTION: there where no courseMemberships before)

~$ curl -X PATCH -H "Content-Type: application/json" "http://localhost:8080/students/3" -d '{"courseMemberships":[{"course":"/courses/1"}]}'
{
  "name" : "John Doe",
  "courseMemberships" : [ {
    "new" : false,
    "_links" : {
      "student" : {
        "href" : "http://localhost:8080/students/3"
      },
      "course" : {
        "href" : "http://localhost:8080/courses/1"
      }
    }
  } ],
  "new" : false,
  "_links" : {
    "self" : {
      "href" : "http://localhost:8080/students/3"
    },
    "student" : {
      "href" : "http://localhost:8080/students/3"
    }
  }
}

Hibernate: select student0_.id as id1_2_0_, student0_.name as name2_2_0_ from students student0_ where student0_.id=?
Hibernate: select coursememb0_.student as student3_0_0_, coursememb0_.id as id1_0_0_, coursememb0_.id as id1_0_1_, coursememb0_.course as course2_0_1_, coursememb0_.student as student3_0_1_, course1_.id as id1_1_2_, course1_.name as name2_1_2_ from course_memberships coursememb0_ inner join courses course1_ on coursememb0_.course=course1_.id where coursememb0_.student=?
Hibernate: select course0_.id as id1_1_0_, course0_.name as name2_1_0_ from courses course0_ where course0_.id=?
Hibernate: call next value for hibernate_sequence
Hibernate: insert into course_memberships (course, student, id) values (?, ?, ?)

~$ curl  -X PATCH -H "Content-Type: application/json" "http://localhost:8080/students/3" -d '{"courseMemberships":[{"course":"/courses/2"}]}'
{
  "name" : "John Doe",
  "courseMemberships" : [ {
    "new" : false,
    "_links" : {
      "student" : {
        "href" : "http://localhost:8080/students/3"
      },
      "course" : {
        "href" : "http://localhost:8080/courses/2"
      }
    }
  } ],
  "new" : false,
  "_links" : {
    "self" : {
      "href" : "http://localhost:8080/students/3"
    },
    "student" : {
      "href" : "http://localhost:8080/students/3"
    }
  }
}

Hibernate: select student0_.id as id1_2_0_, student0_.name as name2_2_0_ from students student0_ where student0_.id=?
Hibernate: select coursememb0_.student as student3_0_0_, coursememb0_.id as id1_0_0_, coursememb0_.id as id1_0_1_, coursememb0_.course as course2_0_1_, coursememb0_.student as student3_0_1_, course1_.id as id1_1_2_, course1_.name as name2_1_2_ from course_memberships coursememb0_ inner join courses course1_ on coursememb0_.course=course1_.id where coursememb0_.student=?
Hibernate: select course0_.id as id1_1_0_, course0_.name as name2_1_0_ from courses course0_ where course0_.id=?
Hibernate: update course_memberships set course=?, student=? where id=?

Upvotes: 0

Views: 448

Answers (1)

hjoeren
hjoeren

Reputation: 589

Since no repository is planned anyway, it is possible to change the CourseMembership from an @Entity to an @Embeddable and the association in Student from @OneToMany to an @ElementCollection:

@Embeddable
@Getter
@Setter
public class CourseMembership {

    @ManyToOne
    @JoinColumn(name = "course", nullable = false)
    private Course course;

}
@Entity
@Table(name = "students")
@Getter
@Setter
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class Student extends AbstractPersistable<Long> implements Serializable {

    private static final long serialVersionUID = 2741464453152791261L;

    @Column(name = "name", nullable = false)
    private String name;

    @ElementCollection
    @CollectionTable(name = "course_memberships", joinColumns = { @JoinColumn(name = "student") })
    private List<CourseMembership> courseMemberships = new ArrayList<>();

}

Given that, the PATCH operations of the question works as expected and I also think, looking to the model this would be the cleaner way (álá "CourseMembership as @Embeddable more expresses that there is a composition and is useless without Student")

Upvotes: 1

Related Questions