diff --git a/documentation/src/main/asciidoc/introduction/Interacting.adoc b/documentation/src/main/asciidoc/introduction/Interacting.adoc index 463cd4399c1a..3ad4e50baa16 100644 --- a/documentation/src/main/asciidoc/introduction/Interacting.adoc +++ b/documentation/src/main/asciidoc/introduction/Interacting.adoc @@ -869,7 +869,7 @@ Java's `record` types now offer an interesting alternative: record IsbnTitle(String isbn, String title) {} var results = - session.createSelectionQuery("select new IsbnTitle(isbn, title) from Book", IsbnTitle.class) + session.createSelectionQuery("select isbn, title from Book", IsbnTitle.class) .getResultList(); for (var result : results) { @@ -882,8 +882,7 @@ Notice that we're able to declare the `record` right before the line which execu Now, this is only _superficially_ more typesafe, since the query itself is not checked statically, and so we can't say it's objectively better. But perhaps you find it more aesthetically pleasing. - -On the other hand, when we're passing query results around the system, the use of `select new` with a `record` type is much better than manually unpacking an `Object[]` array. +And if we're going to be passing query results around the system, the use of a `record` type is _much_ better. Now, the criteria query API offers a much more satisfying solution to the problem. Consider the following code: diff --git a/documentation/src/main/asciidoc/querylanguage/Concepts.adoc b/documentation/src/main/asciidoc/querylanguage/Concepts.adoc index a5618df20418..548765221e56 100644 --- a/documentation/src/main/asciidoc/querylanguage/Concepts.adoc +++ b/documentation/src/main/asciidoc/querylanguage/Concepts.adoc @@ -1,13 +1,13 @@ [[basic-concepts]] == Basic concepts -This document describes Hibernate Query Language (HQL), which is, in some sense, a dialect of the Java (now Jakarta) Persistence Query Language (JPQL). +This document describes Hibernate Query Language (HQL), which is, I suppose we could say, a dialect of the Java (now Jakarta) Persistence Query Language (JPQL). Or is it the other way around? [NOTE] ==== -JPQL was inspired by early versions of HQL, and is a subset of modern HQL. +JPQL was inspired by early versions of HQL, and is a proper subset of modern HQL. Here we focus on describing the complete, more powerful HQL language as it exists today. If strict JPA compliance is what you're looking for, use the setting `hibernate.jpa.compliance.query=true`. @@ -108,7 +108,15 @@ The JPQL specification defines identification variables as case-_insensitive_. And so in strict JPA-compliant mode, Hibernate treats `person.nickName`, `Person.nickName`, and `PERSON.nickName` as the _same_. ==== -A _quoted identifier_ is written in backticks. Quoting lets you use a keyword as an identifier, for example `` thing.\`select` ``. +A _quoted identifier_ is written in backticks. Quoting lets you use a keyword as an identifier. + +[source,hql] +---- +select thing.interval.`from` from Thing thing +---- + +Actually, in most contexts, HQL keywords are "soft", and don't need to be quoted. +The parser is usually able to distinguish if the reserved word is being used as a keyword or as an identifier. [[type-system]] === Type system @@ -135,13 +143,14 @@ Since the language must be executed on SQL databases, every type accommodates nu ==== Null values and ternary logic The SQL `null` behaves quite differently to a null value in Java. -In Java, an expression like `number + 1` produces in an exception if `number` is null. -But in SQL, and therefore also in HQL and JPQL, such an expression evaluates to `null`. + +- In Java, an expression like `number + 1` produces in an exception if `number` is null. +- But in SQL, and therefore also in HQL and JPQL, such an expression evaluates to `null`. [IMPORTANT] ==== It's almost always the case that an operation applied to a null value yields another null value. -This applies to function application, to operators like `*` and `||`, to comparison operators like `<` and `=`, and even to logical operations like `and` and `not`. +This rule applies to function application, to operators like `*` and `||`, to comparison operators like `<` and `=`, and even to logical operations like `and` and `not`. The exceptions to this rule are the functions `coalesce()` and `ifnull()` which are specifically designed for <>. ==== @@ -295,7 +304,7 @@ The second form may insert many new rows, or none at all. The first sort of `insert` statement is not as useful. It's usually better to just use `persist()`. -On the other hand, you might consider using it to set up test data. +But you might consider using it to set up test data. ==== [NOTE] @@ -477,7 +486,7 @@ from Book book select book.title, book.isbn This form of the query is more readable, because the alias is declared _before_ it's used, just as God and nature intended. ==== -Of course, queries are always polymorphic. +Naturally, queries are always polymorphic. Indeed, a fairly innocent-looking HQL query can easily translate to a SQL statement with many joins and unions. [TIP] @@ -485,4 +494,197 @@ Indeed, a fairly innocent-looking HQL query can easily translate to a SQL statem We need to be a _bit_ careful about that, but actually it's usually a good thing. HQL makes it very easy to fetch all the data we need in a single trip to the database, and that's absolutely key to achieving high performance in data access code. Typically, it's much worse to fetch exactly the data we need, but in many round trips to the database server, than it is to fetch just a bit more data than what we're going to need, all a single SQL query. -==== \ No newline at end of file +==== + +[[returning-to-java]] +=== Representing result sets in Java + +One of the most uncomfortable aspects of working with data in Java is that there's no good way to represent a table. +Languages designed for working with data—R is an excellent example—always feature some sort of built-in table or "data frame" type. +Of course, Java's type system gets in the way here. +This problem is much easier to solve in a dynamically-typed language. +The fundamental problem for Java is that it doesn't have tuple types. + +Queries in Hibernate return tables. +Sure, often a column holds whole entity objects, but we're not restricted to returning a single entity, and we often write queries that return multiple entities in each result, or which return things which aren't entities. + +So we're faced with the problem if representing such result sets, and, we're sad to say, there's no fully general and completely satisfying solution. + +Let's begin with the easy case. + +[[query-result-types-single]] +==== Queries with a single projected item + +If there's just one projected item in the `select` list, then, no sweat, that's the type of each query result. + +[source, java] +[%unbreakable] +---- +List results = + entityManager.createQuery("select title from Book", String.class) + .getResultList(); +---- + +There's really no need to fuss about with trying to represent a "tuple of length 1". +We're not even sure what to call those. + +Problems arise as soon as we have multiple items in the `select` list of a query. + +[[query-result-types-multiple]] +==== Queries with multiple projected items + +When there are multiple expressions in the select list then, by default, and in compliance with JPA, each query result is packaged as an array of type `Object[]`. + +[[select-clause-projection-example]] +[source, java] +[%unbreakable] +---- +List results = + entityManager.createQuery("select title, left(book.text, 200) from Book", + Object[].class) + .getResultList(); +for (var result : results) { + String title = (String) result[0]; + String preamble = (String) result[1]; +} +---- + +This is bearable, but let's explore some other options. + +JPA lets us specify that we want each query result packaged as an instance of `javax.persistence.Tuple`. +All we have to do is pass the class `Tuple` to `createQuery()`. + +[source, java] +[%unbreakable] +---- +List tuples = + entityManager.createQuery("select title as title, left(book.text, 200) as preamble from Book", + Tuple.class) + .getResultList(); +for (Tuple tuple : tuples) { + String title = tuple.get("title", String.class); + String preamble = tuple.get("preamble", String.class); +} +---- + +The names of the `Tuple` elements are determined by the aliases given to the projected items in the select list. +If no aliases are specified, the elements may be accessed by their position in the list, where the first item is assigned the position zero. + +As an extension to JPA, and in a similar vein, Hibernate lets us pass `Map` or `List`, and have each result packaged as a map or list: + +[source, java] +[%unbreakable] +---- +var results = + entityManager.createQuery("select title as title, left(book.text, 200) as preamble from Book", + Map.class) + .getResultList(); +for (var map : results) { + String title = (String) tuple.get("title"); + String preamble = (String) tuple.get("preamble"); +} +---- +[source, java] +[%unbreakable] +---- +var results = + entityManager.createQuery("select title, left(book.text, 200) from Book", + List.class) + .getResultList(); +for (var list : results) { + String title = (String) list.get(0); + String preamble = (String) tuple.get(1); +} +---- + +Unfortunately, not one of the types `Object[]`, `List`, `Map`, nor `Tuple` lets us access an individual item in a result tuple without a type cast. +Sure `Tuple` does the type cast for us when we pass a class object to `get()`, but it's logically identical. +Fortunately there's one more option, as we're about to see. + +[NOTE] +==== +Actually, `Tuple` really exists to service the criteria query API, and in that context it _does_ enable truly typesafe access to query results. +==== + +Hibernate 6 lets us pass an arbitrary class type with an appropriate constructor to `createQuery()` and will use it to package the query results. +This works extremely nicely with `record` types. + +[[select-clause-implicit-instantiation-example]] +[source, java] +[%unbreakable] +---- +record BookSummary(String title, String summary) {} + +List results = + entityManager.createQuery("select title, left(book.text, 200) from Book", + BookSummary.class) + .getResultList(); +for (var result : results) { + String title = result.title(); + String preamble = result.summary(); +} +---- + +It's important that the constructor of `BookSummary` has parameters which exactly match the items in the `select` list. + +[NOTE] +==== +This class does not need to be mapped or annotated in any way. + +Even if the class _is_ an entity class, the resulting instances are _not_ managed entities and are _not_ associated with the session. +==== + +We must caution that this still isn't typesafe. +In fact, we've just pushed the typecasts down into the call to `createQuery()`. +But at least we don't have to write them explicitly. + +[[select-new]] +==== Instantiation + +In JPA, and in older versions of Hibernate, this functionality required more ceremony. + +[cols="25,~,~,^15"] +|=== +| Result type | Legacy syntax | Streamlined syntax | JPA standard + +| `Map` | `select new map(x, y)` | `select x, y` | ✖/✖ +| `List` | `select new list(x, y)` | `select x, y` | ✖/✖ +| Arbitrary class `Record` | `select new Record(x, y)` | `select x, y` | ✔/✖ +|=== + +For example, the JPA-standard `select new` construct packages the query results into a user-written Java class instead of an array. + +[[select-clause-dynamic-instantiation-example]] +[source, java] +[%unbreakable] +---- +record BookSummary(String title, String summary) {} + +List results = + entityManager.createQuery("select new BookSummary(title, left(book.text, 200)) from Book", + BookSummary.class) + .getResultList(); +for (var result : results) { + String title = result.title(); + String preamble = result.summary(); +} +---- + +Simplifying slightly, the BNF for a projected item is: + +[[select-item-bnf]] +[source, antlrv4] +---- +selection + : (expression | instantiation) alias? + +instantiation + : "NEW" instantiationTarget "(" selection ("," selection)* ")" + +alias + : "AS"? identifier +---- + +Where the list of ``selection``s in an `instantiation` is essentially a nested projection list. + + diff --git a/documentation/src/main/asciidoc/querylanguage/Relational.adoc b/documentation/src/main/asciidoc/querylanguage/Relational.adoc index 0fe1c4a798a4..cda2224de9e2 100644 --- a/documentation/src/main/asciidoc/querylanguage/Relational.adoc +++ b/documentation/src/main/asciidoc/querylanguage/Relational.adoc @@ -137,142 +137,6 @@ If a query has no explicit `select` list, then, as we saw < results = - entityManager.createQuery("select title from Book", String.class) - .getResultList(); ----- - -But if there are multiple expressions in the select list then, by default, each query result is packaged as an array of type `Object[]`. - -[[select-clause-projection-example]] -[source, java] -[%unbreakable] ----- -List results = - entityManager.createQuery("select title, left(book.text, 200) from Book", - Object[].class) - .getResultList(); -for (var result : results) { - String title = (String) result[0]; - String preamble = (String) result[1]; -} ----- - -Or, if explicitly requested by passing the class `Tuple` to `createQuery()`, the query result is packaged as an instance of `javax.persistence.Tuple`. - -[source, java] -[%unbreakable] ----- -List results = - entityManager.createQuery("select title as title, left(book.text, 200) as preamble from Book", - Tuple.class) - .getResultList(); -for (Tuple tuple : tuples) { - String title = tuple.get("title", String.class); - String preamble = tuple.get("preamble", String.class); -} ----- - -The names of the `Tuple` elements are determined by the aliases given to the projected items in the select list. -If no aliases are specified, the elements may be accessed by their position in the list, where the first item is assigned the position zero. - -Unfortunately, neither `Object[]` nor `Tuple` lets us access an individual item in a result tuple of an HQL query without explicitly specifying the type of the item, either using a typecast in the case of `Object[]`, or by passing the class object to `get()` in the case of `Tuple`. -But there's another option, as we're about to see. - -Simplifying slightly, the BNF for a projected item is: - -[[select-item-bnf]] -[source, antlrv4] ----- -selection - : (expression | instantiation) alias? - -instantiation - : "NEW" instantiationTarget "(" selection ("," selection)* ")" - -alias - : "AS"? identifier ----- - -Where the list of ``selection``s in an `instantiation` is essentially a nested projection list. - -So there's a special expression type that's only legal in the select clause: the `instantiation` rule in the BNF above. -// Let's see what it does. - -[[select-new]] -==== Instantiation - -The `select new` construct packages the query results into a user-written Java class instead of an array. - -[[select-clause-dynamic-instantiation-example]] -[source, java] -[%unbreakable] ----- -record BookSummary(String title, String summary) {} - -List results = - entityManager.createQuery("select new BookSummary(title, left(book.text, 200)) from Book", - BookSummary.class) - .getResultList(); -for (var result : results) { - String title = result.title(); - String preamble = result.summary(); -} ----- - -The class must have a matching constructor. - -[IMPORTANT] -==== -This class does not need to be mapped or annotated in any way. - -Even if the class _is_ an entity class, the resulting instances are _not_ managed entities and are _not_ associated with the session. -==== - -Alternatively, using the syntax `select new map`, the query may specify that each result should be packaged as a map: - -[[select-clause-dynamic-map-instantiation-example]] -[source, java] -[%unbreakable] ----- -List results = - entityManager.createQuery("select new map(title as title, left(book.text, 200) as summary) from Book", - Map.class) - .getResultList(); ----- - -The keys of the map are determined by the aliases given to the projected items in the select list. -If no aliases are specified, the key of an item is its position in the list, where the first item is assigned the position zero. - -Or, using the syntax `select new list`, the query may specify that each result should be packaged as a list: - -[[select-clause-dynamic-list-instantiation-example]] -[source, java] -[%unbreakable] ----- -List results = - entityManager.createQuery("select new list(title as title, left(book.text, 200) as summary) from Book", - List.class) - .getResultList(); ----- - -[NOTE] -==== -This is an older syntax, that predates JPQL. -In hindsight, it's hard to see what advantage `List` offers compared to `Object[]`. -We mention it here only for completeness. -On the other hand, `Map` is a perfectly fine alternative `Tuple`, but of course it isn't portable to other implementations of JPA. -==== - [[distinct]] ==== Duplicate removal diff --git a/documentation/src/main/style/asciidoctor/css/hibernate-layout.css b/documentation/src/main/style/asciidoctor/css/hibernate-layout.css index 0d7a321d7a72..e1f24760fdd0 100644 --- a/documentation/src/main/style/asciidoctor/css/hibernate-layout.css +++ b/documentation/src/main/style/asciidoctor/css/hibernate-layout.css @@ -90,7 +90,15 @@ h3 { margin-bottom: 1.0em !important; line-height:1.0125em !important } -h4,h5,h6{ +h4{ +font-weight: bold !important; + font-size:1.125em !important; + text-decoration: none !important; + color:#4a5d75 !important; + font-family: 'Noto Serif', 'Lucida Grande', Geneva, Verdana, Arial, sans-serif !important + margin-top: 1em !important; + margin-bottom: 0.8em !important; +}h4,h5,h6{ font-size:1.125em !important; text-decoration: none !important; color:#4a5d75 !important; diff --git a/hibernate-core/src/main/java/org/hibernate/NonUniqueResultException.java b/hibernate-core/src/main/java/org/hibernate/NonUniqueResultException.java index 435fbdf95dd9..328211429bab 100644 --- a/hibernate-core/src/main/java/org/hibernate/NonUniqueResultException.java +++ b/hibernate-core/src/main/java/org/hibernate/NonUniqueResultException.java @@ -23,7 +23,7 @@ public class NonUniqueResultException extends HibernateException { * @param resultCount The number of actual results. */ public NonUniqueResultException(int resultCount) { - super( "query did not return a unique result: " + resultCount ); + super( "Query did not return a unique result: " + resultCount + " results were returned" ); } } diff --git a/hibernate-core/src/main/java/org/hibernate/internal/AbstractSharedSessionContract.java b/hibernate-core/src/main/java/org/hibernate/internal/AbstractSharedSessionContract.java index ea28e3594eb5..60299bceca61 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/AbstractSharedSessionContract.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/AbstractSharedSessionContract.java @@ -742,13 +742,13 @@ protected static void checkSelectionQuery(String hql, HqlInterpretation hqlInter } } - protected static void checkResultType(Class expectedResultType, SqmSelectionQueryImpl query) { + protected static void checkResultType(Class expectedResultType, SqmSelectionQueryImpl query) { final Class resultType = query.getResultType(); if ( !expectedResultType.isAssignableFrom( resultType ) ) { throw new QueryTypeMismatchException( String.format( Locale.ROOT, - "Query result-type error - expecting `%s`, but found `%s`", + "Incorrect query result type: query produces '%s' but type '%s' was given", expectedResultType.getName(), resultType.getName() ) @@ -849,6 +849,7 @@ protected void addResultType(Class resultClass, NativeQueryImplementor if ( Tuple.class.equals( resultClass ) ) { query.setTupleTransformer( new NativeQueryTupleTransformer() ); } + // TODO: handle Map, List as well else if ( getFactory().getMappingMetamodel().isEntityClass( resultClass ) ) { query.addEntity( "alias1", resultClass.getName(), LockMode.READ ); } diff --git a/hibernate-core/src/main/java/org/hibernate/query/spi/AbstractSelectionQuery.java b/hibernate-core/src/main/java/org/hibernate/query/spi/AbstractSelectionQuery.java index ccc2c001c662..8dffca663785 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/spi/AbstractSelectionQuery.java +++ b/hibernate-core/src/main/java/org/hibernate/query/spi/AbstractSelectionQuery.java @@ -66,6 +66,7 @@ import org.hibernate.query.sqm.tree.select.SqmQueryPart; import org.hibernate.query.sqm.tree.select.SqmQuerySpec; import org.hibernate.query.sqm.tree.select.SqmSelectStatement; +import org.hibernate.query.sqm.tree.select.SqmSelectableNode; import org.hibernate.query.sqm.tree.select.SqmSelection; import org.hibernate.sql.exec.internal.CallbackImpl; import org.hibernate.sql.exec.spi.Callback; @@ -105,39 +106,58 @@ public AbstractSelectionQuery(SharedSessionContractImplementor session) { } protected TupleMetadata buildTupleMetadata(SqmStatement statement, Class resultType) { - if ( resultType != null && Tuple.class.isAssignableFrom( resultType ) ) { - final List> selections = ( (SqmSelectStatement) statement ).getQueryPart() - .getFirstQuerySpec() - .getSelectClause() - .getSelections(); - // resultType is Tuple.. - if ( getQueryOptions().getTupleTransformer() == null ) { - final Map, Integer> tupleElementMap; - if ( selections.size() == 1 && selections.get( 0 ).getSelectableNode() instanceof CompoundSelection ) { - final List> selectionItems = selections.get( 0 ) - .getSelectableNode() - .getSelectionItems(); - tupleElementMap = new IdentityHashMap<>( selectionItems.size() ); - for ( int i = 0; i < selectionItems.size(); i++ ) { - tupleElementMap.put( selectionItems.get( i ), i ); - } - } - else { - tupleElementMap = new IdentityHashMap<>( selections.size() ); - for ( int i = 0; i < selections.size(); i++ ) { - final SqmSelection selection = selections.get( i ); - tupleElementMap.put( selection.getSelectableNode(), i ); - } - } - return new TupleMetadata( tupleElementMap ); + if ( isInstantiableWithoutMetadata( resultType ) ) { + // no need to build metadata for instantiating tuples + return null; + } + else { + final SqmSelectStatement select = (SqmSelectStatement) statement; + final List> selections = + select.getQueryPart().getFirstQuerySpec().getSelectClause() + .getSelections(); + if ( Tuple.class.equals( resultType ) || selections.size() > 1 ) { + return getTupleMetadata( selections ); + } + else { + // only one element in select list, + // we don't support instantiation + return null; } + } + } + private TupleMetadata getTupleMetadata(List> selections) { + if ( getQueryOptions().getTupleTransformer() == null ) { + return new TupleMetadata( buildTupleElementMap( selections ) ); + } + else { throw new IllegalArgumentException( "Illegal combination of Tuple resultType and (non-JpaTupleBuilder) TupleTransformer : " + getQueryOptions().getTupleTransformer() ); } - return null; + } + + private static Map, Integer> buildTupleElementMap(List> selections) { + final Map, Integer> tupleElementMap; + if ( selections.size() == 1 + && selections.get( 0 ).getSelectableNode() instanceof CompoundSelection ) { + final List> selectionItems = + selections.get( 0 ).getSelectableNode() + .getSelectionItems(); + tupleElementMap = new IdentityHashMap<>( selectionItems.size() ); + for ( int i = 0; i < selectionItems.size(); i++ ) { + tupleElementMap.put( selectionItems.get( i ), i ); + } + } + else { + tupleElementMap = new IdentityHashMap<>( selections.size() ); + for (int i = 0; i < selections.size(); i++ ) { + final SqmSelection selection = selections.get( i ); + tupleElementMap.put( selection.getSelectableNode(), i ); + } + } + return tupleElementMap; } protected void applyOptions(NamedSqmQueryMemento memento) { @@ -207,7 +227,7 @@ protected void applyOptions(NamedQueryMemento memento) { */ protected void visitQueryReturnType( SqmQueryPart queryPart, - Class resultType, + Class expectedResultType, SessionFactoryImplementor factory) { assert getQueryString().equals( CRITERIA_HQL_STRING ); @@ -231,119 +251,132 @@ protected void visitQueryReturnType( } } - if ( resultType != null ) { - checkQueryReturnType( sqmQuerySpec, resultType, factory ); + if ( expectedResultType != null ) { + checkQueryReturnType( sqmQuerySpec, expectedResultType, factory ); } } else { final SqmQueryGroup queryGroup = (SqmQueryGroup) queryPart; for ( SqmQueryPart sqmQueryPart : queryGroup.getQueryParts() ) { - visitQueryReturnType( sqmQueryPart, resultType, factory ); + visitQueryReturnType( sqmQueryPart, expectedResultType, factory ); } } } protected static void checkQueryReturnType( SqmQuerySpec querySpec, - Class resultClass, + Class expectedResultClass, SessionFactoryImplementor sessionFactory) { - if ( resultClass == null || resultClass == Object.class ) { - // nothing to check - return; - } - - final List> selections = querySpec.getSelectClause().getSelections(); - - if ( resultClass.isArray() ) { - // todo (6.0) : implement - } - else if ( Tuple.class.isAssignableFrom( resultClass ) ) { - // todo (6.0) : implement - } - else { - final boolean jpaQueryComplianceEnabled = sessionFactory.getSessionFactoryOptions() - .getJpaCompliance() - .isJpaQueryComplianceEnabled(); - if ( selections.size() != 1 ) { - final String errorMessage = "Query result-type error - multiple selections: use Tuple or array"; - - if ( jpaQueryComplianceEnabled ) { - throw new IllegalArgumentException( errorMessage ); - } - else { - throw new QueryTypeMismatchException( errorMessage ); + if ( !isResultTypeAlwaysAllowed( expectedResultClass ) ) { + final List> selections = querySpec.getSelectClause().getSelections(); + if ( selections.size() == 1 ) { + // we have one item in the select list, + // the type has to match (no instantiation) + final SqmSelection sqmSelection = selections.get(0); + + // special case for parameters in the select list + final SqmSelectableNode selection = sqmSelection.getSelectableNode(); + if ( selection instanceof SqmParameter ) { + final SqmParameter sqmParameter = (SqmParameter) selection; + final SqmExpressible nodeType = sqmParameter.getNodeType(); + // we may not yet know a selection type + if ( nodeType == null || nodeType.getExpressibleJavaType() == null ) { + // we can't verify the result type up front + return; + } } - } - - final SqmSelection sqmSelection = selections.get( 0 ); - if ( sqmSelection.getSelectableNode() instanceof SqmParameter ) { - final SqmParameter sqmParameter = (SqmParameter) sqmSelection.getSelectableNode(); - - // we may not yet know a selection type - if ( sqmParameter.getNodeType() == null || sqmParameter.getNodeType().getExpressibleJavaType() == null ) { - // we can't verify the result type up front - return; + final boolean jpaQueryComplianceEnabled = + sessionFactory.getSessionFactoryOptions() + .getJpaCompliance() + .isJpaQueryComplianceEnabled(); + if ( !jpaQueryComplianceEnabled ) { + verifyResultType( expectedResultClass, sqmSelection.getNodeType() ); } } - - if ( jpaQueryComplianceEnabled ) { - return; - } - verifyResultType( resultClass, sqmSelection.getNodeType(), sessionFactory ); + // else, let's assume we can instantiate it! } } - protected static void verifyResultType( - Class resultClass, - SqmExpressible sqmExpressible, - SessionFactoryImplementor sessionFactory) { + private static boolean isInstantiableWithoutMetadata(Class resultType) { + return resultType == null + || resultType.isArray() + || Object.class == resultType + || List.class == resultType; + } + + private static boolean isResultTypeAlwaysAllowed(Class expectedResultClass) { + return expectedResultClass == null + || expectedResultClass == Object.class + || expectedResultClass == List.class + || expectedResultClass == Tuple.class + || expectedResultClass.isArray(); + } + + protected static void verifyResultType(Class resultClass, SqmExpressible sqmExpressible) { assert sqmExpressible != null; final JavaType expressibleJavaType = sqmExpressible.getExpressibleJavaType(); assert expressibleJavaType != null; final Class javaTypeClass = expressibleJavaType.getJavaTypeClass(); if ( !resultClass.isAssignableFrom( javaTypeClass ) ) { if ( expressibleJavaType instanceof PrimitiveJavaType ) { - if ( ( (PrimitiveJavaType) expressibleJavaType ).getPrimitiveClass() == resultClass ) { - return; + if ( ( (PrimitiveJavaType) expressibleJavaType ).getPrimitiveClass() != resultClass ) { + throwQueryTypeMismatchException( resultClass, sqmExpressible ); } + } + else if ( isMatchingDateType( javaTypeClass, resultClass, sqmExpressible ) ) { + // special case, we are good + } + else { throwQueryTypeMismatchException( resultClass, sqmExpressible ); } - // Special case for date because we always report java.util.Date as expression type - // But the expected resultClass could be a subtype of that, so we need to check the JdbcType - if ( javaTypeClass == Date.class ) { - JdbcType jdbcType = null; - if ( sqmExpressible instanceof BasicDomainType ) { - jdbcType = ( (BasicDomainType) sqmExpressible).getJdbcType(); - } - else if ( sqmExpressible instanceof SqmPathSource ) { - final DomainType domainType = ( (SqmPathSource) sqmExpressible).getSqmPathType(); - if ( domainType instanceof BasicDomainType ) { - jdbcType = ( (BasicDomainType) domainType ).getJdbcType(); + } + } + + // Special case for date because we always report java.util.Date as expression type + // But the expected resultClass could be a subtype of that, so we need to check the JdbcType + private static boolean isMatchingDateType( + Class javaTypeClass, + Class resultClass, + SqmExpressible sqmExpressible) { + return javaTypeClass == Date.class + && isMatchingDateJdbcType( resultClass, getJdbcType( sqmExpressible ) ); + } + + private static JdbcType getJdbcType(SqmExpressible sqmExpressible) { + if ( sqmExpressible instanceof BasicDomainType ) { + return ( (BasicDomainType) sqmExpressible).getJdbcType(); + } + else if ( sqmExpressible instanceof SqmPathSource ) { + final DomainType domainType = ( (SqmPathSource) sqmExpressible).getSqmPathType(); + if ( domainType instanceof BasicDomainType ) { + return ( (BasicDomainType) domainType ).getJdbcType(); + } + } + return null; + } + + private static boolean isMatchingDateJdbcType(Class resultClass, JdbcType jdbcType) { + if ( jdbcType != null ) { + switch ( jdbcType.getDefaultSqlTypeCode() ) { + case Types.DATE: + if ( resultClass.isAssignableFrom( java.sql.Date.class ) ) { + return true; } - } - if ( jdbcType != null ) { - switch ( jdbcType.getDefaultSqlTypeCode() ) { - case Types.DATE: - if ( resultClass.isAssignableFrom( java.sql.Date.class ) ) { - return; - } - break; - case Types.TIME: - if ( resultClass.isAssignableFrom( java.sql.Time.class ) ) { - return; - } - break; - case Types.TIMESTAMP: - if ( resultClass.isAssignableFrom( java.sql.Timestamp.class ) ) { - return; - } - break; + break; + case Types.TIME: + if ( resultClass.isAssignableFrom( java.sql.Time.class ) ) { + return true; } - } + break; + case Types.TIMESTAMP: + if ( resultClass.isAssignableFrom( java.sql.Timestamp.class ) ) { + return true; + } + break; } - throwQueryTypeMismatchException( resultClass, sqmExpressible ); } + return false; } private static void throwQueryTypeMismatchException(Class resultClass, SqmExpressible sqmExpressible) { @@ -887,7 +920,7 @@ public SelectionQuery setParameter(int position, Date value, TemporalType tem } @Override - public SelectionQuery setParameterList(String name, Collection values) { + public SelectionQuery setParameterList(String name, @SuppressWarnings("rawtypes") Collection values) { super.setParameterList( name, values ); return this; } @@ -995,7 +1028,7 @@ public

SelectionQuery setParameterList(QueryParameter

parameter, P[] v } @Override - public SelectionQuery setProperties(Map map) { + public SelectionQuery setProperties(@SuppressWarnings("rawtypes") Map map) { super.setProperties( map ); return this; } diff --git a/hibernate-core/src/main/java/org/hibernate/query/spi/SqmQuery.java b/hibernate-core/src/main/java/org/hibernate/query/spi/SqmQuery.java index 7796b1514670..4c3a09850d05 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/spi/SqmQuery.java +++ b/hibernate-core/src/main/java/org/hibernate/query/spi/SqmQuery.java @@ -149,7 +149,7 @@ public interface SqmQuery extends CommonQueryContract { SqmQuery setProperties(Object bean); @Override - SqmQuery setProperties(Map bean); + SqmQuery setProperties(@SuppressWarnings("rawtypes") Map bean); @Override SqmQuery setHibernateFlushMode(FlushMode flushMode); diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/SqmSelectionQuery.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/SqmSelectionQuery.java index a4b450f5c5d2..0b5c20144ec2 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/SqmSelectionQuery.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/SqmSelectionQuery.java @@ -82,7 +82,7 @@ public interface SqmSelectionQuery extends SqmQuery, SelectionQuery { SqmSelectionQuery setParameter(Parameter param, Date value, TemporalType temporalType); @Override - SqmSelectionQuery setParameterList(String name, Collection values); + SqmSelectionQuery setParameterList(String name, @SuppressWarnings("rawtypes") Collection values); @Override

SqmSelectionQuery setParameterList(String name, Collection values, Class

javaType); @@ -100,7 +100,7 @@ public interface SqmSelectionQuery extends SqmQuery, SelectionQuery {

SqmSelectionQuery setParameterList(String name, P[] values, BindableType

type); @Override - SqmSelectionQuery setParameterList(int position, Collection values); + SqmSelectionQuery setParameterList(int position, @SuppressWarnings("rawtypes") Collection values); @Override

SqmSelectionQuery setParameterList(int position, Collection values, Class

javaType); @@ -139,7 +139,7 @@ public interface SqmSelectionQuery extends SqmQuery, SelectionQuery { SqmSelectionQuery setProperties(Object bean); @Override - SqmSelectionQuery setProperties(Map bean); + SqmSelectionQuery setProperties(@SuppressWarnings("rawtypes") Map bean); @Override SqmSelectionQuery setHibernateFlushMode(FlushMode flushMode); diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/ConcreteSqmSelectQueryPlan.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/ConcreteSqmSelectQueryPlan.java index eb8d8346c245..3a1cfb8b2fb4 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/ConcreteSqmSelectQueryPlan.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/ConcreteSqmSelectQueryPlan.java @@ -11,6 +11,8 @@ import java.util.List; import java.util.Map; +import jakarta.persistence.Tuple; +import org.hibernate.AssertionFailure; import org.hibernate.ScrollMode; import org.hibernate.engine.jdbc.env.spi.JdbcEnvironment; import org.hibernate.engine.jdbc.spi.JdbcServices; @@ -21,7 +23,6 @@ import org.hibernate.internal.EmptyScrollableResults; import org.hibernate.internal.util.collections.ArrayHelper; import org.hibernate.metamodel.mapping.MappingModelExpressible; -import org.hibernate.query.IllegalQueryOperationException; import org.hibernate.query.Query; import org.hibernate.query.TupleTransformer; import org.hibernate.query.spi.DomainQueryExecutionContext; @@ -41,7 +42,6 @@ import org.hibernate.sql.ast.SqlAstTranslator; import org.hibernate.sql.ast.SqlAstTranslatorFactory; import org.hibernate.sql.ast.spi.FromClauseAccess; -import org.hibernate.sql.ast.tree.expression.JdbcParameter; import org.hibernate.sql.ast.tree.select.SelectStatement; import org.hibernate.sql.exec.spi.JdbcOperationQuerySelect; import org.hibernate.sql.exec.spi.JdbcParameterBindings; @@ -49,7 +49,10 @@ import org.hibernate.sql.exec.spi.JdbcSelectExecutor; import org.hibernate.sql.results.graph.entity.LoadingEntityEntry; import org.hibernate.sql.results.internal.RowTransformerArrayImpl; +import org.hibernate.sql.results.internal.RowTransformerConstructorImpl; import org.hibernate.sql.results.internal.RowTransformerJpaTupleImpl; +import org.hibernate.sql.results.internal.RowTransformerListImpl; +import org.hibernate.sql.results.internal.RowTransformerMapImpl; import org.hibernate.sql.results.internal.RowTransformerSingularReturnImpl; import org.hibernate.sql.results.internal.RowTransformerStandardImpl; import org.hibernate.sql.results.internal.RowTransformerTupleTransformerAdapter; @@ -175,39 +178,39 @@ protected static RowTransformer determineRowTransformer( if ( queryOptions.getTupleTransformer() != null ) { return makeRowTransformerTupleTransformerAdapter( sqm, queryOptions ); } - - if ( resultType == null ) { + else if ( resultType == null ) { return RowTransformerStandardImpl.instance(); } - - if ( resultType.isArray() ) { + else if ( resultType.isArray() ) { return (RowTransformer) RowTransformerArrayImpl.instance(); } - - // NOTE : if we get here : - // 1) there is no TupleTransformer specified - // 2) an explicit result-type, other than an array, was specified - - final List> selections = sqm.getQueryPart().getFirstQuerySpec().getSelectClause().getSelections(); - if ( tupleMetadata != null ) { - // resultType is Tuple.. - if ( queryOptions.getTupleTransformer() == null ) { - return (RowTransformer) new RowTransformerJpaTupleImpl( tupleMetadata ); - } - - throw new IllegalArgumentException( - "Illegal combination of Tuple resultType and (non-JpaTupleBuilder) TupleTransformer : " + - queryOptions.getTupleTransformer() - ); - } - - // NOTE : if we get here we have a resultType of some kind - - if ( selections.size() > 1 ) { - throw new IllegalQueryOperationException( "Query defined multiple selections, return cannot be typed (other that Object[] or Tuple)" ); + else if ( List.class.equals( resultType ) ) { + return (RowTransformer) RowTransformerListImpl.instance(); } else { - return RowTransformerSingularReturnImpl.instance(); + // NOTE : if we get here : + // 1) there is no TupleTransformer specified + // 2) an explicit result-type, other than an array, was specified + + if ( tupleMetadata == null ) { + if ( sqm.getQueryPart().getFirstQuerySpec().getSelectClause().getSelections().size() == 1 ) { + return RowTransformerSingularReturnImpl.instance(); + } + else { + throw new AssertionFailure( "Query defined multiple selections, should have had TupleMetadata" ); + } + } + else { + if ( Tuple.class.equals( resultType ) ) { + return (RowTransformer) new RowTransformerJpaTupleImpl( tupleMetadata ); + } + else if ( Map.class.equals( resultType ) ) { + return (RowTransformer) new RowTransformerMapImpl( tupleMetadata ); + } + else { + return new RowTransformerConstructorImpl<>( resultType, tupleMetadata ); + } + } } } diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/QuerySqmImpl.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/QuerySqmImpl.java index 6fc3b7189487..38a3ec0811cd 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/QuerySqmImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/QuerySqmImpl.java @@ -218,7 +218,7 @@ public QuerySqmImpl( */ public QuerySqmImpl( SqmStatement criteria, - Class resultType, + Class expectedResultType, SharedSessionContractImplementor producer) { super( producer ); this.hql = CRITERIA_HQL_STRING; @@ -262,12 +262,12 @@ public QuerySqmImpl( queryPart.validateQueryStructureAndFetchOwners(); visitQueryReturnType( queryPart, - resultType, + expectedResultType, producer.getFactory() ); } else { - if ( resultType != null ) { + if ( expectedResultType != null ) { throw new IllegalQueryOperationException( "Result type given for a non-SELECT Query", hql, @@ -288,8 +288,8 @@ else if ( sqm instanceof SqmInsertStatement ) { } } - this.resultType = resultType; - this.tupleMetadata = buildTupleMetadata( criteria, resultType ); + this.resultType = expectedResultType; + this.tupleMetadata = buildTupleMetadata( criteria, expectedResultType ); } private void validateStatement(SqmStatement sqmStatement, Class resultType) { diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmSelectionQueryImpl.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmSelectionQueryImpl.java index f91ae9225358..eb33e2a1cf2f 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmSelectionQueryImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmSelectionQueryImpl.java @@ -21,7 +21,6 @@ import jakarta.persistence.LockModeType; import jakarta.persistence.Parameter; import jakarta.persistence.TemporalType; -import jakarta.persistence.Tuple; import org.hibernate.CacheMode; import org.hibernate.FlushMode; @@ -55,7 +54,6 @@ import org.hibernate.query.spi.SelectQueryPlan; import org.hibernate.query.sqm.SqmSelectionQuery; import org.hibernate.query.sqm.internal.SqmInterpretationsKey.InterpretationsKeySource; -import org.hibernate.query.sqm.tree.SqmCopyContext; import org.hibernate.query.sqm.tree.expression.JpaCriteriaParameter; import org.hibernate.query.sqm.tree.expression.SqmJpaCriteriaParameterWrapper; import org.hibernate.query.sqm.tree.expression.SqmParameter; @@ -74,6 +72,8 @@ import static org.hibernate.jpa.SpecHints.HINT_SPEC_CACHE_RETRIEVE_MODE; import static org.hibernate.jpa.SpecHints.HINT_SPEC_CACHE_STORE_MODE; import static org.hibernate.query.spi.SqlOmittingQueryOptions.omitSqlQueryOptions; +import static org.hibernate.query.sqm.internal.SqmInterpretationsKey.createInterpretationsKey; +import static org.hibernate.query.sqm.tree.SqmCopyContext.simpleContext; /** * @author Steve Ebersole @@ -86,7 +86,8 @@ public class SqmSelectionQueryImpl extends AbstractSelectionQuery implemen private final DomainParameterXref domainParameterXref; private final QueryParameterBindingsImpl parameterBindings; - private final Class resultType; + private final Class expectedResultType; + private final Class resultType; private final TupleMetadata tupleMetadata; public SqmSelectionQueryImpl( @@ -104,30 +105,34 @@ public SqmSelectionQueryImpl( this.domainParameterXref = hqlInterpretation.getDomainParameterXref(); this.parameterBindings = QueryParameterBindingsImpl.from( parameterMetadata, session.getFactory() ); + this.expectedResultType = expectedResultType; // visitQueryReturnType( sqm.getQueryPart(), expectedResultType, getSessionFactory() ); - this.resultType = determineResultType( sqm, expectedResultType ); + this.resultType = determineResultType( sqm ); setComment( hql ); this.tupleMetadata = buildTupleMetadata( sqm, expectedResultType ); } - private static Class determineResultType(SqmSelectStatement sqm, Class expectedResultType) { - if ( expectedResultType != null && expectedResultType.equals( Tuple.class ) ) { - //noinspection unchecked - return (Class) Tuple.class; - } - - if ( expectedResultType == null || ! expectedResultType.isArray() ) { - final List> selections = sqm.getQuerySpec().getSelectClause().getSelections(); - if ( selections.size() == 1 ) { - final SqmSelection sqmSelection = selections.get( 0 ); - //noinspection unchecked - return (Class) sqmSelection.getNodeJavaType().getJavaTypeClass(); + private Class determineResultType(SqmSelectStatement sqm) { + final List> selections = sqm.getQuerySpec().getSelectClause().getSelections(); + if ( selections.size() == 1 ) { + if ( Object[].class.equals( expectedResultType ) ) { + // for JPA compatibility + return Object[].class; + } + else { + return selections.get(0).getNodeJavaType().getJavaTypeClass(); } } - - //noinspection unchecked - return (Class) Object[].class; + else if ( expectedResultType != null ) { + // assume we can repackage the tuple as + // the given type (worry about how later) + return expectedResultType; + } + else { + // for JPA compatibility + return Object[].class; + } } public SqmSelectionQueryImpl( @@ -136,6 +141,7 @@ public SqmSelectionQueryImpl( SharedSessionContractImplementor session) { super( session ); this.hql = memento.getHqlString(); + this.expectedResultType = resultType; this.resultType = resultType; final SessionFactoryImplementor factory = session.getFactory(); @@ -164,10 +170,10 @@ public SqmSelectionQueryImpl( public SqmSelectionQueryImpl( NamedCriteriaQueryMementoImpl memento, - Class resultType, + Class expectedResultType, SharedSessionContractImplementor session) { //noinspection unchecked - this( (SqmSelectStatement) memento.getSqmStatement(), resultType, session); + this( (SqmSelectStatement) memento.getSqmStatement(), expectedResultType, session ); applyOptions( memento ); } @@ -177,27 +183,20 @@ public SqmSelectionQueryImpl( SharedSessionContractImplementor session) { super( session ); this.hql = CRITERIA_HQL_STRING; - if ( session.isCriteriaCopyTreeEnabled() ) { - this.sqm = criteria.copy( SqmCopyContext.simpleContext() ); - } - else { - this.sqm = criteria; - } + this.sqm = session.isCriteriaCopyTreeEnabled() ? criteria.copy( simpleContext() ) : criteria; this.domainParameterXref = DomainParameterXref.from( sqm ); - if ( ! domainParameterXref.hasParameters() ) { - this.parameterMetadata = ParameterMetadataImpl.EMPTY; - } - else { - this.parameterMetadata = new ParameterMetadataImpl( domainParameterXref.getQueryParameters() ); - } + this.parameterMetadata = domainParameterXref.hasParameters() + ? new ParameterMetadataImpl( domainParameterXref.getQueryParameters() ) + : ParameterMetadataImpl.EMPTY; this.parameterBindings = QueryParameterBindingsImpl.from( parameterMetadata, session.getFactory() ); // Parameters might be created through HibernateCriteriaBuilder.value which we need to bind here - for ( SqmParameter sqmParameter : this.domainParameterXref.getParameterResolutions().getSqmParameters() ) { + for ( SqmParameter sqmParameter : domainParameterXref.getParameterResolutions().getSqmParameters() ) { if ( sqmParameter instanceof SqmJpaCriteriaParameterWrapper ) { - final JpaCriteriaParameter jpaCriteriaParameter = ( (SqmJpaCriteriaParameterWrapper) sqmParameter ).getJpaCriteriaParameter(); + final JpaCriteriaParameter jpaCriteriaParameter = + ( (SqmJpaCriteriaParameterWrapper) sqmParameter ).getJpaCriteriaParameter(); final Object value = jpaCriteriaParameter.getValue(); // We don't set a null value, unless the type is also null which is the case when using HibernateCriteriaBuilder.value if ( value != null || jpaCriteriaParameter.getNodeType() == null ) { @@ -209,7 +208,8 @@ public SqmSelectionQueryImpl( } } - this.resultType = determineResultType( sqm, expectedResultType ); + this.expectedResultType = expectedResultType; + this.resultType = determineResultType( sqm ); visitQueryReturnType( sqm.getQueryPart(), expectedResultType, getSessionFactory() ); setComment( hql ); @@ -336,12 +336,10 @@ protected ScrollableResultsImplementor doScroll(ScrollMode scrollMode) { // Query plan private SelectQueryPlan resolveQueryPlan() { - final QueryInterpretationCache.Key cacheKey = SqmInterpretationsKey.createInterpretationsKey( this ); + final QueryInterpretationCache.Key cacheKey = createInterpretationsKey( this ); if ( cacheKey != null ) { - return getSession().getFactory().getQueryEngine().getInterpretationCache().resolveSelectQueryPlan( - cacheKey, - this::buildQueryPlan - ); + return getSession().getFactory().getQueryEngine().getInterpretationCache() + .resolveSelectQueryPlan( cacheKey, this::buildQueryPlan ); } else { return buildQueryPlan(); @@ -354,40 +352,29 @@ private SelectQueryPlan buildQueryPlan() { getSession().getFactory() ); - if ( concreteSqmStatements.length > 1 ) { - return buildAggregatedQueryPlan( concreteSqmStatements ); - } - else { - return buildConcreteQueryPlan( concreteSqmStatements[0], getResultType(), getQueryOptions() ); - } + return concreteSqmStatements.length > 1 + ? buildAggregatedQueryPlan( concreteSqmStatements ) + : buildConcreteQueryPlan( concreteSqmStatements[0], getQueryOptions() ); } private SelectQueryPlan buildAggregatedQueryPlan(SqmSelectStatement[] concreteSqmStatements) { //noinspection unchecked final SelectQueryPlan[] aggregatedQueryPlans = new SelectQueryPlan[ concreteSqmStatements.length ]; - // todo (6.0) : we want to make sure that certain thing (ResultListTransformer, etc) only get applied at the aggregator-level - for ( int i = 0, x = concreteSqmStatements.length; i < x; i++ ) { - aggregatedQueryPlans[i] = buildConcreteQueryPlan( - concreteSqmStatements[i], - getResultType(), - getQueryOptions() - ); + aggregatedQueryPlans[i] = buildConcreteQueryPlan( concreteSqmStatements[i], getQueryOptions() ); } - return new AggregatedSelectQueryPlanImpl<>( aggregatedQueryPlans ); } - private SelectQueryPlan buildConcreteQueryPlan( + private SelectQueryPlan buildConcreteQueryPlan( SqmSelectStatement concreteSqmStatement, - Class resultType, QueryOptions queryOptions) { return new ConcreteSqmSelectQueryPlan<>( concreteSqmStatement, getQueryString(), getDomainParameterXref(), - resultType, + expectedResultType, tupleMetadata, queryOptions ); @@ -399,7 +386,7 @@ private SelectQueryPlan buildConcreteQueryPlan( // InterpretationsKeySource @Override - public Class getResultType() { + public Class getResultType() { return resultType; } @@ -684,7 +671,7 @@ public SqmSelectionQuery setParameter(int position, Date value, TemporalType } @Override - public SqmSelectionQuery setParameterList(String name, Collection values) { + public SqmSelectionQuery setParameterList(String name, @SuppressWarnings("rawtypes") Collection values) { super.setParameterList( name, values ); return this; } @@ -720,7 +707,7 @@ public

SqmSelectionQuery setParameterList(String name, P[] values, Bindab } @Override - public SqmSelectionQuery setParameterList(int position, Collection values) { + public SqmSelectionQuery setParameterList(int position, @SuppressWarnings("rawtypes") Collection values) { super.setParameterList( position, values ); return this; } @@ -792,7 +779,7 @@ public

SqmSelectionQuery setParameterList(QueryParameter

parameter, P[ } @Override - public SqmSelectionQuery setProperties(Map map) { + public SqmSelectionQuery setProperties(@SuppressWarnings("rawtypes") Map map) { super.setProperties( map ); return this; } diff --git a/hibernate-core/src/main/java/org/hibernate/sql/results/internal/RowTransformerConstructorImpl.java b/hibernate-core/src/main/java/org/hibernate/sql/results/internal/RowTransformerConstructorImpl.java new file mode 100644 index 000000000000..d152c6ff089b --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/sql/results/internal/RowTransformerConstructorImpl.java @@ -0,0 +1,62 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ + +package org.hibernate.sql.results.internal; + +import jakarta.persistence.Tuple; +import jakarta.persistence.TupleElement; +import org.hibernate.InstantiationException; +import org.hibernate.sql.results.spi.RowTransformer; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * RowTransformer generating a JPA {@link Tuple} + * + * @author Steve Ebersole + */ +public class RowTransformerConstructorImpl implements RowTransformer { + private final Class type; + private final TupleMetadata tupleMetadata; + private final Constructor constructor; + + public RowTransformerConstructorImpl(Class type, TupleMetadata tupleMetadata) { + this.type = type; + this.tupleMetadata = tupleMetadata; + final List> elements = tupleMetadata.getList(); + final Class[] sig = new Class[elements.size()]; + for (int i = 0; i < elements.size(); i++) { + sig[i] = elements.get(i).getJavaType(); + } + try { + constructor = type.getConstructor( sig ); + constructor.setAccessible( true ); + } + catch (Exception e) { + throw new InstantiationException( "Cannot instantiate query result type ", type, e ); + } + } + + @Override + public T transformRow(Object[] row) { + try { + return constructor.newInstance( row ); + } + catch (Exception e) { + throw new InstantiationException( "Cannot instantiate query result type", type, e ); + } + } + + @Override + public int determineNumberOfResultElements(int rawElementCount) { + return 1; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/sql/results/internal/RowTransformerListImpl.java b/hibernate-core/src/main/java/org/hibernate/sql/results/internal/RowTransformerListImpl.java new file mode 100644 index 000000000000..3ce50e007ace --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/sql/results/internal/RowTransformerListImpl.java @@ -0,0 +1,32 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.sql.results.internal; + +import org.hibernate.sql.results.spi.RowTransformer; + +import java.util.List; + +/** + * RowTransformer used when an array is explicitly specified as the return type + * + * @author Steve Ebersole + */ +public class RowTransformerListImpl implements RowTransformer> { + /** + * Singleton access + */ + public static final RowTransformerListImpl INSTANCE = new RowTransformerListImpl(); + + public static RowTransformerListImpl instance() { + return INSTANCE; + } + + @Override + public List transformRow(Object[] row) { + return List.of( row ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/sql/results/internal/RowTransformerMapImpl.java b/hibernate-core/src/main/java/org/hibernate/sql/results/internal/RowTransformerMapImpl.java new file mode 100644 index 000000000000..aa481c6ab4af --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/sql/results/internal/RowTransformerMapImpl.java @@ -0,0 +1,48 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ + +package org.hibernate.sql.results.internal; + +import jakarta.persistence.Tuple; +import jakarta.persistence.TupleElement; +import org.hibernate.sql.results.spi.RowTransformer; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * RowTransformer generating a JPA {@link Tuple} + * + * @author Steve Ebersole + */ +public class RowTransformerMapImpl implements RowTransformer> { + private final TupleMetadata tupleMetadata; + + public RowTransformerMapImpl(TupleMetadata tupleMetadata) { + this.tupleMetadata = tupleMetadata; + } + + @Override + public Map transformRow(Object[] row) { + Map map = new HashMap<>( row.length ); + List> list = tupleMetadata.getList(); + for ( int i = 0; i < row.length; i++ ) { + String alias = list.get(i).getAlias(); + if ( alias == null ) { + alias = Integer.toString(i); + } + map.put( alias, row[i] ); + } + return map; + } + + @Override + public int determineNumberOfResultElements(int rawElementCount) { + return 1; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/sql/results/internal/TupleImpl.java b/hibernate-core/src/main/java/org/hibernate/sql/results/internal/TupleImpl.java index 9c5a4f1300d9..7869ef50c9a6 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/results/internal/TupleImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/results/internal/TupleImpl.java @@ -10,9 +10,10 @@ import java.util.List; import jakarta.persistence.TupleElement; -import org.hibernate.internal.util.type.PrimitiveWrapperHelper; import org.hibernate.query.JpaTuple; +import static org.hibernate.internal.util.type.PrimitiveWrapperHelper.getDescriptorByPrimitiveType; + /** * Implementation of the JPA Tuple contract * @@ -45,7 +46,7 @@ public X get(TupleElement tupleElement) { public X get(String alias, Class type) { final Object untyped = get( alias ); if ( untyped != null ) { - if (!elementTypeMatches(type, untyped)) { + if ( !elementTypeMatches( type, untyped ) ) { throw new IllegalArgumentException( String.format( "Requested tuple value [alias=%s, value=%s] cannot be assigned to requested type [%s]", @@ -99,9 +100,8 @@ public Object get(int i) { } private boolean elementTypeMatches(Class type, Object untyped) { - return type.isInstance(untyped) - || type.isPrimitive() - && PrimitiveWrapperHelper.getDescriptorByPrimitiveType( type).getWrapperClass().isInstance( untyped); + return type.isInstance( untyped ) + || type.isPrimitive() && getDescriptorByPrimitiveType( type ).getWrapperClass().isInstance( untyped ); } @Override diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/ImplicitInstantiationTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/ImplicitInstantiationTest.java new file mode 100644 index 000000000000..8e09b9442ecb --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/ImplicitInstantiationTest.java @@ -0,0 +1,163 @@ +package org.hibernate.orm.test.query.hql; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.persistence.Tuple; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +@DomainModel( + annotatedClasses = { + ImplicitInstantiationTest.Thing.class + } +) +@SessionFactory +public class ImplicitInstantiationTest { + + static class Record { + Long id; + String name; + public Record(Long id, String name) { + this.id = id; + this.name = name; + } + Long id() { + return id; + } + String name() { + return name; + } + } + + @Test + public void testRecordInstantiationWithoutAlias(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + session.persist(new Thing(1L, "thing")); + Record result = session.createSelectionQuery("select id, upper(name) from Thing", Record.class).getSingleResult(); + assertEquals( result.id(), 1L ); + assertEquals( result.name(), "THING" ); + session.getTransaction().setRollbackOnly(); + } + ); + } + + @Test + public void testTupleInstantiationWithAlias(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + session.persist(new Thing(1L, "thing")); + Tuple result = session.createQuery("select id as id, upper(name) as name from Thing", Tuple.class).getSingleResult(); + assertEquals( result.get("id"), 1L ); + assertEquals( result.get("name"), "THING" ); + session.getTransaction().setRollbackOnly(); + } + ); + } + + @Test + public void testTupleInstantiationWithoutAlias(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + session.persist(new Thing(1L, "thing")); + Tuple result = session.createSelectionQuery("select id, upper(name) from Thing", Tuple.class).getSingleResult(); + assertEquals( result.get(0), 1L ); + assertEquals( result.get(1), "THING" ); + session.getTransaction().setRollbackOnly(); + } + ); + } + + @Test + public void testMapInstantiationWithoutAlias(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + session.persist(new Thing(1L, "thing")); + Map result = session.createSelectionQuery("select id, upper(name) from Thing", Map.class).getSingleResult(); + assertEquals( result.get("0"), 1L ); + assertEquals( result.get("1"), "THING" ); + session.getTransaction().setRollbackOnly(); + } + ); + } + + @Test + public void testMapInstantiationWithAlias(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + session.persist(new Thing(1L, "thing")); + Map result = session.createQuery("select id as id, upper(name) as name from Thing", Map.class).getSingleResult(); + assertEquals( result.get("id"), 1L ); + assertEquals( result.get("name"), "THING" ); + session.getTransaction().setRollbackOnly(); + } + ); + } + + @Test + public void testListInstantiationWithoutAlias(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + session.persist(new Thing(1L, "thing")); + List result = session.createSelectionQuery("select id, upper(name) from Thing", List.class).getSingleResult(); + assertEquals( result.get(0), 1L ); + assertEquals( result.get(1), "THING" ); + session.getTransaction().setRollbackOnly(); + } + ); + } + + @Test + public void testListInstantiationWithAlias(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + session.persist(new Thing(1L, "thing")); + List result = session.createQuery("select id as id, upper(name) as name from Thing", List.class).getSingleResult(); + assertEquals( result.get(0), 1L ); + assertEquals( result.get(1), "THING" ); + session.getTransaction().setRollbackOnly(); + } + ); + } + + @Entity(name = "Thing") + @Table(name = "thingy_table") + public class Thing { + private Long id; + + private String name; + + public Thing(Long id, String name) { + this.id = id; + this.name = name; + } + + Thing() { + } + + @Id + public Long getId() { + return this.id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } +}