如何处理具有实体关系的 Spring 引导/弹簧数据投影(嵌套投影)



我正在尝试让嵌套投影在 Spring Boot 中工作。我有 2 个实体,ParentChild,其中ParentChild具有单向@OneToMany关系。

以下是类:(使用龙目岛注释)

@Entity
@Data @NoArgsConstructor
public class Parent {
@Id
@GeneratedValue
private long id;
private String basic;
private String detail;
@OneToMany(fetch = FetchType.EAGER)
private List<Child> children;
public Parent(String basic, String detail, List<Child> children) {
this.basic = basic;
this.detail = detail;
this.children = children;
}
}
@Entity
@Data @NoArgsConstructor
public class Child {
@Id
@GeneratedValue(strategy = GenerationType.TABLE)
private long id;
private String basic;
private String detail;
public Child(String basic, String detail) {
this.basic = basic;
this.detail = detail;
}
}

当我在没有投影的情况下获取数据时,我得到以下内容:

[
{
"id": 1,
"basic": "parent-basic-1",
"detail": "parent-detail-1",
"children": [
{
"id": 1,
"basic": "child-basic-1",
"detail": "child-detail-1"
},
{
"id": 2,
"basic": "child-basic-2",
"detail": "child-detail-2"
}
]
},
{
"id": 2,
"basic": "parent-basic-2",
"detail": "parent-detail-2",
"children": [
{
"id": 3,
"basic": "child-basic-3",
"detail": "child-detail-3"
},
{
"id": 4,
"basic": "child-basic-4",
"detail": "child-detail-4"
}
]
}

目标如下:

{
"id": 1,
"basic": "parent-basic-1",
"children": [1,2]
},
{
"id": 2,
"basic": "parent-basic-2",
"children": [3,4]
}

然而,这似乎完全不可能实现。

  1. 到目前为止,我已经尝试了构造函数投影
@Value
public class ParentDto {
long id;
String basic;
// wanted to get it to work with just Child instead of ChildDto first, before getting ChildDto to work
Collection<Child> children; 
public ParentDto(long id, String basic, Collection<Child> children) {
this.id = id;
this.basic = basic;
this.children = children;
}
}
// Constructor Projection in Repository
@Query("select new whz.springbootdemo.application.constructor_projection.ParentDto(p.id, p.basic, p.children) from Parent p")
List<ParentDto> findAllConstructorProjected();

但这会导致以下错误:

could not prepare statement; SQL [select parent0_.id as col_0_0_, parent0_.basic as col_1_0_, . as col_2_0_ from parent parent0_ inner join parent_children children1_ on parent0_.id=children1_.parent_id inner join child child2_ on children1_.children_id=child2_.id]; nested exception is org.hibernate.exception.SQLGrammarException: could not prepare statement
  1. 尝试动态投影
// Dynamic Projection in Repository
List<ParentDto> findAllDynamicProjectionBy();

导致以下错误:

org.hibernate.hql.internal.ast.QuerySyntaxException:
Unable to locate appropriate constructor on class [whz.springbootdemo.application.constructor_projection.ParentDto].
Expected arguments are: <b>long, java.lang.String, whz.springbootdemo.application.child.Child</b>
[select new whz.springbootdemo.application.constructor_projection.ParentDto(generatedAlias0.id, generatedAlias0.basic, children) from whz.springbootdemo.application.parent.Parent as generatedAlias0 left join generatedAlias0.children as children]; nested exception is java.lang.IllegalArgumentException: org.hibernate.hql.internal.ast.QuerySyntaxException: Unable to locate appropriate constructor on class [whz.springbootdemo.application.constructor_projection.ParentDto]. Expected arguments are: long, java.lang.String, whz.springbootdemo.application.child.Child [select new whz.springbootdemo.application.constructor_projection.ParentDto(generatedAlias0.id, generatedAlias0.basic, children) from whz.springbootdemo.application.parent.Parent as generatedAlias0 left join generatedAlias0.children as children]

这基本上告诉我执行了联接,但值没有按父级的 ID 分组,从而产生 x 行,其中 x 是父级拥有的子项数,每个子项都有父项的基本信息和其中一个子项信息。

  1. 唯一"有效"的是接口投影
// Interface Projection in Repository
List<ParentDtoInterface> findAllInterfaceProjectedBy();
public interface ParentDtoInterface {
long getId();
String getBasic();
List<ChildDtoInterface> getChildren();
}
public interface ChildDtoInterface {
long getId();
}

它导致:

[
{
"id": 1,
"children": [
{
"id": 1
},
{
"id": 2
}
],
"basic": "parent-basic-1"
},
{
"id": 2,
"children": [
{
"id": 3
},
{
"id": 4
}
],
"basic": "parent-basic-2"
}
]

现在我对接口投影的问题是,它不仅会加载预期的属性,还会加载所有属性,但 jackson 只会序列化接口提供的属性,因为它使用类/接口定义。

父加载:(sql 日志;请参阅第 4 行,加载详细信息)

select
parent0_.id as id1_1_,
parent0_.basic as basic2_1_,
parent0_.detail as detail3_1_ 
from
parent parent0_

此外,接口投影似乎真的很慢(请参阅此 Stackoverflow 问题),我仍然必须解压缩子项,因为它们被赋予为 [{id:1},{id:2}],但我真的需要 [1,2]。我知道我可以用@JsonIdentityReference(alwaysAsId = true)做到这一点,但这只是一个解决方法。

我也感到困惑,为什么数据在 n+1 个查询中加载 - 1 个用于父母,另一个 n(其中 n 是每个父母孩子的父母数量):

select
parent0_.id as id1_1_,
parent0_.basic as basic2_1_,
parent0_.detail as detail3_1_ 
from
parent parent0_
select
children0_.parent_id as parent_i1_2_0_,
children0_.children_id as children2_2_0_,
child1_.id as id1_0_1_,
child1_.basic as basic2_0_1_,
child1_.detail as detail3_0_1_ 
from
parent_children children0_ 
inner join
child child1_ 
on children0_.children_id=child1_.id 
where
children0_.parent_id=?
//... omitting further child queries

我已经尝试了@OneToMany(fetch=FetchType.LAZY)@Fetch(FetchType.JOINED)- 两者都给出了与上述相同的结果。

所以主要问题是:有没有办法使用 Spring Boot 为嵌套实体实现投影,以便只加载所需的数据尽可能少的查询,在最好的情况下,我可以调整它,以便不必加载列表子项,我可以只加载列表 childIds(也许通过 Jpa 查询,该查询按 parentid 对连接的行进行分组,让我们提取所需的数据来自孩子?

我正在使用休眠和内存数据库。

感谢您的任何答案或提示!

编辑:澄清一下:我不是想找到一种以所需格式序列化数据的方法 - 我已经可以实现这一点。主要重点是仅从数据库中加载必要的信息。

这将始终获取子项,但可以为您提供所需的结果。

public interface SimpleParentProjection {
String getBasic();
String getDetail();
@Value("#{T(SimpleParentProjection).toId(target.getChildren())}")
String[] getChildren();
static String[] toId(Set<Child> childSet) {
return childSet.stream().map(c -> String.valueOf(c.getId())).toArray(String[]::new);
}
}

最新更新