代码之家  ›  专栏  ›  技术社区  ›  hjoeren

如何使用Spring Data Rest修补无存储库的项目资源集合

  •  0
  • hjoeren  · 技术社区  · 3 年前

    短: 是否可以编辑项目资源的值实体集合(“替换集合”)?

    假设有以下模型:

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

    Student Course 通过Spring Data Rest和相应的存储库导出( Students Courses ,两者 JpaRepository )存在。 CourseMembership 不应该导出(不应该存在它的端点),因此,相应的存储库也不存在/不必要。

    以下是三个实体/表的三个类:

    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));
      }
    
    }
    

    当课程会员资格第一次被标记时,一切正常,元素被插入:

    ~$  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"
        }
      }
    }
    

    但在接下来的PATCH操作中,我遇到了困难:

    问题/疑问:

    当我试图替换列表时(例如,用空列表或由其他元素组成的另一个列表),要么什么都不会发生(没有 orphanRemoval = true )或出现异常:

    没有 孤儿删除=true :

    在PATCH响应中,集合看起来已修改,但在PATCH后使用GET,响应将显示原始集合。

    (预设:两个元素所在之处)

    ~$  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"
        }
      }
    }
    

    具有 孤儿删除=true :

    清除现有列表是可能的,但“用另一个列表替换”会导致异常。

    (假设:有一个元素)

    ~$ 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]
    
    

    也许有一个有用的提示:当集合中最初只有一个元素,并且一个PATCH包含一个由另一个元素组成的集合时,该元素会被更新,而不是被删除和插入:

    (前提:之前没有课程会员资格的地方)

    ~$ 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=?
    
    0 回复  |  直到 3 年前
        1
  •  1
  •   hjoeren    3 年前

    由于无论如何都没有计划存储库,因此可以更改 CourseMembership 从a @Entity 到a @Embeddable 以及协会 Student @OneToMany 到a @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<>();
    
    }
    

    鉴于此,问题的PATCH操作按预期运行,我也认为,从模型来看,这将是更清洁的方式(l“ 课程会员资格 作为 @可嵌入 more表示有一个组成,没有就没有用 学生 ")