简体   繁体   English

Hibernate:将 JPQL 查询投影到 DTO 问题

[英]Hibernate: projection JPQL query to DTO issue

To start with, I'll list three models that I work with in a query首先,我将列出我在查询中使用的三个模型

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;
 }

and StockProductInfoEntity:和 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;
}

And i want to fetch from database product with partner + calculate count in all stocks.我想与合作伙伴一起从数据库产品中获取 + 计算所有股票的数量。 For convenience, I created a simple DTO:为方便起见,我创建了一个简单的 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;
    }
}

and write JPQL query in JPA repository:并在 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);

But my application did not even start because of a problem with the query validation.但是由于查询验证问题,我的应用程序甚至没有启动。 I get this message:我收到这条消息:

Caused by: org.hibernate.QueryException: query specified join fetching, but the owner of the fetched association was not present in the select list引起:org.hibernate.QueryException: 查询指定的连接获取,但选择列表中不存在获取的关联的所有者

Very strange, but I tried to replace join fetch -> join.很奇怪,但我尝试替换 join fetch -> join。 And I understood why I got this error, hibernate made such a query to the database:我明白为什么我会收到这个错误,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

But why does he only take the product id and nothing else?但是为什么他只需要产品 ID 而没有其他东西呢? This query with Tuple work and get all fields from product and partner此查询与元组工作并从产品和合作伙伴获取所有字段

  @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);

And this produced native query what i want:这产生了我想要的本机查询:

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

But it's not very convenient.但是不是很方便。 Why DTO projection doesn't work correctrly, but tuple works fine?为什么 DTO 投影不能正常工作,但元组工作正常?

Because that's how Hibernate is currently implemented.因为这就是 Hibernate 当前的实现方式。

Because you used an entity in the DTO Projection, which as the name implies, it should be used for DTOs, not entities, Hibernate is going to assume that you want to GROUP BY by the identifier because it should not GROUP BY all entity properties.因为您在 DTO 投影中使用了一个实体,顾名思义,它应该用于 DTO,而不是实体,Hibernate 将假设您希望按标识符进行 GROUP BY,因为它不应该对所有实体属性进行 GROUP BY。

The Tuple is broken and it will only work in MySQL, but not in Oracle or PostgreSQL since your aggregate query selects columns that are not present in the GROUP BY clause. Tuple坏了,它只能在 MySQL 中工作,但不能在 Oracle 或 PostgreSQL 中工作,因为您的聚合查询选择了 GROUP BY 子句中不存在的列。

However, this is not demanded to work according to the JPA specs.但是,根据 JPA 规范并不要求这样做。 Nevertheless, you should still provide a replicating test case and open an issue so that the behavior is the same for both situations.尽管如此,您仍然应该提供一个复制测试用例并打开一个问题,以便在这两种情况下行为是相同的。

Anyway, once fixed, it will still GROUP BY identifier.无论如何,一旦修复,它仍然是 GROUP BY 标识符。 If you want to select entities and group by as well, you will have to use a native SQL query along with the Hibernate ResultTransformer to transform the ResultSet into a graph of objects.如果您还想选择实体和分组依据,则必须使用本机 SQL 查询和Hibernate ResultTransformerResultSet转换为对象图。

More, fetching entities and aggregations is a code smell.此外,获取实体和聚合是一种代码味道。 Most likely, you need a DTO projection or a read-only view.很可能,您需要 DTO 投影或只读视图。

Entities should only be fetched when you want to modify them.只有当您想修改它们时才应该获取实体。 Otherwise, a DTO projection is more efficient and more straightforward as well.否则,DTO 投影也更高效、更直接。

Since Vlad already explained the why, I will focus on an alternative solution.由于 Vlad 已经解释了原因,我将专注于替代解决方案。 Having to specify all attributes that you are really interested in in the SELECT clause and the GROUP BY clause is a lot of work.必须在 SELECT 子句和 GROUP BY 子句中指定您真正感兴趣的所有属性是一项大量工作。 If you used Blaze-Persistence Entity Views on top of Hibernate, this could look like the following如果您在 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
}

With the Spring Data or DeltaSpike Data integration you can even use it like that通过 Spring Data 或 DeltaSpike Data 集成,您甚至可以像这样使用它

Optional<ProductCountDTO> findById(long id);

It will produce a JPQL query like the following它将产生一个 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 */

Maybe give it a shot?也许试一试? https://github.com/Blazebit/blaze-persistence#entity-view-usage https://github.com/Blazebit/blaze-persistence#entity-view-usage

The magic is that Blaze-Persistence handles the GROUP BY automatically when encountering an aggregate function by putting every non-aggregate expression you use into the GROUP BY clause if there is at least one aggregate function used.神奇的是,如果至少使用了一个聚合函数,Blaze-Persistence 会在遇到聚合函数时自动处理 GROUP BY,将您使用的每个非聚合表达式放入 GROUP BY 子句中。 When using Entity Views instead of entities directly, you won't be facing the join fetch problems as Entity Views will only put the fields you actually map into the resulting SELECT clause of the JPQL and SQL.当使用实体视图而不是直接使用实体时,您将不会面临连接提取问题,因为实体视图只会将您实际映射的字段放入 JPQL 和 SQL 的结果 SELECT 子句中。 Even if you used entities directly or via the ProductCountDTO, the query builder used behind the scenes handles selects of entity types in case of a group by gracefully, just as you'd expect it from Hibernate.即使您直接或通过 ProductCountDTO 使用实体,后台使用的查询构建器也会优雅地处理实体类型的选择,就像您对 Hibernate 所期望的那样。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM