将实体从多方持久化到一侧时事务会回滚

Ksh*_*tij 1 jpa spring-data spring-data-jpa spring-boot

我在数据库中有这个关联 - 表结构在数据库中看起来像这样 -

我希望数据像这样保存在表中 - 表中的记录

相应的 JPA 实体已按此方式建模(为简单起见,省略了 getter/setter)-

学生实体 -

@Entity 
@Table(name = "student")
public class Student {
    @Id
    @SequenceGenerator(name = "student_pk_generator", sequenceName = 
                              "student_pk_sequence", allocationSize = 1)
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = 
                               "student_pk_generator")
    @Column(name = "student_id", nullable = false)
    private Long studentId;

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

    @OneToMany(mappedBy = "student", cascade = CascadeType.ALL)
    private Set<StudentSubscription> studentSubscription;
}
Run Code Online (Sandbox Code Playgroud)

STUDENT_SUBSCRIPTION 实体 -

@Entity
@Table(name = "student_subscription")
@Inheritance(strategy = InheritanceType.JOINED)
public abstract class StudentSubscription {
    @Id
    private Long studentId;

    @ManyToOne(optional = false)
    @JoinColumn(name = "student_id", referencedColumnName = "student_id")
    @MapsId
    private Student student;

    @Column(name = "valid_from")
    private Date validFrom;

    @Column(name = "valid_to")
    private Date validTo;
}
Run Code Online (Sandbox Code Playgroud)

LIBRARY_SUBSCRIPTION 实体 -

@Entity
@Table(name = "library_subscription", 
       uniqueConstraints = {@UniqueConstraint(columnNames = {"library_code"})})
@PrimaryKeyJoinColumn(name = "student_id")
public class LibrarySubscription extends StudentSubscription {

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

    @PrePersist
    private void generateLibraryCode() {
        this.libraryCode = // some logic to generate unique libraryCode
    }

}
Run Code Online (Sandbox Code Playgroud)

COURSE_SUBSCRIPTION 实体 -

@Entity
@Table(name = "course_subscription", 
       uniqueConstraints = {@UniqueConstraint(columnNames = {"course_code"})})
@PrimaryKeyJoinColumn(name = "student_id")
public class CourseSubscription extends StudentSubscription {

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

    @PrePersist
    private void generateCourseCode() {
        this.courseCode = // some logic to generate unique courseCode
    }
}
Run Code Online (Sandbox Code Playgroud)

现在,已经有一个学生实体,其 ID 为 100。现在我想保留该学生的图书馆订阅。为此,我使用 Spring DATA JPA 存储库创建了一个简单的测试 -

@Test
public void testLibrarySubscriptionPersist() {
    Student student = studentRepository.findById(100L).get();
    StudentSubscription librarySubscription = new LibrarySubscription();
    librarySubscription.setValidFrom(//some date);
    librarySubscription.setValidTo(//some date);
    librarySubscription.setStudent(student);
    studentSubscriptionRepository.save(librarySubscription);
}
Run Code Online (Sandbox Code Playgroud)

运行此测试时我收到异常 -

org.springframework.dao.InvalidDataAccessApiUsageException: detached entity passed to persist: com.springboot.data.jpa.entity.Student; nested exception is org.hibernate.PersistentObjectException: detached entity passed to persist: com.springboot.data.jpa.entity.Student
Run Code Online (Sandbox Code Playgroud)

为了解决这个问题,我将 @Transactional 附加到测试中。这修复了分离实体的上述异常,但实体 StudentSubscription 和 LibrarySubscription 没有持久化到数据库。事实上,交易正在回滚。

在日志中获取此异常 -

INFO 3515 --- [           main] o.s.t.c.transaction.TransactionContext   : Rolled back transaction for test: [DefaultTestContext@35390ee3 testClass = SpringDataJpaApplicationTests, testInstance = com.springboot.data.jpa.SpringDataJpaApplicationTests@48a12036, testMethod = testLibrarySubscriptionPersist@SpringDataJpaApplicationTests, testException = [null], mergedContextConfiguration = [MergedContextConfiguration@5e01a982 testClass = SpringDataJpaApplicationTests, locations = '{}', classes = '{class com.springboot.data.jpa.SpringDataJpaApplication}', contextInitializerClasses = '[]', activeProfiles = '{}', propertySourceLocations = '{}', propertySourceProperties = '{org.springframework.boot.test.context.SpringBootTestContextBootstrapper=true}', contextCustomizers = set[org.springframework.boot.test.context.filter.ExcludeFilterContextCustomizer@18ece7f4, org.springframework.boot.test.json.DuplicateJsonObjectContextCustomizerFactory$DuplicateJsonObjectContextCustomizer@264f218, org.springframework.boot.test.mock.mockito.MockitoContextCustomizer@0, org.springframework.boot.test.web.client.TestRestTemplateContextCustomizer@2462cb01, org.springframework.boot.test.autoconfigure.actuate.metrics.MetricsExportContextCustomizerFactory$DisableMetricExportContextCustomizer@928763c, org.springframework.boot.test.autoconfigure.properties.PropertyMappingContextCustomizer@0, org.springframework.boot.test.autoconfigure.web.servlet.WebDriverContextCustomizerFactory$Customizer@7c3fdb62, org.springframework.boot.test.context.SpringBootTestArgs@1, org.springframework.boot.test.context.SpringBootTestWebEnvironment@1ad282e0], contextLoader = 'org.springframework.boot.test.context.SpringBootContextLoader', parent = [null]], attributes = map['org.springframework.test.context.event.ApplicationEventsTestExecutionListener.recordApplicationEvents' -> false]]
Run Code Online (Sandbox Code Playgroud)

现在我有几个问题 -

  1. 为什么我会遇到分离实体异常。当我们从数据库中获取实体时,Spring Data JPA 必须使用entityManager 来获取实体。获取的实体会自动附加到持久性上下文,对吗?

  2. 在测试中附加 @Transactional 时,为什么事务会回滚,并且没有实体被持久化。我期望这两个实体 - StudentSubscription 和 LibrarySubscription 应该使用连接表继承方法进行持久化。

我尝试了很多事情但没有运气。向 JPA 和 Spring DATA 专家寻求帮助:-)

提前致谢。

Oli*_*ohm 9

让我添加一些细节,概述您的代码中的一些设计问题,这些问题使情况变得非常复杂。一般来说,在使用 Spring Data 时,您不能简单地查看表、为这些表创建千篇一律的实体和存储库并期望事情能够简单地工作。您至少需要花一些时间来理解领域驱动设计构建块实体、聚合和存储库。

\n

存储库管理聚合

\n

在您的情况下,StudentStudentSubscriptions 视为一个实体(完整的对象引用,级联持久性操作),但同时\xe2\x80\xa6Subscription存在一个用于持久化 s 的存储库。这从根本上打破了保持聚合一致性的责任Student,因为您可以简单地\xe2\x80\xa6Subscription通过存储库从存储中删除 a,而聚合没有机会干预。假设\xe2\x80\xa6Subscriptions 本身是聚合,并且您希望保持该方向的依赖关系,则只能通过标识符引用它们,而不是通过完整的对象表示来引用。

\n

这种安排还增加了认知负担,因为现在有两种添加订阅的方法:

\n
    \n
  1. 创建一个\xe2\x80\xa6Subscription实例,分配Student,通过存储库保留订阅。
  2. \n
  3. 加载一个Student,创建一个\xe2\x80\xa6Subscription,将其添加到学生,Student通过它的存储库保存。
  4. \n
\n

虽然这已经是一种味道,但 和 之间的双向关系\xe2\x80\xa6Subscription强制Student需要在代码中手动管理这些关系。此外,这些关系在概念之间建立了依赖循环,这使得整个安排很难改变。您已经看到,对于一个相当简单的示例,您已经积累了很多(映射)复杂性。

\n

更好的替代方案会是什么样子?

\n

选项 1(可能性较小):Students 和\xe2\x80\xa6Subscriptions 是“一”

\n

如果您想让这些概念紧密结合在一起,并且不需要单独查询订阅,您可以避免这些订阅聚合并删除它们的存储库。\xe2\x80\xa6Subscription这将允许您删除to的反向引用Student,并只留下一种添加订阅的方法:加载Student、添加\xe2\x80\xa6Subscription实例、保存Student、完成。这也赋予了Student聚合其核心责任:在其状态上强制执行不变量(必须遵循一些规则的集合\xe2\x80\xa6Subscription,例如至少选择一个等)

\n

选项 2(更有可能):Students 和\xe2\x80\xa6Subscriptions 是单独的聚合(可能来自单独的逻辑模块)

\n

在这种情况下,我会\xe2\x80\xa6SubscriptionStudent整个中删除 s。如果您需要查找某个Students \xe2\x80\xa6Subscription,您可以向 中添加查询\xe2\x80\xa6SubscriptionRepository(例如List<\xe2\x80\xa6Subscription> findByStudentId(\xe2\x80\xa6))。作为其副作用,您删除了循环并且Student不再(必须)了解有关\xe2\x80\xa6Subscriptions 的任何信息,这简化了映射。无需与急切/延迟加载等进行斗争。如果适用任何跨聚合规则,这些规则将应用于面向SubscriptionRepository.

\n

启发式总结

\n
    \n
  • 明确区分什么是聚合和什么不是聚合(前者有相应的存储库,后者则没有)
  • \n
  • 仅通过标识符引用聚合。
  • \n
  • 避免双向关系。通常,关系的一侧可以替换为存储库上的查询方法。
  • \n
  • 尝试对从高级概念到低级概念的依赖关系进行建模(Student带 s 的Subscriptionss 可能有意义,\xe2\x80\xa6Subscription不带 a 的sStudent很可能没有意义。因此,后者与模型的关系更好,并且只能使用。)
  • \n
\n