Spring JPA一对多关联查询如何聚合列表结果避免数据重复
你对问题的判断完全准确:JPA默认执行一对多关联的findAll()查询时,会生成左连接语句,返回的结果集行数等于主表和子表的匹配行数。你的案例里1个用户对应2条社交账号记录,结果集就会返回2行内容完全相同的User实体,即使你用Set接收关联集合,也不会在SQL结果映射阶段自动完成主表实体去重,最终就会出现重复的JSON返回。
下面三个方案按推荐优先级从高到低排列:
方案1:自定义查询语句加去重与关联加载(改动最小,见效最快)
不要直接用默认的findAll()方法,在UserRepository里自定义查询方法,通过DISTINCT关键字完成主表实体去重,同时用JOIN FETCH避免N+1查询问题:public interface UserRepository extends JpaRepository<User, UUID> { @Query("SELECT DISTINCT u FROM User u LEFT JOIN FETCH u.userSocial") List<User> findAllWithSocialAccounts(); }业务层调用这个自定义方法替代原来的
findAll()即可,不需要改动现有实体结构。如果你不想写JPQL,也可以搭配@EntityGraph指定要加载的关联属性:@EntityGraph(attributePaths = "userSocial") @Query("SELECT DISTINCT u FROM User u") List<User> findAllWithSocialAccounts();方案2:修正实体映射逻辑(长期维护推荐)
你当前的实体映射存在不符合JPA规范的问题:@ManyToOne关联不能直接定义在@Embeddable复合主键类中,这类写法会导致JPA对实体的唯一性判断、懒加载代理生成出现异常,也是去重失效的潜在诱因。
正确的映射方式是把外键关联从内嵌主键类移到实体本身,内嵌主键类只存基础字段,同时为主键类实现equals()和hashCode()方法保证唯一性判断准确:
修正后的UserSocial.java代码:@Entity @Table(name = "user_social") public class UserSocial { @EmbeddedId private UserSocialId id; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "user_id", insertable = false, updatable = false) private User userObj; @Embeddable public static class UserSocialId implements Serializable { @Column(name = "user_id") private UUID userId; private String social_link; private String social_channel; @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; UserSocialId that = (UserSocialId) o; return Objects.equals(userId, that.userId) && Objects.equals(social_link, that.social_link) && Objects.equals(social_channel, that.social_channel); } @Override public int hashCode() { return Objects.hash(userId, social_link, social_channel); } } }同步修改
User.java中的@OneToMany注解的mappedBy属性,指向实体类的关联字段:@OneToMany(mappedBy = "userObj") Set<UserSocial> userSocial = new HashSet<>();修正映射后再搭配方案1的自定义查询,就能彻底解决重复问题,也能避免后续出现其他映射相关的bug。
方案3:序列化/业务层兜底去重(不推荐,仅临时应急用)
如果暂时不能修改查询和实体映射,可以在结果返回前做手动去重:比如在业务层把查询结果按用户id做去重,或者在User实体上加@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id")注解,让Jackson序列化时自动跳过id相同的重复对象。但这类方法没有解决SQL层查询冗余数据的问题,会白白浪费数据库性能,只适合临时救急。
内容的提问来源于stack exchange,提问作者Akhilesh Goutam

