英文:
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 = "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;
}
Here is my StudentRepository:
public interface StudentRepository extends JpaRepository<Student, Long> {
Optional<Student> findById(Long studentId);
}
Calling StudentRepository.findById produces an object like this:
{
"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%"
}
]
}
Problem
I also want to perform the inverse of this operation so I can query Course and get an object like this:
{
"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%"
}
]
}
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 = "" + hashCode();
@JsonView(ManagedReferenceView.class)
private List<Child> children;
}
Child:
@Getter @Setter
@RequiredArgsConstructor
public class Child {
public static class BackReferenceView {
};
// Just to have something to show
private String name = "" + 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.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论