I have the following (example) javax.persistence Entity:
@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;
}
I have this entity linked via Foreign Key to 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;
}
On my frontend, I have a button that on-demand generates a JSON export of a selected ExampleData instance. I have created the following class to contain everything required to be in the exported 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
}
Now, I want to use QueryDSL Projections.constructor to use the ExampleDataExport's constructor to select everything required into an instance of this class:
// 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();
}
So in short, what I want to achieve, is to select all the ExampleData and their corresponding AnotherEntitiy-s into an ExampleDataExport instance (which then I can send to my frontend).
The issue: I have tried a lot of variations to replace the "JPAExpression", however in all cases, the issue is the same. When creating the query, QueryDSL does not find a "matching constructor" in ExampleDataExport, because it searches for one that matches:
[class java.lang.Long, class java.lang.String, class java.lang.String, class my.study.entity.AnotherEntity]
instead of
[class java.lang.Long, class java.lang.String, class java.lang.String, class java.util.List]
I have tried using .leftJoin(anotherEntity.exampleData(), exampleData)
, or use just select(..., anotherEntity)
instead of a JPAExpression and use a .where(.../*same condition as in JPAExpression*/)
and many more, but could not figure out how to approach this.
I have also used constructor projection successfully before to "select into" a non-entity class, in which I used left-joins to attach all the required.
My questions are:
What am I doing wrong? Is it possible to achieve this, and I just don't see the solution? Am I missing some kind of QueryDsl syntax which would solve my problem if I've had used it? I haven't been able to make use of @ElementCollection because my ExampleDataExport class is not an entity. Did I miss something here?
Am I approaching the problem from the wrong angle, and I shouldnt use a constructor projection to achieve my goal, and just use two separate queries to get all the data instead? Or would making ExampleDataExport into an Entity solve my issue? Would that be a good approach/good code?
Is this a niche use-case for Constructor Projection? I'm trying to learn, and this seemed like a good example.
select(Projections.constructor(clasz, a, b))
is just syntactic sugar around select(a, b)
, that transforms the query result like so:
getResultList().stream().map(tuple -> new Clasz(tuple.get(a), tuple.get(b))).collect(toList())
So eventually the query fragment rendered is just SELECT a, b
which means you'll end up with tuples containing a
and b
, not a list of b
's for every a
. JPQL, the query language for JPA, lacks a concept of embedded structures like lists in tuples. Which is no surprise, because SQL also lacks such a concepts. Some vendor specific solutions do exists though, for example combining PostgreSQL record and array types, but these solutions are almost impossible to apply through the entire query chain of Hibernate and Querydsl.
The most feasible solution is to collect to a list in-memory at the Java side. Querydsl actually does have syntaxtic sugar for this, in the form of GroupBy
expressions. So one could do GroupBy.list(b)
and Querydsl will attempt grouping to a List<B>
during the transformation. However, when combining projections and deep nesting of group by expressions, you may run into known limitations. However, I think the following should work:
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();
However, personally I try to avoid DTO returns, whenever I can just return the entities with the same performance:
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));
Will return a result of type Map<AnotherEntity, List<ExampleData>>
. Note that the subquery also can easily be replaced by an association join. The same optimization is applicable to the other queries as well.