You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何让Spring Data投影接收关联关系的集合作为参数

问题描述

实体定义

public class RecipeDAO extends AbstractDAO {
    private Boolean visible;

    @ManyToMany(mappedBy = "recipes", cascade = CascadeType.ALL)
    private Set<TopicDAO> topics;

    @MapKey(name = "id.locale")
    @OneToMany(mappedBy = "recipe", cascade = CascadeType.ALL, orphanRemoval = true)
    private Map<String, Localization> localizations;

    @MapKey(name = "id.locale")
    @OneToMany(mappedBy = "recipe", cascade = CascadeType.ALL, orphanRemoval = true)
    private Map<String, JsonData> json;
}

查询代码

你在Repository中定义的JPQL查询如下:

@Query("select new com.fullstack.dtos.RecipeDTO(t.id, t.createdBy, t.lastModifiedBy, t.created, t.lastModified, t.version, t.visible, l.title, l.description, l.footnote, topics) from recipes t inner join t.localizations l inner join t.topics topics where l.id.locale = :lang and l.title like %:title%")
Page<RecipeDTO> findAllByTitle(String lang, String title, Pageable pageable);

问题表现

这是多语言系统,用Map存储多语言对应的localizations和json字段,不关联topics时查询正常,关联后出现两个核心问题:

  1. 构造投影时如果用Collection/List/Set类型接收topics参数,会报类型不匹配,JPQL期望传入单个TopicDAO类型
  2. 用单个TopicDAO接收时,数据库中仅1条Recipe数据,会返回和关联Topic数量一致的重复Recipe实例,所有实例@Id完全相同,且Hibernate会对每个Topic单独执行查询,出现N+1问题

你输出的日志如下:

TopicDAO(id=1771b663-e5d2-4a09-a758-9dec919cb3c6, topic=savory)
TopicDAO(id=1de01b0d-b01c-42da-a5c5-78b2048b8f1a, topic=cheap)
TopicDAO(id=342fdc0d-a091-4669-9753-69c6b9293562, topic=not vegan)
TopicDAO(id=46ad594e-4aaf-4438-90fc-7401fca4fda4, topic=nice)
TopicDAO(id=67477796-4a6d-4b48-9c69-c9692d341d7a, topic=easy)
TopicDAO(id=be7a016d-7d6f-4cd1-a1ab-4faadf484c30, topic=nutritious)
TopicDAO(id=c29347bd-e755-4414-9a63-28f70a2498cb, topic=meat)
TopicDAO(id=d28cbaba-cd24-4256-8767-6cf9f75e5e1b, topic=lunch)
TopicDAO(id=f845f510-15ad-471e-8cfb-6b46d0ab1ec6, topic=dinner)
TopicDAO(id=fa97c9be-346b-435f-a8a1-4400c13f26cd, topic=breakfast)

Page 1 of 1 containing com.fullstack.dtos.RecipeDTO instances
[RecipeDTO(visible=true, topics=["easy"]), RecipeDTO(visible=true, topics=["breakfast"]), RecipeDTO(visible=true, topics=["nice"]), RecipeDTO(visible=true, topics=["lunch"]), RecipeDTO(visible=true, topics=["not vegan"]), RecipeDTO(visible=true, topics=["dinner"]), RecipeDTO(visible=true, topics=["cheap"]), RecipeDTO(visible=true, topics=["meat"]), RecipeDTO(visible=true, topics=["savory"]), RecipeDTO(visible=true, topics=["nutritious"])]

补充说明

  • AbstractDAO是所有DAO类的公共父类,AbstractDTO是所有DTO的公共父类,二者都存放@Id、@Created、@CreatedBy、@Version这类全局公共字段
  • 尝试过基于接口的投影,结果和类投影完全一致,仍返回重复的Recipe对象

解决方案

问题的核心原因是:普通inner join关联集合属性时,JPQL会展开集合生成笛卡尔积,每条关联的Topic对应一行结果,因此会返回重复的Recipe条目;同时标准JPQL的构造投影默认不支持直接传入集合类型参数,所以会报类型不匹配。

你可以选择以下任意一种方案解决:

方案1:使用Hibernate聚合函数直接返回集合(推荐,Hibernate 5.2+支持)

用Hibernate内置的collect()聚合函数把Topic收集为集合,配合GROUP BY合并相同Recipe的结果,修改后的查询如下:

@Query("select new com.fullstack.dtos.RecipeDTO(t.id, t.createdBy, t.lastModifiedBy, t.created, t.lastModified, t.version, t.visible, l.title, l.description, l.footnote, collect(topics)) " +
        "from RecipeDAO t " +
        "inner join t.localizations l " +
        "inner join t.topics topics " +
        "where l.id.locale = :lang and l.title like %:title% " +
        "group by t.id, t.createdBy, t.lastModifiedBy, t.created, t.lastModified, t.version, t.visible, l.title, l.description, l.footnote")
Page<RecipeDTO> findAllByTitle(String lang, String title, Pageable pageable);

同时把RecipeDTO中topics的参数类型改为Set<TopicDAO>即可,不需要额外处理,直接返回合并后的集合。

方案2:使用fetch join + 实体转DTO

如果不想依赖Hibernate专属函数,可以先查询带关联数据的RecipeDAO实体,再手动转换为DTO:

@Query("select distinct t from RecipeDAO t " +
        "inner join fetch t.localizations l " +
        "inner join fetch t.topics " +
        "where l.id.locale = :lang and l.title like %:title%")
Page<RecipeDAO> findAllByTitle(String lang, String title, Pageable pageable);

distinct会让Hibernate自动合并相同id的实体,避免重复,fetch关键字会一次性加载所有关联的Topic和Localization,解决N+1查询问题,拿到实体后手动映射为DTO即可。

方案3:结果后处理合并(兼容性最强)

如果不想修改原有查询逻辑,拿到查询返回的重复DTO列表后,按Recipe的id分组,把相同id的DTO的Topic合并为一个集合,再重新组装Page对象即可,适合需要兼容不同JPA实现的场景。


内容的提问来源于stack exchange,提问作者Goncalo Condeco

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.05 00:15:02