Hibernate:投影JPQL查询到DTO问题

时间:2018-02-07 21:42:49

标签: java hibernate jpa

首先,我将列出我在查询中使用的三个模型

ProductEntity:

@Entity
@Table(name = "product")
public class ProductEntity extends BaseEntity {
  //some fields

   @ManyToOne(fetch = FetchType.LAZY)
   @JoinColumn(name = "owner_id")
   private PartnerEntity owner;

   @OneToMany(
            mappedBy = "product",
            fetch = FetchType.LAZY
    )
    private List<StockProductInfoEntity> stocks;
 }

PartnerEntity:

@Entity
@Table(name = "partner")
public class PartnerEntity extends AbstractDetails {
    //some fields

    @OneToMany(
            mappedBy = "owner",
            fetch = FetchType.LAZY
    )
    private List<ProductEntity> products;
 }

和StockProductInfoEntity:

@Entity
@Table(name = "stock_product")
public class StockProductInfoEntity extends BaseEntity {
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "product_id")
    private ProductEntity product;

    //other fields
    @Column(name = "rest")
    private int rest;
}

我想从合作伙伴处获取数据库产品+计算所有股票的数量。 为方便起见,我创建了一个简单的DTO:

@Getter
@AllArgsConstructor
public class ProductCountDTO {
    private ProductEntity productEntity;
    private int count;

    //hack for hibernate
    public ProductCountDTO(ProductEntity productEntity, long count) {
        this.productEntity = productEntity;
        this.count = (int) count;
    }
}

并在JPA存储库中编写JPQL查询:

@Query("select new ru.oral.market.persistence.entity.product.util.ProductCountDTO(p, sum(stocks.rest))"+ 
            " from ProductEntity p" +
            " join fetch p.owner owner" +
            " join p.stocks stocks" +
            " where p.id = :id" +
            " group by p, owner")
    Optional<ProductCountDTO> findProductWithCount(@Param("id") long id);

但由于查询验证存在问题,我的应用程序甚至没有启动。我收到这条消息:

  

引起:org.hibernate.QueryException:查询指定的连接   获取,但获取的关联的所有者不存在   选择列表

很奇怪,但我尝试替换了连接提取 - &gt;加入。 我理解为什么我得到这个错误,hibernate对数据库进行了这样的查询:

select
            productent0_.id as col_0_0_,
            sum(stocks2_.rest) as col_1_0_ 
        from
            product productent0_ 
        inner join
            partner partnerent1_ 
                on productent0_.owner_id=partnerent1_.user_id 
        inner join
            stock_product stocks2_ 
                on productent0_.id=stocks2_.product_id 
        where
            productent0_.id=? 
        group by
            productent0_.id ,
            partnerent1_.user_id

但为什么他只拿产品ID而没有别的? 这个查询与Tuple一起工作,并从产品和合作伙伴

获取所有字段
  @Query("select p, sum(stocks.rest) from ProductEntity p" +
            " join fetch p.owner owner" +
            " join p.stocks stocks" +
            " where p.id = :id" +
            " group by p, owner")
    Optional<Tuple> findProductWithCount(@Param("id") long id);

这产生了我想要的原生查询:

select
            productent0_.id as col_0_0_,
            sum(stocks2_.rest) as col_1_0_,
            partnerent1_.user_id as user_id31_12_1_,
            productent0_.id as id1_14_0_,
            productent0_.brand_id as brand_i17_14_0_,
            productent0_.commission_volume as commissi2_14_0_,
            productent0_.created as created3_14_0_,
            productent0_.description as descript4_14_0_,
            productent0_.height as height5_14_0_,
            productent0_.length as length6_14_0_,
            productent0_.long_description as long_des7_14_0_,
            productent0_.name as name8_14_0_,
            productent0_.old_price as old_pric9_14_0_,
            productent0_.owner_id as owner_i18_14_0_,
            productent0_.pitctures as pitctur10_14_0_,
            productent0_.price as price11_14_0_,
            productent0_.status as status12_14_0_,
            productent0_.updated as updated13_14_0_,
            productent0_.vendor_code as vendor_14_14_0_,
            productent0_.weight as weight15_14_0_,
            productent0_.width as width16_14_0_,
            partnerent1_.about_company as about_co1_12_1_,
            partnerent1_.bik as bik2_12_1_,
            partnerent1_.bank_inn as bank_inn3_12_1_,
            partnerent1_.bank_kpp as bank_kpp4_12_1_,
            partnerent1_.bank as bank5_12_1_,
            partnerent1_.bank_address as bank_add6_12_1_,
            partnerent1_.checking_account as checking7_12_1_,
            partnerent1_.correspondent_account as correspo8_12_1_,
            partnerent1_.company_name as company_9_12_1_,
            partnerent1_.company_inn as company10_12_1_,
            partnerent1_.company_kpp as company11_12_1_,
            partnerent1_.ogrn as ogrn12_12_1_,
            partnerent1_.okato as okato13_12_1_,
            partnerent1_.actual_address as actual_14_12_1_,
            partnerent1_.director as directo15_12_1_,
            partnerent1_.full_name as full_na16_12_1_,
            partnerent1_.legal_address as legal_a17_12_1_,
            partnerent1_.short_name as short_n18_12_1_,
            partnerent1_.country as country19_12_1_,
            partnerent1_.discount_conditions as discoun20_12_1_,
            partnerent1_.discounts as discoun21_12_1_,
            partnerent1_.logo as logo22_12_1_,
            partnerent1_.min_amount_order as min_amo23_12_1_,
            partnerent1_.min_shipment as min_shi24_12_1_,
            partnerent1_.min_sum_order as min_sum25_12_1_,
            partnerent1_.own_delivery as own_del26_12_1_,
            partnerent1_.own_production as own_pro27_12_1_,
            partnerent1_.phones as phones28_12_1_,
            partnerent1_.return_information as return_29_12_1_,
            partnerent1_.site as site30_12_1_ 
        from
            product productent0_ 
        inner join
            partner partnerent1_ 
                on productent0_.owner_id=partnerent1_.user_id 
        inner join
            stock_product stocks2_ 
                on productent0_.id=stocks2_.product_id 
        where
            productent0_.id=? 
        group by
            productent0_.id ,
            partnerent1_.user_id

但这不太方便。 为什么DTO投影不能正常工作,但元组工作正常?

2 个答案:

答案 0 :(得分:2)

因为这就是Hibernate目前的实现方式。

因为您在DTO Projection中使用了一个实体,顾名思义,它应该用于DTO,而不是实体,Hibernate会假设您希望GROUP BY由标识符,因为它不应该是GROUP BY all实体属性。

Tuple已被破坏,它只能在MySQL中运行,但不能在Oracle或PostgreSQL中运行,因为您的聚合查询会选择GROUP BY子句中不存在的列。

然而,根据JPA规范,这不是要求工作。尽管如此,您仍然应该provide a replicating test case并打开一个问题,以便两种情况下的行为相同。

无论如何,一旦修复,它仍将是GROUP BY标识符。如果您还要选择实体和分组,则必须使用本机SQL查询和Hibernate ResultTransformerResultSet转换为对象图。

更多,获取实体和聚合是一种代码味道。最有可能的是,您需要一个只读视图的DTO投影。

在我的书High-Performance Java Persistence中,我解释说只有在你想修改它们时才能获取实体。否则,DTO投影也会更有效,更直接。

答案 1 :(得分:1)

由于弗拉德已经解释了原因,我将专注于另一种解决方案。必须在SELECT子句和GROUP BY子句中指定您真正感兴趣的所有属性才能完成很多工作。 如果您在Hibernate之上使用Blaze-Persistence Entity Views,则可能如下所示

@EntityView(ProductEntity.class)
public interface ProductCountDTO {
    // Or map the ProductEntity itself if you like..
    @Mapping("this")
    ProductView getProduct();
    @Mapping("sum(stocks.rest)")
    int getCount();
}

@EntityView(ProductEntity.class)
public interface ProductView {
    // Whatever mappings you like
}

使用Spring Data或DeltaSpike Data集成,你甚至可以像那样使用它

Optional<ProductCountDTO> findById(long id);

它将生成一个JPQL查询,如下所示

SELECT
  p /* All the attributes you map in ProductView  */,
  sum(stocks_1.rest)
FROM
  ProductEntity p
LEFT JOIN
  p.stocks stocks_1
GROUP BY
  p /* All the attributes you map in ProductView */

也许试一试? https://github.com/Blazebit/blaze-persistence#entity-view-usage

神奇的是,如果使用至少一个聚合函数,Blaze-Persistence在遇到聚合函数时会自动处理GROUP BY,方法是将您使用的每个非聚合表达式放入GROUP BY子句中。 当直接使用实体视图而不是实体时,您不会面临连接提取问题,因为实体视图只会将您实际映射的字段放入JPQL和SQL的结果SELECT子句中。 即使您直接使用实体或通过ProductCountDTO使用实体,在幕后使用的查询构建器也可以优雅地处理组中实体类型的选择,就像您希望它来自Hibernate一样。