如何在JPQL构造表达式中传递SELECT语句以实现正确DTO投影
JPA类投影查询返回重复DTO,如何实现单条文章对应唯一DTO?
我正在使用JPA基于类的投影构造ArticleDTO,该DTO包含title字段和isWrittenByMe字段——当传入的作者是文章的作者之一时,isWrittenByMe设为true,否则为false。
当前实现
我在JPA仓库中添加了自定义JPQL查询方法:
@Query("SELECT new com.example.learnjpql.dto.ArticleDTO(a.title, :author MEMBER OF a.authors) FROM Article a") List<ArticleDTO> getAllArticles(Author author);
Hibernate生成的SQL查询:
Hibernate: select article0_.title as col_0_0_, ? in (authors1_.author_id) as col_1_0_ from article article0_ cross join article_authors2 authors1_ where article0_.id=authors1_.article_id
当前方法输出结果(出现重复DTO):
[ArticleDTO[title=Article 1, isAuthoredByMe=true], ArticleDTO[title=Article 1, isAuthoredByMe=false], ArticleDTO[title=Article 2, isAuthoredByMe=true], ArticleDTO[title=Article 3, isAuthoredByMe=false], ArticleDTO[title=Article 3, isAuthoredByMe=false], ArticleDTO[title=Article 4, isAuthoredByMe=false], ArticleDTO[title=Article 4, isAuthoredByMe=false], ArticleDTO[title=Article 5, isAuthoredByMe=false]]
期望结果
每个Article对应唯一的DTO,示例如下:
| Title | isWrittenByMe |
|---|---|
| Article 1 | true |
| Article 2 | false |
| Article 3 | true |
| Article 4 | false |
| Article 5 | false |
尝试过的方法
我曾尝试用SELECT语句替代:author MEMBER OF a.authors作为构造函数参数,希望为每条记录执行单独查询,但触发了“unexpected token”错误。
问题原因
当前JPQL中的MEMBER OF会触发Article与关联表article_authors2的交叉连接,一篇文章有多少个作者就会生成多少条DTO记录,其中只有匹配传入作者的条目为true,其余为false,最终导致结果重复。
解决方案
方案1:添加DISTINCT去重
在JPQL中加入DISTINCT关键字,确保每个Article只返回唯一的DTO:
@Query("SELECT DISTINCT new com.example.learnjpql.dto.ArticleDTO(a.title, :author MEMBER OF a.authors) FROM Article a") List<ArticleDTO> getAllArticles(Author author);
方案2:使用子查询替代MEMBER OF
通过子查询判断作者是否属于文章的作者集合,避免交叉连接:
@Query("SELECT new com.example.learnjpql.dto.ArticleDTO(a.title, EXISTS (SELECT 1 FROM a.authors auth WHERE auth.id = :authorId)) FROM Article a") List<ArticleDTO> getAllArticles(@Param("authorId") Long authorId);
注:改用作者ID作为参数能简化查询逻辑;若必须传入
Author实体,可调整为auth = :author,你的代码中Author的equals/hashCode实现正确,不会有问题。
方案3:实体层添加@Formula(可选)
如果希望在实体层面直接获取该字段,可在Article实体中添加@Formula注解:
@Formula("(SELECT CASE WHEN EXISTS (SELECT 1 FROM article_authors2 aa WHERE aa.article_id = id AND aa.author_id = :authorId) THEN TRUE ELSE FALSE END)") private boolean isWrittenByMe;
该方式需在查询时手动设置参数,灵活性稍差,适合固定场景。
相关实体与DTO代码
Author实体
@Entity @NoArgsConstructor @AllArgsConstructor @Getter public class Author { @Id private Long id; private String firstname; private String lastname; @ManyToMany(fetch = FetchType.EAGER, mappedBy = "authors") private Set<Article> articles; @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof Author author)) return false; return getId().equals(author.getId()) && getFirstname().equals(author.getFirstname()) && getLastname().equals(author.getLastname()); } @Override public int hashCode() { return Objects.hash(getId(), getFirstname(), getLastname()); } public String toString() { return "Author(id=" + this.getId() + ", firstname=" + this.getFirstname() + ", lastname=" + this.getLastname(); } }
Article实体
@Entity @NoArgsConstructor @AllArgsConstructor @Getter @ToString public class Article { @Id private Long id; private String title; @ManyToMany(fetch = FetchType.EAGER) @JoinTable(name = "article_authors2", joinColumns = { @JoinColumn(name = "article_id") }, inverseJoinColumns = { @JoinColumn(name = "author_id") }) private Set<Author> authors; @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof Article article)) return false; return getId().equals(article.getId()) && getTitle().equals(article.getTitle()); } @Override public int hashCode() { return Objects.hash(getId(), getTitle()); } }
ArticleDTO
public record ArticleDTO( String title, boolean isAuthoredByMe ) { }
内容的提问来源于stack exchange,提问作者Houssem Nasri
相关产品推荐
相关产品推荐

