Blazebit / blaze-persistence

Rich Criteria API for JPA providers
https://persistence.blazebit.com
Apache License 2.0
742 stars 90 forks source link

Using the entity view with Querydsl throws java.lang.ClassCastException #1943

Closed guofengzh closed 3 weeks ago

guofengzh commented 1 month ago

Description

I tried to use the entity view to project a Querydsl query as described in Querydsl integration. This is the code:

@ExtendWith(SpringExtension.class)
@SpringBootTest
@Transactional
class CatQuerydslTest {
    private final static Logger log = LoggerFactory.getLogger(CatQuerydslTest.class);

    @Autowired
    private CriteriaBuilderFactory cbf;
    @PersistenceContext
    private EntityManager em;
    @Autowired
    private EntityViewManager evm;

    @BeforeEach
    public void init() {
        Cat p1 = new Cat();
        p1.setName("Tom");
        em.persist(p1);
    }

    @Test
    public void entityViewTest() {
        QCat cat = QCat.cat;
        BlazeJPAQuery<Cat> query = new BlazeJPAQuery<>(em, JPQLNextTemplates.DEFAULT, cbf)
                .from(cat)
                .select(cat);

        BlazeCriteriaBuilderRenderer<Cat> bbr = new BlazeCriteriaBuilderRenderer<>(cbf, em, JPQLNextTemplates.DEFAULT);
        Queryable<Cat, ?> catQueryable = bbr.render(query);
        CriteriaBuilder<Cat> cb = (CriteriaBuilder<Cat>) catQueryable;
        //CriteriaBuilder<Cat> cb = cbf.create(em, Cat.class);  // This is ok
        CriteriaBuilder<CatSimpleView> catSimpleViewCriteriaBuilder = evm.applySetting(EntityViewSetting.create(CatSimpleView.class), cb);

        List<CatSimpleView> catViews = catSimpleViewCriteriaBuilder.getResultList();
        catViews.forEach(p -> log.info(p.getId() + " " + p.getName()));
    }
}

Expected behavior

The SQL generated by Hibernate should be

select c1_0.id,c1_0.name from cat c1_0

and the query result shoud be something like

1 tom

Actual behavior

The SQl generated is

select c1_0.id,c1_0.age,c1_0.father_id,c1_0.mother_id,c1_0.name,c1_0.owner_id from cat c1_0

please note that it selects all the columns of the Cat entity, and not make the projection.

then throws the exception:

java.lang.RuntimeException: Could not invoke the proxy constructor 'public com.demo.blazebitquerydsl.view.CatSimpleViewImpl(com.demo.blazebitquerydsl.view.CatSimpleViewImpl,int,java.lang.Object[])' with the given tuple: [com.demo.blazebitquerydsl.model.Cat@7197b96c, 1, Tom] with the types: [com.demo.blazebitquerydsl.model.Cat, java.lang.Long, java.lang.String]

    at com.blazebit.persistence.view.impl.proxy.TupleConstructorReflectionInstantiator.newInstance(TupleConstructorReflectionInstantiator.java:92)
    at com.blazebit.persistence.view.impl.objectbuilder.ViewTypeObjectBuilder.build(ViewTypeObjectBuilder.java:80)
    at com.blazebit.persistence.impl.query.ObjectBuilderTypedQuery.getResultList(ObjectBuilderTypedQuery.java:71)
    at com.blazebit.persistence.impl.AbstractQueryBuilder.getResultList(AbstractQueryBuilder.java:58)
    at com.demo.blazebitquerydsl.model.CatQuerydslTest.entityViewTest(CatQuerydslTest.java:59)
    at java.base/java.lang.reflect.Method.invoke(Method.java:580)
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
Caused by: java.lang.reflect.InvocationTargetException
    at java.base/jdk.internal.reflect.DirectConstructorHandleAccessor.newInstance(DirectConstructorHandleAccessor.java:67)
    at java.base/java.lang.reflect.Constructor.newInstanceWithCaller(Constructor.java:502)
    at java.base/java.lang.reflect.Constructor.newInstance(Constructor.java:486)
    at com.blazebit.persistence.view.impl.proxy.TupleConstructorReflectionInstantiator.newInstance(TupleConstructorReflectionInstantiator.java:79)
    ... 7 more
Caused by: java.lang.ClassCastException: class com.demo.blazebitquerydsl.model.Cat cannot be cast to class java.lang.Long (com.demo.blazebitquerydsl.model.Cat is in unnamed module of loader 'app'; java.lang.Long is in module java.base of loader 'bootstrap')
    at com.demo.blazebitquerydsl.view.CatSimpleViewImpl.<init>(CatSimpleViewImpl.java:44)
    at java.base/jdk.internal.reflect.DirectConstructorHandleAccessor.newInstance(DirectConstructorHandleAccessor.java:62)
    ... 10 more

I debug the generated source code CatSimpleViewImpl.java.

    public CatSimpleViewImpl(CatSimpleViewImpl noop, int offset, int[] assignment, Object[] tuple) {
        super();
        this.id = (Long) tuple[offset + assignment[0]];
        this.name = (String) tuple[offset + assignment[1]];
    }

noop = null offset = 0 tuple.length is 3 tuple[0] = Cat@13462 (a Cat instance) tuple[1] = 1 tuple[2] = 'Tom'

Steps to reproduce

Clone blaze-querydsl-demo, then run

mvn test

Environment

Version: 1.6.12 JPA-Provider: Hibernate 6.5.2.Final Spring Boot: 3.3.2 DBMS: H2 2.2.224 Querydsl: 6.7, OpenFeign fork JDK: Oracle Java SE 21

guofengzh commented 1 month ago

Remind: This issue can also be repoduced using com.querydsl 5.0 and 5.1, see the branch com.querydsl-5.x.

By the way, Blaze Persistence is excellent, but it uses string constants to reference the attributes of entities and associations, which is not type-safe and is not convenient for refactoring attribute names in IDE tools.

So, I'd like to ask here as well, Querydsl uses Q classes to support type-safe access to entity attributes (for example, QCat.cat.name), and uses "new QCat("ketty)" to specify the alias. Does Blaze Persistence have similar functionality (I'm new to Blaze Persistence)?

jwgmeligmeyling commented 1 month ago

Although it does seem not many changes are required to make EntityViews work with the Querydsl integrations, currently this is not implemented (https://github.com/Blazebit/blaze-persistence/issues/1644). Also note that casing to CriteriaBuilder is not safe for set operations and queries with common table expressions.

beikov commented 1 month ago

Does Blaze Persistence have similar functionality (I'm new to Blaze Persistence)?

It does not yet, but I had some ideas on how to approach this:

All these ideas require the a custom static metamodel which means writing a custom annotation processor. It's doable, but nobody stepped up yet to do the work.

guofengzh commented 1 month ago

I'm touched that you responded so quickly.

I tried that using the followin Querydsl query can solve the issue. The simple test wroks.

...
.select(cat.id, cat.name)
...

But I tried a lot and failed to write a select clause for the following view:

@EntityView(Person.class)
public interface PersonView {
    @IdMapping
    Long getId();

    String getName();

    List<CatSimpleView> getKittens();

}

By the way, the code listed in 21.8. Recursive CTEs does not work with mysql 8.2 (It works with H2). See the test case at QuerydslCTETest#recursiveCTETest1. The thrown exception is

org.hibernate.exception.GenericJDBCException: JDBC exception executing SQL [with recursive CatCte(id, name, ancestor_id) AS( select c1_0.id,c1_0.name,c1_0.ancestor_id from cat c1_0 where c1_0.id=? UNION ALL select c1_0.id,c1_0.name,c1_0.ancestor_id from cat c1_0,CatCte cc1_0,CatCte cc2_0 where c1_0.id=cc2_0.ancestor_id ) select cc1_0.id,cc1_0.ancestor_id,cc1_0.name from CatCte cc1_0] [In recursive query block of Recursive Common Table Expression 'catcte', the recursive table must be referenced only once, and not in any subquery] [n/a]

    at org.hibernate.exception.internal.StandardSQLExceptionConverter.convert(StandardSQLExceptionConverter.java:63)
    at org.hibernate.engine.jdbc.spi.SqlExceptionHelper.convert(SqlExceptionHelper.java:108)
    at org.hibernate.engine.jdbc.spi.SqlExceptionHelper.convert(SqlExceptionHelper.java:94)
    at org.hibernate.sql.results.jdbc.internal.DeferredResultSetAccess.executeQuery(DeferredResultSetAccess.java:264)
    at org.hibernate.sql.results.jdbc.internal.DeferredResultSetAccess.getResultSet(DeferredResultSetAccess.java:167)
    at org.hibernate.sql.results.jdbc.internal.JdbcValuesResultSetImpl.advanceNext(JdbcValuesResultSetImpl.java:265)
    at org.hibernate.sql.results.jdbc.internal.JdbcValuesResultSetImpl.processNext(JdbcValuesResultSetImpl.java:145)
    at org.hibernate.sql.results.jdbc.internal.AbstractJdbcValues.next(AbstractJdbcValues.java:19)
    at org.hibernate.sql.results.internal.RowProcessingStateStandardImpl.next(RowProcessingStateStandardImpl.java:67)
    at org.hibernate.sql.results.spi.ListResultsConsumer.consume(ListResultsConsumer.java:182)
    at org.hibernate.sql.results.spi.ListResultsConsumer.consume(ListResultsConsumer.java:33)
    at org.hibernate.sql.exec.internal.JdbcSelectExecutorStandardImpl.doExecuteQuery(JdbcSelectExecutorStandardImpl.java:211)
    at org.hibernate.sql.exec.internal.JdbcSelectExecutorStandardImpl.executeQuery(JdbcSelectExecutorStandardImpl.java:83)
    at org.hibernate.sql.exec.spi.JdbcSelectExecutor.list(JdbcSelectExecutor.java:76)
    at org.hibernate.sql.exec.spi.JdbcSelectExecutor.list(JdbcSelectExecutor.java:65)
    at com.blazebit.persistence.integration.hibernate.base.HibernateExtendedQuerySupport.getResultList(HibernateExtendedQuerySupport.java:591)
    at com.blazebit.persistence.integration.hibernate.base.HibernateExtendedQuerySupport.getResultList(HibernateExtendedQuerySupport.java:503)
    at com.blazebit.persistence.impl.plan.CustomSelectQueryPlan.getResultList(CustomSelectQueryPlan.java:58)
    at com.blazebit.persistence.impl.query.CustomSQLTypedQuery.getResultList(CustomSQLTypedQuery.java:53)
    at com.querydsl.jpa.impl.AbstractJPAQuery.getResultList(AbstractJPAQuery.java:195)
    at com.querydsl.jpa.impl.AbstractJPAQuery.fetch(AbstractJPAQuery.java:247)
    at com.avic.querydsl.QuerydslCTETest.recursiveCTETest1(QuerydslCTETest.java:73)
    at java.base/java.lang.reflect.Method.invoke(Method.java:580)
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
Caused by: java.sql.SQLException: In recursive query block of Recursive Common Table Expression 'catcte', the recursive table must be referenced only once, and not in any subquery
    at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:130)
    at com.mysql.cj.jdbc.exceptions.SQLExceptionsMapping.translateException(SQLExceptionsMapping.java:122)
    at com.mysql.cj.jdbc.ClientPreparedStatement.executeInternal(ClientPreparedStatement.java:916)
    at com.mysql.cj.jdbc.ClientPreparedStatement.executeQuery(ClientPreparedStatement.java:972)
    at com.zaxxer.hikari.pool.ProxyPreparedStatement.executeQuery(ProxyPreparedStatement.java:52)
    at com.zaxxer.hikari.pool.HikariProxyPreparedStatement.executeQuery(HikariProxyPreparedStatement.java)
    at org.hibernate.sql.results.jdbc.internal.DeferredResultSetAccess.executeQuery(DeferredResultSetAccess.java:246)
    ... 21 more

The sql generated by Hibernate is:

with recursive CatCte(id, name, ancestor_id) AS
( 
  select c1_0.id,c1_0.name,c1_0.ancestor_id from cat c1_0 where c1_0.id=? 
UNION ALL 
  select c1_0.id,c1_0.name,c1_0.ancestor_id from cat c1_0,CatCte cc1_0,CatCte cc2_0 where c1_0.id=cc2_0.ancestor_id 
) 
select cc1_0.id,cc1_0.ancestor_id,cc1_0.name from CatCte cc1_0

CatCte cc1_0 following UNION ALL may be redundant.

The code list in 14.2. Recursive CTEs works, but the following code must be changed from

.where("id").eqExpression("parentCat.ancestor.id")  (3)

to

.where("cat.id").eqExpression("parentCat.ancestor.id") (3)

or the following exception is thrown:

java.lang.IllegalArgumentException: Could not join path [id] because it did not use an absolute path but multiple root nodes are available!

Thanks for your help very much.

beikov commented 1 month ago

The documentation is part of the repository. We would very much appreciate a PR that fixes this.

guofengzh commented 3 weeks ago

I have created the pull request 1943.

Thanks for your helps!

beikov commented 3 weeks ago

Thank you!