简体   繁体   中英

Multiple subqueries in constructor from main query breaks Hibernate conversion

I have following simple class for mapping data extracted from query.

public class Statistics {
    private double maxPrice;
    private double minPrice;
    private double actualPrice;
    private double startPrice;
}

It has also a constructor.

    public Statistics(double maxPrice, double minPrice, double actualPrice, double startPrice) {
        this.maxPrice = maxPrice;
        this.minPrice = minPrice;
        this.actualPrice = actualPrice;
        this.startPrice = startPrice;
    }

The query looks ugly but should work.

@Query(value = "select new Statistics(max(price.value), min(price.value), " +
    "                 (select price.value as startPrice " +
    "                  from Price price " +
    "                  where price.date = (select min(date) from Price where price.item.id = :item_id )" +
    "                        and price.item.id = :item_id" +
    "                 ) as min_price," +
    "                 (select price.value as endPrice " +
    "                  from Price price " +
    "                  where price.date = (select max(date) from Price where price.item.id = :item_id )" +
    "                        and price.item.id = :item_id" +
    "                 ) as max_price " +
    "               ) " +
    " from Price price" +
    " where price.item.id = :item_id")

Hibernate generates following exception trace:

Caused by: java.lang.IllegalArgumentException: org.hibernate.QueryException: could not instantiate class [Statistics] from tuple
    at org.hibernate.internal.ExceptionConverterImpl.convert(ExceptionConverterImpl.java:138)
    at org.hibernate.query.internal.AbstractProducedQuery.list(AbstractProducedQuery.java:1514)
    at org.hibernate.query.Query.getResultList(Query.java:132)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at org.springframework.orm.jpa.SharedEntityManagerCreator$DeferredQueryInvocationHandler.invoke(SharedEntityManagerCreator.java:402)
    at com.sun.proxy.$Proxy222.getResultList(Unknown Source)
    at org.springframework.data.jpa.repository.query.JpaQueryExecution$CollectionExecution.doExecute(JpaQueryExecution.java:129)
    at org.springframework.data.jpa.repository.query.JpaQueryExecution.execute(JpaQueryExecution.java:91)
    at org.springframework.data.jpa.repository.query.AbstractJpaQuery.doExecute(AbstractJpaQuery.java:136)
    at org.springframework.data.jpa.repository.query.AbstractJpaQuery.execute(AbstractJpaQuery.java:125)
    at org.springframework.data.repository.core.support.RepositoryFactorySupport$QueryExecutorMethodInterceptor.doInvoke(RepositoryFactorySupport.java:605)
    at org.springframework.data.repository.core.support.RepositoryFactorySupport$QueryExecutorMethodInterceptor.lambda$invoke$3(RepositoryFactorySupport.java:595)
    at org.springframework.data.repository.core.support.RepositoryFactorySupport$QueryExecutorMethodInterceptor.invoke(RepositoryFactorySupport.java:595)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
    at org.springframework.data.projection.DefaultMethodInvokingMethodInterceptor.invoke(DefaultMethodInvokingMethodInterceptor.java:59)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
    at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:295)
    at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:98)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
    at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:139)
    ... 157 common frames omitted
Caused by: org.hibernate.QueryException: could not instantiate class [Statistics] from tuple
    at org.hibernate.transform.AliasToBeanConstructorResultTransformer.transformTuple(AliasToBeanConstructorResultTransformer.java:41)
    at org.hibernate.hql.internal.HolderInstantiator.instantiate(HolderInstantiator.java:85)
    at org.hibernate.loader.hql.QueryLoader.getResultList(QueryLoader.java:472)
    at org.hibernate.loader.Loader.listIgnoreQueryCache(Loader.java:2506)
    at org.hibernate.loader.Loader.list(Loader.java:2501)
    at org.hibernate.loader.hql.QueryLoader.list(QueryLoader.java:504)
    at org.hibernate.hql.internal.ast.QueryTranslatorImpl.list(QueryTranslatorImpl.java:395)
    at org.hibernate.engine.query.spi.HQLQueryPlan.performList(HQLQueryPlan.java:220)
    at org.hibernate.internal.SessionImpl.list(SessionImpl.java:1508)
    at org.hibernate.query.internal.AbstractProducedQuery.doList(AbstractProducedQuery.java:1537)
    at org.hibernate.query.internal.AbstractProducedQuery.list(AbstractProducedQuery.java:1505)
    ... 178 common frames omitted

Alongside it doesn't work, i remarked one intriguing fact. If i change the query to return same value as value for first date and latest date(i change in both subqueries to call min function), IT WORKS.

    @Query(value = "select new Statistics(max(price.value), min(price.value), " +
        "                 (select price.value as startPrice " +
        "                  from Price price " +
        "                  where price.date = (select min(date) from Price where price.item.id = :item_id )" +
        "                        and price.item.id = :item_id" +
        "                 ) as min_price," +
        "                 (select price.value as endPrice " +
        "                  from Price price " +
        "                  where price.date = (select min(date) from Price where price.item.id = :item_id )" +
        "                        and price.item.id = :item_id" +
        "                 ) as max_price " +
        "               ) " +
        " from Price price" +
        " where price.item.id = :item_id")

Need to mention that for testing in DB i have a single entry which will return same result for both subqueries.

Try to correct your query in the following way:

@Query(value = "select new Statistics(max(price.value), min(price.value), " +
    "     (select distinct price.value as startPrice " +
    "      from Price price " +
    "      where price.date = (select min(date) from Price where price.item.id = :item_id )" +
    "      and price.item.id = :item_id" +
    "      ) as min_price," +
    "      (select distinct price.value as endPrice " +
    "       from Price price " +
    "       where price.date = (select max(date) from Price where price.item.id = :item_id )" +
    "       and price.item.id = :item_id" +
    "      ) as max_price " +
    " ) " +
    " from Price price" +
    " where price.item.id = :item_id")

It looks like you gets this error due to the duplicate rows returned by subquery.

I don't think it's possible to use an alias for a constructor argument. Try this instead:

@Query(value = "select new Statistics(max(price.value), min(price.value), " +
"     (select distinct price.value as startPrice " +
"      from Price price " +
"      where price.date = (select min(date) from Price where price.item.id = :item_id )" +
"      and price.item.id = :item_id" +
"      )," +
"      (select distinct price.value as endPrice " +
"       from Price price " +
"       where price.date = (select max(date) from Price where price.item.id = :item_id )" +
"       and price.item.id = :item_id" +
"      ) " +
" ) " +
" from Price price" +
" where price.item.id = :item_id")

Apart from that, I think this is a perfect use case forBlaze-Persistence Entity Views .

I created the library to allow easy mapping between JPA models and custom interface or abstract class defined models, something like Spring Data Projections on steroids. The idea is that you define your target structure(domain model) the way you like and map attributes(getters) via JPQL expressions to the entity model.

A DTO model for your use case could look like the following with Blaze-Persistence Entity-Views:

@EntityView(Price.class)
public interface Statistics {
    @Mapping("MAX(value)")
    double getMaxPrice();
    @Mapping("MIN(value)")
    double getMinPrice();
    @Limit(limit = "1", order = "date asc")
    @MappingCorrelatedSimple(
      correlated = Price.class,
      correlationBasis = "this",
      correlationExpression = "item.id = EMBEDDING_VIEW(item.id)",
      correlationResult = "value"
    )
    double getStartPrice();
    @Limit(limit = "1", order = "date desc")
    @MappingCorrelatedSimple(
      correlated = Price.class,
      correlationBasis = "this",
      correlationExpression = "item.id = EMBEDDING_VIEW(item.id)",
      correlationResult = "value"
    )
    double getLastPrice();
}

Querying is a matter of applying the entity view to a query, the simplest being just a query by id.

Statistics a = entityViewManager.find(entityManager, Statistics.class, id);

The Spring Data integration allows you to use it almost like Spring Data Projections: https://persistence.blazebit.com/documentation/entity-view/manual/en_US/index.html#spring-data-features

Statistics findByItemId(int itemId);

This will make use of lateral joins if the DBMS permits it which is usually a lot more efficient than doing two nested subqueries. The resulting SQL query looks roughly like this:

select max(p.value), min(p.value), p1.value, p2.value
from price p
left join lateral (select * from price p0 where p0.item_id = p.item_id order by p0.date asc limit 1) p1 on 1=1
left join lateral (select * from price p0 where p0.item_id = p.item_id order by p0.date desc limit 1) p2 on 1=1
where p.item_id = ?
group by p1.value, p2.value

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

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