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

如何在不使用@OneToMany注解的情况下,通过单查询分页获取用户及其所有爱好并映射到指定DTO?

如何在不使用@OneToMany注解的情况下,通过单查询分页获取用户及其所有爱好并映射到指定DTO?

我来帮你解决这个问题!你遇到的报错核心原因是:JPA的构造函数查询不支持直接将子查询返回的集合(比如多个爱好名称)作为参数传入DTO的构造函数——构造函数的参数必须是单个标量值(比如字符串、数字),而不能是List这类集合类型。当你把参数改成单个String时,子查询只能返回一条结果(如果用户有多个爱好,其实会触发NonUniqueResultException,只是你的测试数据可能刚好每个用户只有一个爱好,所以能运行),但这显然不是你想要的效果。

下面给你两种可靠的解决方案,你可以根据自己的场景选择:


方案一:分两步查询(推荐,通用不依赖数据库)

这种方法先分页获取用户基础信息,再批量查询所有用户的爱好,最后把爱好映射到对应的DTO上,既避免了N+1查询,又不依赖数据库特定功能。

步骤1:修改UserRepository,先查询用户基础信息

我们先只查用户的id、姓名,给爱好留空,后续再填充:

@Repository
public interface UserRepository extends JpaRepository<UserEntity, String> {
    @Query("""
           SELECT new com.package.to.SearchUserModel(
               user.id,
               user.contactInformation.name,
               user.contactInformation.surname,
               new java.util.ArrayList<>()
           )
           FROM UserEntity user
           WHERE user.id != :userId
           """)
    Page<SearchUserModel> getUsersBaseInfo(String userId, Pageable pageable);
}

步骤2:在UserHobbyRepository添加批量查询方法

通过用户id集合批量查询所有爱好,减少查询次数:

@Repository
public interface UserHobbyRepository extends JpaRepository<UserHobbyEntity, Long> {
    @Query("SELECT uh.userEntity.id, uh.hobbyName FROM UserHobbyEntity uh WHERE uh.userEntity.id IN :userIds")
    List<Object[]> findHobbiesByUserIds(List<String> userIds);
}

步骤3:在Service层组装数据

把两步查询的结果结合起来,给每个用户填充对应的爱好列表:

@Service
public class UserService {
    private final UserRepository userRepository;
    private final UserHobbyRepository userHobbyRepository;

    // 用构造注入替代@Autowired,更符合Spring规范
    public UserService(UserRepository userRepository, UserHobbyRepository userHobbyRepository) {
        this.userRepository = userRepository;
        this.userHobbyRepository = userHobbyRepository;
    }

    public Page<SearchUserModel> getUsersForUser(String userId, Pageable pageable) {
        // 1. 分页获取用户基础信息
        Page<SearchUserModel> userPage = userRepository.getUsersBaseInfo(userId, pageable);
        
        // 2. 提取当前页所有用户的id
        List<String> userIds = userPage.getContent().stream()
                .map(SearchUserModel::getUserId)
                .collect(Collectors.toList());
        
        if (userIds.isEmpty()) {
            return userPage; // 没有用户直接返回
        }
        
        // 3. 批量查询所有用户的爱好
        List<Object[]> hobbyResults = userHobbyRepository.findHobbiesByUserIds(userIds);
        
        // 4. 把爱好按用户id分组,方便快速查找
        Map<String, List<String>> userHobbyMap = hobbyResults.stream()
                .collect(Collectors.groupingBy(
                        result -> (String) result[0],
                        Collectors.mapping(result -> (String) result[1], Collectors.toList())
                ));
        
        // 5. 给每个用户填充爱好列表
        userPage.getContent().forEach(user -> {
            user.setHobbies(userHobbyMap.getOrDefault(user.getUserId(), new ArrayList<>()));
        });
        
        return userPage;
    }
}

这种方法的优势是:

  • 通用性强,不管你用MySQL、PostgreSQL还是Oracle都能跑
  • 只做两次查询,避免了N+1问题,性能有保障
  • 逻辑清晰,容易维护

方案二:使用数据库特定聚合函数(适合简单场景)

如果你的项目固定使用某一种数据库,可以用数据库的字符串聚合函数,把多个爱好拼接成一个字符串,再在DTO里拆分成列表。

步骤1:修改SearchUserModel,支持字符串转列表

添加一个接受拼接字符串的构造函数,在内部完成拆分:

public class SearchUserModel {
    private String userId;
    private String name;
    private String surname;
    private List<String> hobbies;

    // 用于JPA查询的构造函数:接受拼接后的爱好字符串
    public SearchUserModel(String userId, String name, String surname, String hobbiesStr) {
        this.userId = userId;
        this.name = name;
        this.surname = surname;
        // 拆分字符串为列表,注意分隔符要和查询中的一致
        this.hobbies = hobbiesStr != null ? Arrays.asList(hobbiesStr.split(",")) : new ArrayList<>();
    }

    // 保留原构造函数,方便其他场景使用
    public SearchUserModel(String userId, String name, String surname, List<String> hobbies) {
        this.userId = userId;
        this.name = name;
        this.surname = surname;
        this.hobbies = hobbies;
    }

    // 省略getter、setter
}

步骤2:修改UserRepository的查询(以MySQL为例)

用GROUP_CONCAT聚合爱好名称,注意要分组查询:

@Repository
public interface UserRepository extends JpaRepository<UserEntity, String> {
    @Query("""
           SELECT new com.package.to.SearchUserModel(
               user.id,
               user.contactInformation.name,
               user.contactInformation.surname,
               (SELECT GROUP_CONCAT(uh.hobbyName SEPARATOR ',') FROM UserHobbyEntity uh WHERE uh.userEntity.id = user.id)
           )
           FROM UserEntity user
           WHERE user.id != :userId
           GROUP BY user.id, user.contactInformation.name, user.contactInformation.surname
           """)
    Page<SearchUserModel> getUsersForUser(String userId, Pageable pageable);
}

不同数据库的聚合函数替换:

  • PostgreSQL:把GROUP_CONCAT(uh.hobbyName SEPARATOR ',')换成STRING_AGG(uh.hobbyName, ',')
  • Oracle:换成LISTAGG(uh.hobbyName, ',') WITHIN GROUP (ORDER BY uh.hobbyName)

这种方法的局限性是:

  • 依赖数据库特定语法,换数据库需要修改查询
  • 如果爱好名称本身包含分隔符(比如逗号),会导致拆分错误,需要选择特殊分隔符

备注:内容来源于stack exchange,提问作者monstereo

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.21 13:24:31