如何在不使用@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
相关产品推荐
相关产品推荐

