Search code examples
hibernatejpaspring-data-jpajpqlentitymanager

Spring Data JPA/Hibernate: JPQL constructor expressions- difference between using "new(..)" and not using it


I am using JPQL and projecting the specific returned fields into a different Class than the Entity. I can achieve it using at least the following 2 ways in JPQL with EntityManger (em in code):-

@Test
void testWithoutNew() {
    List<OneNew> l = em.createQuery("SELECT o.field, f.field FROM One o JOIN o.manyForeignKey f", OneNew.class).getResultList();
    System.out.println(l);
    
}
@Test
void testWithNew() {
    List<OneNew> l2 = em.createQuery("SELECT new com.test.OneNew(o.field, f.field) FROM One o JOIN o.manyForeignKey f", OneNew.class).getResultList();
    System.out.println(l2);
}

Which one of these ways is more preferable and what is the difference between them?

Which is the intended way to achieve this functionality? And when should you prefer one approach over the other?

The class I am projecting the results into, is:-

@ToString
@AllArgsConstructor
class OneNew {
    private String field;
    private String joinedTablesField;
}

And my entities are:-

@Entity
@Getter
@Setter
@ToString
public class One {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long oneId;
    
    private String field;
    private String fieldNotInProjection;
    
    @OneToMany
    @JoinColumn(name = "one_id")
    private List<ManyForeignKey> manyForeignKey;
    
}
@Entity
@Getter
@Setter
@ToString
public class ManyForeignKey {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long manyForeignKeyId;
    
    private String field;
    private String fieldNotInProjection;

}

Both of the above ways are working but when I try to purposefully mismatch the arguments in the constructor with respect to the fields returned in the query I find that both of these ways throw different exceptions:-

when not using the new(..) syntax:-

org.hibernate.query.QueryTypeMismatchException: Result type is 'OneNew' but the query returned a 'String'
    at org.hibernate.sql.results.internal.RowTransformerCheckingImpl.transformRow(RowTransformerCheckingImpl.java:33)
    at org.hibernate.sql.results.internal.StandardRowReader.readRow(StandardRowReader.java:102)
    at org.hibernate.sql.results.spi.ListResultsConsumer.consume(ListResultsConsumer.java:205)
    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 org.hibernate.query.sqm.internal.ConcreteSqmSelectQueryPlan.lambda$new$2(ConcreteSqmSelectQueryPlan.java:139)
    at org.hibernate.query.sqm.internal.ConcreteSqmSelectQueryPlan.withCacheableSqmInterpretation(ConcreteSqmSelectQueryPlan.java:382)
    at org.hibernate.query.sqm.internal.ConcreteSqmSelectQueryPlan.performList(ConcreteSqmSelectQueryPlan.java:302)
    at org.hibernate.query.sqm.internal.QuerySqmImpl.doList(QuerySqmImpl.java:526)
    at org.hibernate.query.spi.AbstractSelectionQuery.list(AbstractSelectionQuery.java:423)
    at org.hibernate.query.Query.getResultList(Query.java:120)
    at com.test.TestDbRelationshipsApplicationTests.testWithoutNew(TestDbRelationshipsApplicationTests.java:49)
    at java.base/java.lang.reflect.Method.invoke(Method.java:568)
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)

While, when using the new(..) syntax:-

java.lang.IllegalArgumentException: org.hibernate.query.SemanticException: Missing constructor for type 'OneNew' [SELECT new com.test.OneNew(o.field, f.field) FROM One o JOIN o.manyForeignKey f]
    at org.hibernate.internal.ExceptionConverterImpl.convert(ExceptionConverterImpl.java:143)
    at org.hibernate.internal.ExceptionConverterImpl.convert(ExceptionConverterImpl.java:167)
    at org.hibernate.internal.ExceptionConverterImpl.convert(ExceptionConverterImpl.java:173)
    at org.hibernate.internal.AbstractSharedSessionContract.createQuery(AbstractSharedSessionContract.java:860)
    at org.hibernate.internal.AbstractSharedSessionContract.createQuery(AbstractSharedSessionContract.java:140)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:568)
    at org.springframework.orm.jpa.SharedEntityManagerCreator$SharedEntityManagerInvocationHandler.invoke(SharedEntityManagerCreator.java:319)
    at jdk.proxy2/jdk.proxy2.$Proxy106.createQuery(Unknown Source)
    at com.test.TestDbRelationshipsApplicationTests.testWithNew(TestDbRelationshipsApplicationTests.java:57)
    at java.base/java.lang.reflect.Method.invoke(Method.java:568)
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
Caused by: org.hibernate.query.SemanticException: Missing constructor for type 'OneNew' [SELECT new com.test.OneNew(o.field, f.field) FROM One o JOIN o.manyForeignKey f]
    at org.hibernate.query.hql.internal.SemanticQueryBuilder.visitInstantiation(SemanticQueryBuilder.java:1506)
    at org.hibernate.query.hql.internal.SemanticQueryBuilder.visitInstantiation(SemanticQueryBuilder.java:275)
    at org.hibernate.grammars.hql.HqlParser$InstantiationContext.accept(HqlParser.java:4029)
    at org.hibernate.query.hql.internal.SemanticQueryBuilder.visitSelectableNode(SemanticQueryBuilder.java:1453)
    at org.hibernate.query.hql.internal.SemanticQueryBuilder.visitSelection(SemanticQueryBuilder.java:1407)
    at org.hibernate.query.hql.internal.SemanticQueryBuilder.visitSelectClause(SemanticQueryBuilder.java:1400)
    at org.hibernate.query.hql.internal.SemanticQueryBuilder.visitQuery(SemanticQueryBuilder.java:1249)
    at org.hibernate.query.hql.internal.SemanticQueryBuilder.visitQuerySpecExpression(SemanticQueryBuilder.java:1035)
    at org.hibernate.query.hql.internal.SemanticQueryBuilder.visitQuerySpecExpression(SemanticQueryBuilder.java:275)
    at org.hibernate.grammars.hql.HqlParser$QuerySpecExpressionContext.accept(HqlParser.java:2132)
    at org.hibernate.query.hql.internal.SemanticQueryBuilder.visitSimpleQueryGroup(SemanticQueryBuilder.java:1020)
    at org.hibernate.query.hql.internal.SemanticQueryBuilder.visitSimpleQueryGroup(SemanticQueryBuilder.java:275)
    at org.hibernate.grammars.hql.HqlParser$SimpleQueryGroupContext.accept(HqlParser.java:2003)
    at org.hibernate.query.hql.internal.SemanticQueryBuilder.visitSelectStatement(SemanticQueryBuilder.java:490)
    at org.hibernate.query.hql.internal.SemanticQueryBuilder.visitStatement(SemanticQueryBuilder.java:449)
    at org.hibernate.query.hql.internal.SemanticQueryBuilder.buildSemanticModel(SemanticQueryBuilder.java:322)
    at org.hibernate.query.hql.internal.StandardHqlTranslator.translate(StandardHqlTranslator.java:71)
    at org.hibernate.query.internal.QueryInterpretationCacheStandardImpl.createHqlInterpretation(QueryInterpretationCacheStandardImpl.java:145)
    at org.hibernate.query.internal.QueryInterpretationCacheStandardImpl.resolveHqlInterpretation(QueryInterpretationCacheStandardImpl.java:132)
    at org.hibernate.internal.AbstractSharedSessionContract.interpretHql(AbstractSharedSessionContract.java:802)
    at org.hibernate.internal.AbstractSharedSessionContract.createQuery(AbstractSharedSessionContract.java:852)
    ... 11 more

Solution

  • If you use the new keyword then the class referenced can be either or some DTO class (i.e. not mapped as an entity) or an existing Entity class. However in the latter case the returned instance will be in the NEW state i.e. will not be managed.

    Where E is an entity

    select E from E where ... ->

    return a managed instance. Lazily mapped associations can be fetched on demand. Changes made in the context of a session will be auto-saved on transaction end.

    select new E(...) ->

    returns a non-managed instance of E. Only the fields specified in the constructor expression will be set.

    select new SomeDto(...) ->

    return some other class populated according to the constructor expression.

    Where using the new syntax then you must have a constructor matching the fields on the query. Your OneNew has no 2 arg constructor so hence the error message.

    So the 2 things are completely different and use cases will determine which one you want to use.