使用 JPQL 或 HQL 时如何将 @OneToMany 字段映射到 List<DTO>?
How to map @OneToMany field into List<DTO> when using JPQL or HQL?
我有以下实体:
@Entity
public class CityExpert {
@Id
private long id;
@OneToOne
private User user;
@OneToMany(mappedBy = "cityExpert")
private List<CityExpertDocument> documents;
// Lots of other fields...
}
@Entity
public class CityExpertDocument {
@Id
private long id;
@ManyToOne
private CityExpert cityExpert;
// Lots of other fields...
}
@Entity
public class User {
@Id
private long id;
private String name;
private String email;
// Lots of other fields...
}
我有以下 HQL 查询,其中 select CityExpert
的一个子集:
"select " +
"e " +
"from " +
"CityExpert e " +
"where " +
"( (lower(e.user.name) like concat('%', lower(?1), '%') or e.user.name is null) or ?1 = '' ) " +
"and " +
"( (lower(e.user.phone) like concat('%', lower(?2), '%') or e.user.phone is null) or ?2 = '' ) "
但是,由于 CityExpert
字段太多,我不想 select 所有字段。因此,我将查询更改如下:
"select " +
"e.user.name, " +
"e.user.email, " +
"e.documents " +
"from " +
"CityExpert e " +
"where " +
"( (lower(e.user.name) like concat('%', lower(?1), '%') or e.user.name is null) or ?1 = '' ) " +
"and " +
"( (lower(e.user.phone) like concat('%', lower(?2), '%') or e.user.phone is null) or ?2 = '' ) "
但是,显然我们不能 select 像这样的实体中的一对多字段,因为我在前面的查询中得到了 MySQLSyntaxErrorException
(请参阅 ).因此,我已将查询更改为以下内容:
"select " +
"e.user.name, " +
"e.user.email, " +
"d " +
"from " +
"CityExpert e " +
"left join " +
"e.documents d" +
"where " +
"( (lower(e.user.name) like concat('%', lower(?1), '%') or e.user.name is null) or ?1 = '' ) " +
"and " +
"( (lower(e.user.phone) like concat('%', lower(?2), '%') or e.user.phone is null) or ?2 = '' ) "
然而,这次结果变成了List<Object[]>
,而不是List<CityExpert>
。
我创建了以下 DTO:
public class CityExpertDTO {
private String name;
private String email;
private List<CityExpertDocument> documents;
}
但是,我不知道应该如何将 Hibernate 返回的结果映射到 List<CityExpertDTO>
。我的意思是,我可以手动执行此操作,但肯定有 Hibernate 提供的自动化解决方案。
我正在使用 Spring Data JPA 并按如下方式使用 HQL:
public interface CityExpertRepository extends JpaRepository<CityExpert, Long> {
@Query(
"select " +
"e " +
"from " +
"CityExpert e " +
"where " +
"( (lower(e.user.name) like concat('%', lower(?1), '%') or e.user.name is null) or ?1 = '' ) " +
"and " +
"( (lower(e.user.phone) like concat('%', lower(?2), '%') or e.user.phone is null) or ?2 = '' ) "
)
Set<CityExpert> findUsingNameAndPhoneNumber(String name,
String phoneNumber);
}
如何将结果映射到 CityExpertDTO
?
Table 关系
假设我们有下面的post
和post_comment
table,它们通过post_id
外键列在post_comment
table.
SQL投影
考虑到我们有一个只需要从 post
table 以及 id
中获取 id
和 title
列的用例和 post_comment
table 中的 review
列,我们可以使用以下 JPQL 查询来获取所需的投影:
select p.id as p_id,
p.title as p_title,
pc.id as pc_id,
pc.review as pc_review
from PostComment pc
join pc.post p
order by pc.id
当运行上面的投影查询时,我们得到如下结果:
| p.id | p.title | pc.id | pc.review |
|------|-----------------------------------|-------|---------------------------------------|
| 1 | High-Performance Java Persistence | 1 | Best book on JPA and Hibernate! |
| 1 | High-Performance Java Persistence | 2 | A must-read for every Java developer! |
| 2 | Hypersistence Optimizer | 3 | It's like pair programming with Vlad! |
DTO 投影
但是,我们不想使用基于表格的 ResultSet
或默认的 List<Object[]>
JPA 或 Hibernate 查询投影。我们想将上述查询结果集转换为 List
个 PostDTO
个对象,每个这样的对象都有一个 comments
集合,其中包含所有关联的 PostCommentDTO
个对象:
我们可以使用 Hibernate ResultTransformer
,如下例所示:
List<PostDTO> postDTOs = entityManager.createQuery("""
select p.id as p_id,
p.title as p_title,
pc.id as pc_id,
pc.review as pc_review
from PostComment pc
join pc.post p
order by pc.id
""")
.unwrap(org.hibernate.query.Query.class)
.setResultTransformer(new PostDTOResultTransformer())
.getResultList();
assertEquals(2, postDTOs.size());
assertEquals(2, postDTOs.get(0).getComments().size());
assertEquals(1, postDTOs.get(1).getComments().size());
PostDTOResultTransformer
将定义 Object[]
投影与包含 PostCommentDTO
子 DTO 对象的 PostDTO
对象之间的映射:
public class PostDTOResultTransformer
implements ResultTransformer {
private Map<Long, PostDTO> postDTOMap = new LinkedHashMap<>();
@Override
public Object transformTuple(
Object[] tuple,
String[] aliases) {
Map<String, Integer> aliasToIndexMap = aliasToIndexMap(aliases);
Long postId = longValue(tuple[aliasToIndexMap.get(PostDTO.ID_ALIAS)]);
PostDTO postDTO = postDTOMap.computeIfAbsent(
postId,
id -> new PostDTO(tuple, aliasToIndexMap)
);
postDTO.getComments().add(
new PostCommentDTO(tuple, aliasToIndexMap)
);
return postDTO;
}
@Override
public List transformList(List collection) {
return new ArrayList<>(postDTOMap.values());
}
}
aliasToIndexMap
只是一个小实用程序,它允许我们构建一个 Map
结构,该结构关联列别名和列值在 Object[]
tuple
数组:
public Map<String, Integer> aliasToIndexMap(
String[] aliases) {
Map<String, Integer> aliasToIndexMap = new LinkedHashMap<>();
for (int i = 0; i < aliases.length; i++) {
aliasToIndexMap.put(aliases[i], i);
}
return aliasToIndexMap;
}
postDTOMap
是我们要存储所有 PostDTO
实体的地方,这些实体最终将由查询执行返回。我们使用 postDTOMap
的原因是父行在每个子记录的 SQL 查询结果集中重复。
computeIfAbsent
方法允许我们仅在 postDTOMap
.
中没有现有 PostDTO
引用的情况下创建一个 PostDTO
对象
PostDTO
class 有一个构造函数,可以使用专用列别名设置 id
和 title
属性:
public class PostDTO {
public static final String ID_ALIAS = "p_id";
public static final String TITLE_ALIAS = "p_title";
private Long id;
private String title;
private List<PostCommentDTO> comments = new ArrayList<>();
public PostDTO(
Object[] tuples,
Map<String, Integer> aliasToIndexMap) {
this.id = longValue(tuples[aliasToIndexMap.get(ID_ALIAS)]);
this.title = stringValue(tuples[aliasToIndexMap.get(TITLE_ALIAS)]);
}
//Getters and setters omitted for brevity
}
PostCommentDTO
的构建方式类似:
public class PostCommentDTO {
public static final String ID_ALIAS = "pc_id";
public static final String REVIEW_ALIAS = "pc_review";
private Long id;
private String review;
public PostCommentDTO(
Object[] tuples,
Map<String, Integer> aliasToIndexMap) {
this.id = longValue(tuples[aliasToIndexMap.get(ID_ALIAS)]);
this.review = stringValue(tuples[aliasToIndexMap.get(REVIEW_ALIAS)]);
}
//Getters and setters omitted for brevity
}
就是这样!
使用PostDTOResultTransformer
,SQL结果集可以转换为层次结构的DTO投影,使用起来非常方便,特别是如果需要将其编组为[=95] =] 回应:
postDTOs = {ArrayList}, size = 2
0 = {PostDTO}
id = 1L
title = "High-Performance Java Persistence"
comments = {ArrayList}, size = 2
0 = {PostCommentDTO}
id = 1L
review = "Best book on JPA and Hibernate!"
1 = {PostCommentDTO}
id = 2L
review = "A must read for every Java developer!"
1 = {PostDTO}
id = 2L
title = "Hypersistence Optimizer"
comments = {ArrayList}, size = 1
0 = {PostCommentDTO}
id = 3L
review = "It's like pair programming with Vlad!"
我有以下实体:
@Entity
public class CityExpert {
@Id
private long id;
@OneToOne
private User user;
@OneToMany(mappedBy = "cityExpert")
private List<CityExpertDocument> documents;
// Lots of other fields...
}
@Entity
public class CityExpertDocument {
@Id
private long id;
@ManyToOne
private CityExpert cityExpert;
// Lots of other fields...
}
@Entity
public class User {
@Id
private long id;
private String name;
private String email;
// Lots of other fields...
}
我有以下 HQL 查询,其中 select CityExpert
的一个子集:
"select " +
"e " +
"from " +
"CityExpert e " +
"where " +
"( (lower(e.user.name) like concat('%', lower(?1), '%') or e.user.name is null) or ?1 = '' ) " +
"and " +
"( (lower(e.user.phone) like concat('%', lower(?2), '%') or e.user.phone is null) or ?2 = '' ) "
但是,由于 CityExpert
字段太多,我不想 select 所有字段。因此,我将查询更改如下:
"select " +
"e.user.name, " +
"e.user.email, " +
"e.documents " +
"from " +
"CityExpert e " +
"where " +
"( (lower(e.user.name) like concat('%', lower(?1), '%') or e.user.name is null) or ?1 = '' ) " +
"and " +
"( (lower(e.user.phone) like concat('%', lower(?2), '%') or e.user.phone is null) or ?2 = '' ) "
但是,显然我们不能 select 像这样的实体中的一对多字段,因为我在前面的查询中得到了 MySQLSyntaxErrorException
(请参阅
"select " +
"e.user.name, " +
"e.user.email, " +
"d " +
"from " +
"CityExpert e " +
"left join " +
"e.documents d" +
"where " +
"( (lower(e.user.name) like concat('%', lower(?1), '%') or e.user.name is null) or ?1 = '' ) " +
"and " +
"( (lower(e.user.phone) like concat('%', lower(?2), '%') or e.user.phone is null) or ?2 = '' ) "
然而,这次结果变成了List<Object[]>
,而不是List<CityExpert>
。
我创建了以下 DTO:
public class CityExpertDTO {
private String name;
private String email;
private List<CityExpertDocument> documents;
}
但是,我不知道应该如何将 Hibernate 返回的结果映射到 List<CityExpertDTO>
。我的意思是,我可以手动执行此操作,但肯定有 Hibernate 提供的自动化解决方案。
我正在使用 Spring Data JPA 并按如下方式使用 HQL:
public interface CityExpertRepository extends JpaRepository<CityExpert, Long> {
@Query(
"select " +
"e " +
"from " +
"CityExpert e " +
"where " +
"( (lower(e.user.name) like concat('%', lower(?1), '%') or e.user.name is null) or ?1 = '' ) " +
"and " +
"( (lower(e.user.phone) like concat('%', lower(?2), '%') or e.user.phone is null) or ?2 = '' ) "
)
Set<CityExpert> findUsingNameAndPhoneNumber(String name,
String phoneNumber);
}
如何将结果映射到 CityExpertDTO
?
Table 关系
假设我们有下面的post
和post_comment
table,它们通过post_id
外键列在post_comment
table.
SQL投影
考虑到我们有一个只需要从 post
table 以及 id
中获取 id
和 title
列的用例和 post_comment
table 中的 review
列,我们可以使用以下 JPQL 查询来获取所需的投影:
select p.id as p_id,
p.title as p_title,
pc.id as pc_id,
pc.review as pc_review
from PostComment pc
join pc.post p
order by pc.id
当运行上面的投影查询时,我们得到如下结果:
| p.id | p.title | pc.id | pc.review |
|------|-----------------------------------|-------|---------------------------------------|
| 1 | High-Performance Java Persistence | 1 | Best book on JPA and Hibernate! |
| 1 | High-Performance Java Persistence | 2 | A must-read for every Java developer! |
| 2 | Hypersistence Optimizer | 3 | It's like pair programming with Vlad! |
DTO 投影
但是,我们不想使用基于表格的 ResultSet
或默认的 List<Object[]>
JPA 或 Hibernate 查询投影。我们想将上述查询结果集转换为 List
个 PostDTO
个对象,每个这样的对象都有一个 comments
集合,其中包含所有关联的 PostCommentDTO
个对象:
我们可以使用 Hibernate ResultTransformer
,如下例所示:
List<PostDTO> postDTOs = entityManager.createQuery("""
select p.id as p_id,
p.title as p_title,
pc.id as pc_id,
pc.review as pc_review
from PostComment pc
join pc.post p
order by pc.id
""")
.unwrap(org.hibernate.query.Query.class)
.setResultTransformer(new PostDTOResultTransformer())
.getResultList();
assertEquals(2, postDTOs.size());
assertEquals(2, postDTOs.get(0).getComments().size());
assertEquals(1, postDTOs.get(1).getComments().size());
PostDTOResultTransformer
将定义 Object[]
投影与包含 PostCommentDTO
子 DTO 对象的 PostDTO
对象之间的映射:
public class PostDTOResultTransformer
implements ResultTransformer {
private Map<Long, PostDTO> postDTOMap = new LinkedHashMap<>();
@Override
public Object transformTuple(
Object[] tuple,
String[] aliases) {
Map<String, Integer> aliasToIndexMap = aliasToIndexMap(aliases);
Long postId = longValue(tuple[aliasToIndexMap.get(PostDTO.ID_ALIAS)]);
PostDTO postDTO = postDTOMap.computeIfAbsent(
postId,
id -> new PostDTO(tuple, aliasToIndexMap)
);
postDTO.getComments().add(
new PostCommentDTO(tuple, aliasToIndexMap)
);
return postDTO;
}
@Override
public List transformList(List collection) {
return new ArrayList<>(postDTOMap.values());
}
}
aliasToIndexMap
只是一个小实用程序,它允许我们构建一个 Map
结构,该结构关联列别名和列值在 Object[]
tuple
数组:
public Map<String, Integer> aliasToIndexMap(
String[] aliases) {
Map<String, Integer> aliasToIndexMap = new LinkedHashMap<>();
for (int i = 0; i < aliases.length; i++) {
aliasToIndexMap.put(aliases[i], i);
}
return aliasToIndexMap;
}
postDTOMap
是我们要存储所有 PostDTO
实体的地方,这些实体最终将由查询执行返回。我们使用 postDTOMap
的原因是父行在每个子记录的 SQL 查询结果集中重复。
computeIfAbsent
方法允许我们仅在 postDTOMap
.
PostDTO
引用的情况下创建一个 PostDTO
对象
PostDTO
class 有一个构造函数,可以使用专用列别名设置 id
和 title
属性:
public class PostDTO {
public static final String ID_ALIAS = "p_id";
public static final String TITLE_ALIAS = "p_title";
private Long id;
private String title;
private List<PostCommentDTO> comments = new ArrayList<>();
public PostDTO(
Object[] tuples,
Map<String, Integer> aliasToIndexMap) {
this.id = longValue(tuples[aliasToIndexMap.get(ID_ALIAS)]);
this.title = stringValue(tuples[aliasToIndexMap.get(TITLE_ALIAS)]);
}
//Getters and setters omitted for brevity
}
PostCommentDTO
的构建方式类似:
public class PostCommentDTO {
public static final String ID_ALIAS = "pc_id";
public static final String REVIEW_ALIAS = "pc_review";
private Long id;
private String review;
public PostCommentDTO(
Object[] tuples,
Map<String, Integer> aliasToIndexMap) {
this.id = longValue(tuples[aliasToIndexMap.get(ID_ALIAS)]);
this.review = stringValue(tuples[aliasToIndexMap.get(REVIEW_ALIAS)]);
}
//Getters and setters omitted for brevity
}
就是这样!
使用PostDTOResultTransformer
,SQL结果集可以转换为层次结构的DTO投影,使用起来非常方便,特别是如果需要将其编组为[=95] =] 回应:
postDTOs = {ArrayList}, size = 2
0 = {PostDTO}
id = 1L
title = "High-Performance Java Persistence"
comments = {ArrayList}, size = 2
0 = {PostCommentDTO}
id = 1L
review = "Best book on JPA and Hibernate!"
1 = {PostCommentDTO}
id = 2L
review = "A must read for every Java developer!"
1 = {PostDTO}
id = 2L
title = "Hypersistence Optimizer"
comments = {ArrayList}, size = 1
0 = {PostCommentDTO}
id = 3L
review = "It's like pair programming with Vlad!"