春季数据 JPA – 不同查询的不同连接

huangapple go评论71阅读模式
英文:

Spring Data JPA - Different joins for different queries

问题

使用Java 11、Spring Boot和Spring Data JPA

概述

我在一个MySQL数据库中有3个关联表,我想使用Spring Data JPA来访问它们。为了简单起见,我们称它们为student、course和performance_report。

以下是我的数据类:

@Entity
@Table(name = "student")
@Data
public class Student {

    @Id
    @Column(name = "student_id")
    private Long studentId;

    @Column(name = "student_name")
    private String studentName;

    @OneToMany(mappedBy = "student", fetch = FetchType.EAGER,
            cascade = CascadeType.ALL)
    private List<PerformanceReport> performanceReports;
}
@Entity
@Table(name = "course")
@Data
public class Course {

    @Id
    @Column(name = "course_id")
    private Long courseId;

    @Column(name = "course_name")
    private String courseName;

}
@Entity
@Table(name = "performance_report")
@Data
public class PerformanceReport {

    @Id
    @Column(name = "performance_report_id")
    private Long performanceReportId;

    @ManyToOne(fetch = FetchType.EAGER, optional = false)
    @JoinColumn(name = "student_id", nullable = false)
    // JsonBackReference needed to prevent infinite recursion.
    @JsonBackReference
    private Student student;

    @ManyToOne(fetch = FetchType.EAGER, optional = false)
    @JoinColumn(name = "course_id", nullable = false)
    private Course course;

    @Column(name = "grade")
    private String grade;

    @Column(name = "attendance")
    private String attendance;
}

这是我的StudentRepository:

public interface StudentRepository extends JpaRepository<Student, Long> {

    Optional<Student> findById(Long studentId);
}

调用StudentRepository.findById会产生如下对象:

{
  "studentId": 1,
  "studentName": "Spongebob Squarepants",
  "performanceReports": [
    {
      "performanceReportId": 5473,
      "course": {
        "courseId": 643,
        "courseName": "Boating 101"
      },
      "grade": "F",
      "attendance": "100%"
    },
    {
      "performanceReportId": 4723,
      "course": {
        "courseId": 346,
        "courseName": "Grilling 101"
      },
      "grade": "A+",
      "attendance": "100%"
    }
  ]
}

问题

我还想执行与此操作相反的操作,以便可以查询Course并获得如下对象:

{
  "courseId": 346,
  "courseName": "Grilling 101",
  "performanceReports": [
    {
      "performanceReportId": 4723,
      "student": {
        "studentId": 1,
        "studentName": "Spongebob Squarepants"
      },
      "grade": "A+",
      "attendance": "100%"
    },
    {
      "performanceReportId": 4774,
      "student": {
        "studentId": 4,
        "studentName": "Squidward Tentacles"
      },
      "grade": "C-",
      "attendance": "72%"
    }
  ]
}

我无法通过当前的实体结构实现这一点。

如果我为Course设置与我在Student中所做的方式相同的关联(通过在Course中添加@OneToMany,并在PerformanceReport的第二个@ManyToOne中添加@JsonBackReference),我将无法在结果中获得任何Student数据。这还会阻止Course数据流传到Student查询中。如果我删除@JsonBackReference注释,将会导致无限递归和堆栈溢出错误。

我尝试创建单独的实体来处理这些情况。我从Student中删除了关联,并将其放在扩展Student的类中。然后我对Course和PerformanceReport做同样的操作。这不仅会导致新错误,而且非常混乱。这还要求我为处理这些扩展类创建单独的存储库。

一定有更好的方法。

我是否正确对待了这个问题?Spring Data JPA是否是实现这种任务的最佳方式?如果我想查询Student或Course而不使用任何关联,该怎么办?

我肯定不需要为每种可能的情况创建新的实体。我如何自定义不同查询的表连接方式?

英文:

Using Java 11, Spring Boot, and Spring Data JPA

Overview

I have 3 joined tables in a mysql database that I want to access using Spring Data JPA. For the sake of simplicity, let's call them student, course, and performance_report.

Here are my data classes:

@Entity
@Table(name = &quot;student&quot;)
@Data
public class Student {

    @Id
    @Column(name = &quot;student_id&quot;)
    private Long studentId;

    @Column(name = &quot;student_name&quot;)
    private String studentName;

    @OneToMany(mappedBy = &quot;student&quot;, fetch = FetchType.EAGER,
            cascade = CascadeType.ALL)
    private List&lt;PerformanceReport&gt; performanceReports;
}
@Entity
@Table(name = &quot;course&quot;)
@Data
public class Course {

    @Id
    @Column(name = &quot;course_id&quot;)
    private Long courseId;

    @Column(name = &quot;course_name&quot;)
    private String courseName;

}
@Entity
@Table(name = &quot;performance_report&quot;)
@Data
public class PerformanceReport {

    @Id
    @Column(name = &quot;performance_report_id&quot;)
    private Long performanceReportId;

    @ManyToOne(fetch = FetchType.EAGER, optional = false)
    @JoinColumn(name = &quot;student_id&quot;, nullable = false)
    // JsonBackReference needed to prevent infinite recursion.
    @JsonBackReference
    private Student student;

    @ManyToOne(fetch = FetchType.EAGER, optional = false)
    @JoinColumn(name = &quot;course_id&quot;, nullable = false)
    private Course course;

    @Column(name = &quot;grade&quot;)
    private String grade;

    @Column(name = &quot;attendance&quot;)
    private String attendance;
}

Here is my StudentRepository:

public interface StudentRepository extends JpaRepository&lt;Student, Long&gt; {

    Optional&lt;Student&gt; findById(Long studentId);
}

Calling StudentRepository.findById produces an object like this:

{
  &quot;studentId&quot;: 1,
  &quot;studentName&quot;: &quot;Spongebob Squarepants&quot;,
  &quot;performanceReports&quot;: [
    {
      &quot;performanceReportId&quot;: 5473,
      &quot;course&quot;: {
        &quot;courseId&quot;: 643,
        &quot;courseName&quot;: &quot;Boating 101&quot;
      },
      &quot;grade&quot;: &quot;F&quot;,
      &quot;attendance&quot;: &quot;100%&quot;
    },
    {
      &quot;performanceReportId&quot;: 4723,
      &quot;course&quot;: {
        &quot;courseId&quot;: 346,
        &quot;courseName&quot;: &quot;Grilling 101&quot;
      },
      &quot;grade&quot;: &quot;A+&quot;,
      &quot;attendance&quot;: &quot;100%&quot;
    }
  ]
}

Problem

I also want to perform the inverse of this operation so I can query Course and get an object like this:

{
  &quot;courseId&quot;: 346,
  &quot;courseName&quot;: &quot;Grilling 101&quot;,
  &quot;performanceReports&quot;: [
    {
      &quot;performanceReportId&quot;: 4723,
      &quot;student&quot;: {
        &quot;studentId&quot;: 1,
        &quot;studentName&quot;: &quot;Spongebob Squarepants&quot;
      },
      &quot;grade&quot;: &quot;A+&quot;,
      &quot;attendance&quot;: &quot;100%&quot;
    },
    {
      &quot;performanceReportId&quot;: 4774,
      &quot;student&quot;: {
        &quot;studentId&quot;: 4,
        &quot;studentName&quot;: &quot;Squidward Tentacles&quot;
      },
      &quot;grade&quot;: &quot;C-&quot;,
      &quot;attendance&quot;: &quot;72%&quot;
    }
  ]
}

I cannot do this with my current entity structure.

If I set up the joins for Course in the same way as I do for Student - by adding a @OneToMany in Course and adding a @JsonBackReference to the second @ManyToOne in PerformanceReport - I won't get any Student data in my result. It will also prevent the Course data from flowing through to the Student query. If I remove the @JsonBackReference annotations, I get infinite recursion and a StackOverflow error.

I tried creating separate entities to account for these scenarios. I removed the join from the Student and put it in a class that extends Student. I then do the same for Course and PerformanceReport. Not only does this cause new errors, but it is very messy. It also requires me to create separate repositories for dealing with these extended classes.

There must be a better way.

Am I approaching this correctly? Is Spring Data JPA the best way to accomplish such a task? What if I want to query Student or Course without using any joins at all?

Surely I don't need new entities for every possible scenario. How can I customize the way I join tables for different queries?

答案1

得分: 1

你的问题与 JPA 关系不大,可以更简洁地表达。我提出并解释了一种解决方案,它使用 @JsonView 注解,而不是 @JsonBackrefence

假设我们有两个类:

Parent:

@Getter @Setter
public class Parent {
    public static class ManagedReferenceView {};
    // 仅为了展示而存在
    private String name = "" + hashCode();
    @JsonView(ManagedReferenceView.class)
    private List<Child> children;
}

Child:

@Getter @Setter
@RequiredArgsConstructor
public class Child {
    public static class BackReferenceView {
    };
    // 仅为了展示而存在
    private String name = "" + hashCode();
    @JsonView(BackReferenceView.class)
    private final Parent parent;
}

正如你所见,视图类不需要实际代码,它们只是要使用的“名称”。关键是告诉序列化何时序列化什么。以下是如何使用它们的示例:

构造一个父对象:

Parent p = new Parent();
p.setChildren(Arrays.asList(new Child(p), new Child(p)));

序列化 Parent

objectMapper.writerWithView(Parent.ManagedReferenceView.class).writeValueAsString(p);

序列化 Child

objectMapper.writerWithView(Child.BackReferenceView.class).writeValueAsString(p);

至于你问题中关于 JPA 优化的部分,你可以在连接上添加 fetch = FetchType.LAZY,这样 Hibernate 可以决定是否从数据库获取引用是明智的。在最佳情况下,引用在序列化时不会被获取,直到 objectMapper 需要调用引用的 getter 方法。也许你还需要一个不触发 getter 的视图。

此外,你还可以实现 JPA 投影等,但那是另外一个故事了。

英文:

Your problem is not so much related to JPA and can be expressed in a more compact way. I present and explain a solution that uses - instead of @JsonBackrefence - a @JsonView annotation.

Assume we have two classes:

Parent:

@Getter @Setter
public class Parent {
	public static class ManagedReferenceView {};
	// Just to have something to show	
	private String name = &quot;&quot; + hashCode();
	@JsonView(ManagedReferenceView.class)
	private List&lt;Child&gt; children;
}

Child:

@Getter @Setter
@RequiredArgsConstructor
public class Child {
	public static class BackReferenceView {
	};
	// Just to have something to show	
	private String name = &quot;&quot; + hashCode();
	@JsonView(BackReferenceView.class)
	private final Parent parent;
}

As you see the view classes need no code, those are just "names" to be used. The point is to tell what we want to serialize when. And this is how those are used (for example):

Construct some parent:

Parent p = new Parent();
p.setChildren(Arrays.asList(new Child(p), new Child(p)));

Serialize Parent:

objectMapper.writerWithView(Parent.ManagedReferenceView.class).writeValueAsString(p);

Serialize Child:

objectMapper.writerWithView(Child.BackReferenceView.class).writeValueAsString(p);

And for the JPA optimizing part of your question you can add fetch = FetchType.LAZY to your joins which lets Hibernate to decide whether it is wise to fetch references from db. In best case references are not fetched until objectMapper needs to call getter for reference when serializing. And maybe you need a view also that does not trigger getter.

Also you can implement JPA projections and such but that is an another story.

huangapple
  • 本文由 发表于 2020年9月20日 07:48:19
  • 转载请务必保留本文链接:https://go.coder-hub.com/63974330.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定