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

Spring Boot CrudRepository自定义find方法查询非唯一ID列结果重复问题

问题原因

这不是Spring Boot的bug,是JPA实现Hibernate的内置机制导致的:

  • JPA规范强制要求所有实体必须具备全局唯一的主键标识,Hibernate处理任意返回实体类的查询结果时,都会优先使用主键值作为key,去当前持久化上下文的一级缓存中匹配已有实体:如果缓存中已经存在对应主键的实体,就直接返回该缓存对象,不会用新返回的数据库行数据覆盖已有实体的字段。
  • 你将不唯一的c字段标记为@Id,查询c='a'的结果时,第一行主键值为a,缓存中不存在对应实体,Hibernate就创建对应实体放入缓存;后续两行主键值同样是a,Hibernate直接复用缓存中的第一个实体对象,因此最终返回三条完全相同的记录。
  • 查询c2字段正常,是因为c2不是主键,不会作为缓存匹配的key,每一行数据库结果都会生成新的实体对象,因此不会出现重复。
无需新增主键列的解决方案

方案1:使用投影接收查询结果(最推荐)

投影不会走实体的缓存映射逻辑,直接映射数据库返回的原始结果,完全规避该问题。
首先定义投影接口:

public interface TProjection {
    String getC();
    String getC2();
}

修改仓库方法返回值为投影类型:

public interface tr extends PagingAndSortingRepository<t, String> {
    List<TProjection> findAllByc(String c);
}

如果业务逻辑需要实体对象,手动把投影属性复制到实体实例即可。

方案2:直接返回原始查询结果自定义映射

通过Tuple类型接收原始查询结果,自己手动映射为实体,完全绕过Hibernate的实体实例化逻辑:

public interface tr extends PagingAndSortingRepository<t, String> {
    @Query("select t.c as c, t.c2 as c2 from t t where t.c = :c")
    List<Tuple> findAllBycRaw(@Param("c") String c);
}

调用时遍历Tuple列表,手动构造t实体对象即可。

方案3:使用复合主键+自定义持久化逻辑

如果一定要使用实体接收所有查询结果,可以使用复合主键,同时自行区分插入和更新逻辑,避免你提到的save方法不符合预期的问题:

  1. 首先定义复合主键类:
@Embeddable
public class TId implements Serializable {
    private String c;
    private String c2;
    // 省略构造方法、equals、hashCode
}
  1. 实体类使用复合主键:
@Entity
@Table(schema = "s")
public class t implements Serializable {
    @EmbeddedId
    private TId id;
    // 省略其他字段、getter、setter
}
  1. 自定义save逻辑:注入EntityManager,当保存的实体c值为b时调用merge方法执行更新,c值为a时调用persist方法执行插入,完全自主控制持久化行为。

内容的提问来源于stack exchange,提问作者KRISTIJAN TOMASINI

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.30 12:15:06