使用 Cascade ALL 处理 OneToMany 关系的子实体时出现问题

Problem manipulating child entity of a OneToMany relation with Cascade ALL

我在使用双向关系 (OneToMany - ManyToOne) 时遇到性能问题。

我有一个实体 Interaction 可以有很多 ClassifiedGroup(请参阅下面的 类 了解更多详细信息)但我有一个特定的用例,我需要删除几个 ClassifiedGroups 和添加其他一些。

问题是,当我使用 InteractionRepository.java 本身(一个简单的 JpaRepository)来保存相关 ClassifiedGroup 的所有修改时,代码将不得不从与一个链接的数据库中获取所有组相互作用。它还将获取 Interaction 的许多其他属性,这些属性也可能很昂贵。我不想将 Interaction 实体操作为 delete/create new ClassifiedGroup 但我也希望将级联类型设置为 ALL 因为我在交互时创建了第一个组已创建,如果我删除交互本身,则应删除所有组。

我试图为 ClassifiedGroup 实体创建一个 JpaRepository 并直接操作它,但我在尝试保存它时遇到错误:

代码:

      List<ClassifiedGroup> toAdd = generate(); // generate list to add
      toAdd.forEach(g -> g.setInteraction(interaction));

      this.classifiedGroupRepository.saveAll(toAdd);

错误:

org.springframework.beans.ConversionNotSupportedException: Failed to convert property value of type 'java.util.UUID' to required type 'br.com.stilingue.smartcare.entities.Interaction' for property 'interaction'; nested exception is java.lang.IllegalStateException: Cannot convert value of type 'java.util.UUID' to required type 'br.com.stilingue.smartcare.entities.Interaction' for property 'interaction': no matching editors or conversion strategy found

    at org.springframework.beans.AbstractNestablePropertyAccessor.convertIfNecessary(AbstractNestablePropertyAccessor.java:595)
    at org.springframework.beans.AbstractNestablePropertyAccessor.convertForProperty(AbstractNestablePropertyAccessor.java:609)
    at org.springframework.beans.AbstractNestablePropertyAccessor.processLocalProperty(AbstractNestablePropertyAccessor.java:458)
    at org.springframework.beans.AbstractNestablePropertyAccessor.setPropertyValue(AbstractNestablePropertyAccessor.java:278)
    at org.springframework.beans.AbstractNestablePropertyAccessor.setPropertyValue(AbstractNestablePropertyAccessor.java:246)
    at org.springframework.data.util.DirectFieldAccessFallbackBeanWrapper.setPropertyValue(DirectFieldAccessFallbackBeanWrapper.java:75)
    at org.springframework.data.jpa.repository.support.JpaMetamodelEntityInformation$IdentifierDerivingDirectFieldAccessFallbackBeanWrapper.setPropertyValue(JpaMetamodelEntityInformation.java:367)
    at org.springframework.data.jpa.repository.support.JpaMetamodelEntityInformation.getId(JpaMetamodelEntityInformation.java:175)
    at org.springframework.data.repository.core.support.AbstractEntityInformation.isNew(AbstractEntityInformation.java:46)
    at org.springframework.data.jpa.repository.support.JpaMetamodelEntityInformation.isNew(JpaMetamodelEntityInformation.java:246)
    at org.springframework.data.jpa.repository.support.SimpleJpaRepository.save(SimpleJpaRepository.java:596)
    at org.springframework.data.jpa.repository.support.SimpleJpaRepository.saveAll(SimpleJpaRepository.java:631)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:564)
    at org.springframework.data.repository.core.support.RepositoryMethodInvoker$RepositoryFragmentMethodInvoker.lambda$new[=11=](RepositoryMethodInvoker.java:289)
    at org.springframework.data.repository.core.support.RepositoryMethodInvoker.doInvoke(RepositoryMethodInvoker.java:137)
    at org.springframework.data.repository.core.support.RepositoryMethodInvoker.invoke(RepositoryMethodInvoker.java:121)
    at org.springframework.data.repository.core.support.RepositoryComposition$RepositoryFragments.invoke(RepositoryComposition.java:529)
    at org.springframework.data.repository.core.support.RepositoryComposition.invoke(RepositoryComposition.java:285)
    at org.springframework.data.repository.core.support.RepositoryFactorySupport$ImplementationMethodExecutionInterceptor.invoke(RepositoryFactorySupport.java:599)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
    at org.springframework.data.repository.core.support.QueryExecutorMethodInterceptor.doInvoke(QueryExecutorMethodInterceptor.java:163)
    at org.springframework.data.repository.core.support.QueryExecutorMethodInterceptor.invoke(QueryExecutorMethodInterceptor.java:138)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
    at org.springframework.data.projection.DefaultMethodInvokingMethodInterceptor.invoke(DefaultMethodInvokingMethodInterceptor.java:80)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
    at org.springframework.transaction.interceptor.TransactionInterceptor.proceedWithInvocation(TransactionInterceptor.java:123)
    at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:388)
    at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:119)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
    at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:137)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
    at org.springframework.data.jpa.repository.support.CrudMethodMetadataPostProcessor$CrudMethodMetadataPopulatingMethodInterceptor.invoke(CrudMethodMetadataPostProcessor.java:174)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
    at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:97)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
    at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:215)
    at com.sun.proxy.$Proxy197.saveAll(Unknown Source)
    at br.com.stilingue.smartcare.event.interaction.update.strategy.impl.InteractionStilingueArrayUpdateStrategy.updateInteraction(InteractionStilingueArrayUpdateStrategy.java:78)
    at br.com.stilingue.smartcare.event.interaction.update.strategy.impl.InteractionStilingueArrayUpdateStrategy$$FastClassBySpringCGLIB$$b5a01170.invoke(<generated>)
    at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218)
    at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:779)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163)
    at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:750)
    at org.springframework.transaction.interceptor.TransactionInterceptor.proceedWithInvocation(TransactionInterceptor.java:123)
    at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:388)
    at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:119)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
    at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:750)
    at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:692)
    at br.com.stilingue.smartcare.event.interaction.update.strategy.impl.InteractionStilingueArrayUpdateStrategy$$EnhancerBySpringCGLIB$a4de66a.updateInteraction(<generated>)
    at br.com.stilingue.smartcare.event.interaction.update.strategy.impl.InteractionStilingueArrayUpdateStrategyIntegrationTest.testShouldAddNewGroups(InteractionStilingueArrayUpdateStrategyIntegrationTest.java:114)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:564)
    at org.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:688)
    at org.junit.jupiter.engine.execution.MethodInvocation.proceed(MethodInvocation.java:60)
    at org.junit.jupiter.engine.execution.InvocationInterceptorChain$ValidatingInvocation.proceed(InvocationInterceptorChain.java:131)
    at org.junit.jupiter.engine.extension.TimeoutExtension.intercept(TimeoutExtension.java:149)
    at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestableMethod(TimeoutExtension.java:140)
    at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestMethod(TimeoutExtension.java:84)
    at org.junit.jupiter.engine.execution.ExecutableInvoker$ReflectiveInterceptorCall.lambda$ofVoidMethod[=11=](ExecutableInvoker.java:115)
    at org.junit.jupiter.engine.execution.ExecutableInvoker.lambda$invoke[=11=](ExecutableInvoker.java:105)
    at org.junit.jupiter.engine.execution.InvocationInterceptorChain$InterceptedInvocation.proceed(InvocationInterceptorChain.java:106)
    at org.junit.jupiter.engine.execution.InvocationInterceptorChain.proceed(InvocationInterceptorChain.java:64)
    at org.junit.jupiter.engine.execution.InvocationInterceptorChain.chainAndInvoke(InvocationInterceptorChain.java:45)
    at org.junit.jupiter.engine.execution.InvocationInterceptorChain.invoke(InvocationInterceptorChain.java:37)
    at org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:104)
    at org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:98)
    at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeTestMethod(TestMethodTestDescriptor.java:210)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeTestMethod(TestMethodTestDescriptor.java:206)
    at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:131)
    at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:65)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively(NodeTestTask.java:139)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively(NodeTestTask.java:129)
    at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively(NodeTestTask.java:127)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:126)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:84)
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
    at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:38)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively(NodeTestTask.java:143)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively(NodeTestTask.java:129)
    at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively(NodeTestTask.java:127)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:126)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:84)
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
    at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:38)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively(NodeTestTask.java:143)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively(NodeTestTask.java:129)
    at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively(NodeTestTask.java:127)
    at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:126)
    at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:84)
    at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.submit(SameThreadHierarchicalTestExecutorService.java:32)
    at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:57)
    at org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:51)
    at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:108)
    at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:88)
    at org.junit.platform.launcher.core.EngineExecutionOrchestrator.lambda$execute[=11=](EngineExecutionOrchestrator.java:54)
    at org.junit.platform.launcher.core.EngineExecutionOrchestrator.withInterceptedStreams(EngineExecutionOrchestrator.java:67)
    at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:52)
    at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:96)
    at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:75)
    at com.intellij.junit5.JUnit5IdeaTestRunner.startRunnerWithArgs(JUnit5IdeaTestRunner.java:71)
    at com.intellij.rt.junit.IdeaTestRunner$Repeater.execute(IdeaTestRunner.java:38)
    at com.intellij.rt.execution.junit.TestsRepeater.repeat(TestsRepeater.java:11)
    at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:35)
    at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:235)
    at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:54)
Caused by: java.lang.IllegalStateException: Cannot convert value of type 'java.util.UUID' to required type 'br.com.stilingue.smartcare.entities.Interaction' for property 'interaction': no matching editors or conversion strategy found
    at org.springframework.beans.TypeConverterDelegate.convertIfNecessary(TypeConverterDelegate.java:262)
    at org.springframework.beans.AbstractNestablePropertyAccessor.convertIfNecessary(AbstractNestablePropertyAccessor.java:590)
    ... 120 more

我发现了一些其他问题,如果我在父实体中有 CascadeType.PERSIST,我将无法直接操作子实体,因为父实体将是子实体的“所有者”子实体。

但是我要问:有什么办法可以避免这个性能瓶颈吗?我的交互实体还有其他一些 OneToOneManyToManyManyToOneOneToMany 关系,每次我使用 InteractionRepository#save 时,所有关系都从数据库中获取。我真的需要自己 create/delete ClassifiedGroup 个实体。

我的类:

互动

package br.com.stilingue.smartcare.entities;

import com.fasterxml.jackson.annotation.JsonIgnore;
import com.google.cloud.spanner.hibernate.types.SpannerJsonType;
import java.sql.Timestamp;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.OneToMany;
import javax.persistence.Table;
import javax.persistence.UniqueConstraint;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import lombok.experimental.Accessors;
import org.hibernate.annotations.Type;
import org.hibernate.annotations.TypeDef;

@Data
@Entity
@Accessors(chain = true)
@Table(
    name = "interactions",
    uniqueConstraints = {@UniqueConstraint(columnNames = {"pid", "channel", "universe_id"})})
public class Interaction {

  @Id
  @GeneratedValue
  @Type(type = "uuid-char")
  @Column(name = "interaction_id", nullable = false)
  private UUID interactionId;

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

  @Enumerated(EnumType.STRING)
  @Column(name = "channel", nullable = false)
  private Channel channel;

  @Column(name = "universe_id", nullable = false)
  private Long universeId;

  @Column(name = "post_date", nullable = false)
  private Timestamp postDate;

  @ToString.Exclude
  @EqualsAndHashCode.Exclude
  @OneToMany(
      mappedBy = "interaction",
      cascade = CascadeType.ALL,
      fetch = FetchType.LAZY,
      orphanRemoval = true)
  private Set<ClassifiedGroup> groups;

  // many other relations/attributes not relevant for the issue, but they also are loaded when I use InteractionRepository#save method

  // method when the Interaction is created the first time
  public void setRelations() {
    if (this.groups != null) {
      this.groups.forEach(group -> group.setInteraction(this));
    }
  }
}

分类组

package br.com.stilingue.smartcare.entities;

import java.io.Serializable;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.FetchType;
import javax.persistence.Id;
import javax.persistence.IdClass;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.ToString;
import lombok.experimental.Accessors;

@Data
@Entity
@NoArgsConstructor
@Accessors(chain = true)
@Table(name = "classified_groups")
@IdClass(ClassifiedGroup.PrimaryKeys.class)
public class ClassifiedGroup {
  @Data
  public static class PrimaryKeys implements Serializable {
    private Interaction interaction;
    private String descriptor;
    private String value;
  }

  public ClassifiedGroup(String group, ClassificationType type, String descriptor) {
    this.value = group;
    this.type = type;
    this.descriptor = descriptor;
  }

  public ClassifiedGroup(String group, ClassificationType type) {
    this(group, type, "");
  }

  @Id
  @ToString.Exclude
  @EqualsAndHashCode.Exclude
  @ManyToOne(fetch = FetchType.LAZY)
  @JoinColumn(
      name = "interaction_id",
      referencedColumnName = "interaction_id",
      insertable = false,
      updatable = false)
  private Interaction interaction;

  @Enumerated(EnumType.STRING)
  @Column(name = "type", nullable = false)
  private ClassificationType type;

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

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

ClassifiedGroupRepository

package br.com.stilingue.smartcare.repositories;

import br.com.stilingue.smartcare.entities.ClassifiedGroup;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface ClassifiedGroupRepository
    extends JpaRepository<ClassifiedGroup, ClassifiedGroup.PrimaryKeys> {}

InteractionRepository

package br.com.stilingue.smartcare.repositories;

import java.util.Optional;
import java.util.UUID;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface InteractionRepository extends JpaRepository<Interaction, UUID>{}

我目前正在使用 Hibernate 5.6.5.Final

您的映射不正确 - 您需要查看有关如何映射派生 ID 的 JPA 教程,但该错误会告诉您哪里出了问题,指向您的 ClassifiedGroup.interaction 映射。 PK class 必须使用交互的 ID(UUID),而不是交互实例本身。

  public static class PrimaryKeys implements Serializable {
    private UUID interaction;
    private String descriptor;
    private String value;
  }

由于 JPA 需要将值插入定义的“interaction_id”fk,您在 ClassifiedGroup 中的映射应 可写,类似于:

  @Id
  @ManyToOne(fetch = FetchType.LAZY)
  @JoinColumn(
      name = "interaction_id",
      referencedColumnName = "interaction_id")
  private Interaction interaction;

关于你的其他问题 - 是的,有很多方法可以处理你想要的东西,但这取决于你想要投入多少工作。让关系变得懒惰可以避免它们被不必要地获取,但是你然后必须考虑通过其他机制进行序列化和对分离实体的更改。我不相信 Spring/JPA 提供商在关系中收到空值时自动知道我想要做什么,因为可能是数据未序列化,或者可能是更改。因此,在关注性能的地方,我编写了自己的合并方法来处理接收序列化数据(比如来自 REST 接口)并确定它应该如何合并到托管实例中(如果存在)。

另请注意,CascadeType.ALL 可能不是您真正想要的。你所说的只是你想要的是对 Interaction.groups 列表的更改被拾取并且引用的 ClassifiedGroups 与交互一起被删除,并且作为一种说服力,你可以在一次调用中保留一个交互及其组列表。那只是 cascade={PERSIST, REMOVE}。 ALL 包括合并、刷新和分离,它们会产生性能开销,如果您真的不需要它们,可能会让您感到悲伤。特别是合并 - 当您在其组列表中传递带有更改和分类组的交互时,这些分类组中与数据库中内容的任何差异都将被合并。这可能会导致过时数据覆盖等后果,当然会产生很大的开销,因为合并也会级联 ClassifiedGroup 引用。不要轻易使用级联选项。