QueryDSL Constructor Projection,select想要一个Entity,但是ctor(和结果)是一个List

Posted

技术标签:

【中文标题】QueryDSL Constructor Projection,select想要一个Entity,但是ctor(和结果)是一个List【英文标题】:QueryDSL Constructor Projection, select wants a single Entity, but ctor (and the result) is a List 【发布时间】:2021-11-28 12:58:20 【问题描述】:

我有以下(示例)javax.persistence 实体:

@Entity
@Table(name = "example_data")
@Data //lombok for getters/setters
public class ExampleData extends AbstractEntity  // AbstractEntity contains the ID only
    @Column(name = "data_content")
    @NotNull
    private String dataContent;

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

我有这个实体通过外键链接到 ExampleData:

@Entity
@Table(name = "another_entity")
@Data
public class AnotherEntity extends AbstractEntity 
    // ... stuff

    @ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
    @JoinColumn(name = "example_data_id")
    private ExampleData exampleData;

在我的前端,我有一个按钮,可以按需生成所选 ExampleData 实例的 JSON 导出。我创建了以下类来包含导出的 JSON 中所需的所有内容:

@NoArgsConstructor //lombok to generate ctor
@Data //lombok for getters/setters
public class ExampleDataExport extends ExampleData 
    
    public ExampleDataExport(Long id, 
                             String dataContent, 
                             String code,
                             List<AnotherEntity> attachedList) 
         // ... all set
    

    private List<AnotherEntity> attachedList; //List of other Entity required for export

现在,我想使用 QueryDSL Projections.constructor 来使用 ExampleDataExport 的构造函数来选择此类实例所需的所有内容:

// I have static imported all the QueryDSL QObjects such as:
import static my.study.entity.QExampleData.exampleData;
import static my.study.entity.QAnotherEntity.anotherEntity;
//...
public List<ExampleDataExport> exportSelected(List<String> codes) 
     return new JPAQuery<ExampleDataExport>(entityManager)
         .select(Projections.constructor(
             ExampleDataExport.class,
             exampleData.id,
             exampleData.dataContent,
             exampleData.code,
             JPAExpressions.select(anotherEntity)
                           .from(anotherEntity)
                           .where(anotherEntity.exampleData().eq(exampleData) 
             ))
             .leftJoin(anotherEntity.exampleData(), exampleData)
             .from(exampleData)
             .where(exampleData.code.in(codes))
             .fetch();

简而言之,我想要实现的是将所有 ExampleData 及其对应的 AnotherEntitiy-s 选择到一个 ExampleDataExport 实例中(然后我可以将其发送到我的前端)。

问题:我尝试了很多变体来替换“JPAExpression”,但是在所有情况下,问题都是一样的。创建查询时,QueryDSL 在 ExampleDataExport 中找不到“匹配构造函数”,因为它会搜索匹配的构造函数:

[class java.lang.Long, class java.lang.String, class java.lang.String, class my.study.entity.AnotherEntity]

而不是

[class java.lang.Long, class java.lang.String, class java.lang.String, class java.util.List]

我尝试过使用.leftJoin(anotherEntity.exampleData(), exampleData),或者只使用select(..., anotherEntity) 而不是JPAExpression 并使用.where(.../*same condition as in JPAExpression*/) 等等,但不知道如何解决这个问题。

我之前也成功地使用了构造函数投影来“选择”一个非实体类,其中我使用左连接来附加所有必需的。

我的问题是:

    我做错了什么?是否有可能实现这一目标,而我只是看不到解决方案?我是否缺少某种 QueryDsl 语法,如果我使用过它可以解决我的问题? 我无法使用@ElementCollection,因为我的 ExampleDataExport 类不是实体。我错过了什么吗?

    我是不是从错误的角度解决问题,我不应该使用构造函数投影来实现我的目标,而只使用两个单独的查询来获取所有数据?或者将 ExampleDataExport 变成一个实体可以解决我的问题吗?那会是一个好方法/好代码吗?

    这是构造函数投影的利基用例吗?我正在努力学习,这似乎是一个很好的例子。

【问题讨论】:

【参考方案1】:

select(Projections.constructor(clasz, a, b)) 只是select(a, b) 周围的语法糖,它可以像这样转换查询结果:

getResultList().stream().map(tuple -> new Clasz(tuple.get(a), tuple.get(b))).collect(toList())

所以最终呈现的查询片段只是SELECT a, b,这意味着您最终会得到包含ab 的元组,而不是每个ab 列表。 JPQL 是 JPA 的查询语言,缺少像元组中的列表这样的嵌入式结构的概念。这并不奇怪,因为 SQL 也缺少这样的概念。虽然确实存在一些特定于供应商的解决方案,例如结合 PostgreSQL 记录和数组类型,但这些解决方案几乎不可能应用于整个 Hibernate 和 Querydsl 的查询链。

最可行的解决方案是在 Java 端收集到内存中的列表。 Querydsl 实际上确实为此提供了语法糖,形式为GroupBy 表达式。所以可以做GroupBy.list(b),Querydsl 将在转换过程中尝试分组到List&lt;B&gt;。但是,当组合投影和 group by 表达式的深度嵌套时,您可能会遇到已知的限制。但是,我认为以下应该可行:

return new JPAQuery<ExampleDataExport>(entityManager)
    .select(Projections.constructor(
         ExampleDataExport.class,
         exampleData.id,
         exampleData.dataContent,
         exampleData.code,
         GroupBy.list( JPAExpressions.select(anotherEntity)
                           .from(anotherEntity)
                           .where(anotherEntity.exampleData().eq(exampleData) 
          ))
         .leftJoin(anotherEntity.exampleData(), exampleData)
         .from(exampleData)
         .where(exampleData.code.in(codes))
         .fetch();

但是,我个人尽量避免 DTO 返回,只要我可以返回具有相同性能的实体:

Map<AnotherEntity, List<ExampleData>> result = new JPAQuery<ExampleDataExport>(entityManager)
         .from(anotherEntity)
         .leftJoin(anotherEntity.exampleData(), exampleData)
         .on(exampleData.code.in(codes))
         .transform(GroupBy.groupBy(anotherEntity).as(GroupBy.list(exampleData));

将返回Map&lt;AnotherEntity, List&lt;ExampleData&gt;&gt; 类型的结果。请注意,子查询也可以很容易地被关联连接替换。同样的优化也适用于其他查询。

【讨论】:

检查我的示例和您的示例生成的 SQL 帮助我了解我做错了什么,这对我来说是一个学习项目,所以我最终使用第二个示例来了解有关转换的更多信息()。谢谢!

以上是关于QueryDSL Constructor Projection,select想要一个Entity,但是ctor(和结果)是一个List的主要内容,如果未能解决你的问题,请参考以下文章

QueryDSL 4 与 RowNumber Window 功能

SpringBoot12 QueryDSL02之利用QueryDSL实现多表关联查询

QueryDSL 的最新状态是啥?

如何使用 Querydsl 更新 JPA 实体?

如何连接多个 queryDSL 表

Querydsl.JPQLQuery 获取记录数