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

使用JOIN FETCH查询DTO投影时遭遇NonUniqueResultException求助

Spring Data JPA JOIN FETCH关联集合时DTO查询报NonUniqueResultException问题

实体定义

Order 实体

public class Order {

    @Id
    @Column(name = "id")
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @OneToOne(mappedBy = "order",
            cascade = CascadeType.ALL)
    private Cart cart;
}

Cart 实体

public class Cart {

    @Id
    private Long id;

    @OneToOne(fetch = FetchType.LAZY)
    @MapsId
    private Order order;

    @OneToMany(mappedBy = "cart",
            cascade = CascadeType.ALL,
            orphanRemoval = true)
    private List<OrderItem> orderItems;
}

OrderItem 实体

public class OrderItem {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "cart_id")
    private Cart cart;
}

问题场景

执行以下DTO查询时抛出NonUniqueResultException:

return em.createQuery("""
            select new OrderDTO(
               order.id,
               order.cart
            )
            from Order order join fetch order.cart.orderItems
            where order.id = :orderId
            """, OrderDTO.class).setParameter("orderId", orderId).getSingleResult();

异常信息

jakarta.persistence.NonUniqueResultException: query did not return a unique result: 2

数据库数据

  • Order表:
+----+
| id |
+----+
|  1 |
+----+
  • Cart表(因使用@MapsId,id与order_id字段值一致):
+----------+
| order_id |
+----------+
|        1 |
+----------+
  • OrderItem表:
+----+---------+
| id | cart_id |
+----+---------+
|  1 |       1 |
|  2 |       1 |
+----+---------+

需求说明

需通过JOIN FETCH加载所有订单项以避免N+1查询问题,但当前查询因关联集合产生多条结果,调用getSingleResult()报错。直接返回Order实体时查询正常,但改用DTO会触发语义异常;移除fetch指令则仍存在重复结果问题。

解决方案

方案1:添加DISTINCT关键字去重

在JPQL中加入DISTINCT,Hibernate会自动识别并合并重复的DTO实例,同时保留已加载的集合数据:

return em.createQuery("""
            select distinct new OrderDTO(
               order.id,
               order.cart
            )
            from Order order join fetch order.cart.orderItems
            where order.id = :orderId
            """, OrderDTO.class).setParameter("orderId", orderId).getSingleResult();

方案2:先查询实体再转换DTO

先通过JOIN FETCH获取Order实体(Hibernate会自动合并SQL返回的多条结果为单个实体),再手动转换为DTO:

Order order = em.createQuery("""
            select distinct o
            from Order o join fetch o.cart c join fetch c.orderItems
            where o.id = :orderId
            """, Order.class).setParameter("orderId", orderId).getSingleResult();
return new OrderDTO(order.getId(), order.getCart());

方案3:修改DTO构造函数直接传入集合

调整OrderDTO的构造函数,直接接收orderItems集合,避免通过Cart间接获取:

// 假设OrderDTO新增构造函数:OrderDTO(Long orderId, Cart cart, List<OrderItem> orderItems)
return em.createQuery("""
            select distinct new OrderDTO(
               order.id,
               order.cart,
               order.cart.orderItems
            )
            from Order order join fetch order.cart.orderItems
            where order.id = :orderId
            """, OrderDTO.class).setParameter("orderId", orderId).getSingleResult();

原理说明

JOIN FETCH关联OneToMany集合时,SQL层面会返回与集合元素数量一致的结果行,但Hibernate查询实体时会自动合并为单个实体对象。而查询DTO时,Hibernate不会自动合并重复的DTO实例,因此需要通过DISTINCT触发合并逻辑,或者先获取实体再做转换。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.07 22:10:04