diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/SingularAttributeImpl.java b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/SingularAttributeImpl.java index 59e1ac670bef..3d31831ce16b 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/SingularAttributeImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/SingularAttributeImpl.java @@ -298,6 +298,11 @@ public SqmPath createSqmPath(SqmPath lhs, SqmPathSource intermediatePat return sqmPathSource.createSqmPath( lhs, intermediatePathSource ); } + @Override + public JavaType getRelationalJavaType() { + return sqmPathSource.getRelationalJavaType(); + } + private class DelayedKeyTypeAccess implements Supplier>, Serializable { private boolean resolved; private SimpleDomainType type; diff --git a/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaExpression.java b/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaExpression.java index cb608307cd48..29d14a63e9b6 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaExpression.java +++ b/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaExpression.java @@ -52,4 +52,6 @@ public interface JpaExpression extends JpaSelection, Expression { @Override JpaPredicate in(Expression> values); + + JpaExpression cast(Class type); } diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/SemanticQueryWalker.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/SemanticQueryWalker.java index 9f878d8cfe42..747aa76518e5 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/SemanticQueryWalker.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/SemanticQueryWalker.java @@ -46,6 +46,7 @@ import org.hibernate.query.sqm.tree.domain.SqmSetJoin; import org.hibernate.query.sqm.tree.domain.SqmSingularJoin; import org.hibernate.query.sqm.tree.domain.SqmTreatedPath; +import org.hibernate.query.sqm.tree.expression.AsWrapperSqmExpression; import org.hibernate.query.sqm.tree.expression.JpaCriteriaParameter; import org.hibernate.query.sqm.tree.expression.SqmAny; import org.hibernate.query.sqm.tree.expression.SqmAnyDiscriminatorValue; @@ -413,4 +414,5 @@ default T visitCorrelatedRoot(SqmCorrelatedRoot correlatedRoot){ T visitFullyQualifiedClass(Class namedClass); + T visitAsWrapperExpression(AsWrapperSqmExpression expression); } diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmTreePrinter.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmTreePrinter.java index 7387060d07f4..e9bc3e800fbc 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmTreePrinter.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmTreePrinter.java @@ -33,6 +33,7 @@ import org.hibernate.query.sqm.tree.domain.SqmPluralPartJoin; import org.hibernate.query.sqm.tree.domain.SqmPluralValuedSimplePath; import org.hibernate.query.sqm.tree.domain.SqmTreatedPath; +import org.hibernate.query.sqm.tree.expression.AsWrapperSqmExpression; import org.hibernate.query.sqm.tree.expression.JpaCriteriaParameter; import org.hibernate.query.sqm.tree.expression.SqmAny; import org.hibernate.query.sqm.tree.expression.SqmAnyDiscriminatorValue; @@ -1133,6 +1134,11 @@ public Object visitFullyQualifiedClass(Class namedClass) { return null; } + @Override + public Object visitAsWrapperExpression(AsWrapperSqmExpression expression) { + return null; + } + @Override public Object visitModifiedSubQueryExpression(SqmModifiedSubQueryExpression expression) { return null; diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/spi/BaseSemanticQueryWalker.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/spi/BaseSemanticQueryWalker.java index 896c20f056cf..4293120efe4d 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/spi/BaseSemanticQueryWalker.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/spi/BaseSemanticQueryWalker.java @@ -33,6 +33,7 @@ import org.hibernate.query.sqm.tree.domain.SqmPluralPartJoin; import org.hibernate.query.sqm.tree.domain.SqmPluralValuedSimplePath; import org.hibernate.query.sqm.tree.domain.SqmTreatedPath; +import org.hibernate.query.sqm.tree.expression.AsWrapperSqmExpression; import org.hibernate.query.sqm.tree.expression.JpaCriteriaParameter; import org.hibernate.query.sqm.tree.expression.SqmAggregateFunction; import org.hibernate.query.sqm.tree.expression.SqmAny; @@ -940,4 +941,9 @@ public Object visitFieldLiteral(SqmFieldLiteral sqmFieldLiteral) { return sqmFieldLiteral; } + @Override + public Object visitAsWrapperExpression(AsWrapperSqmExpression expression) { + expression.getExpression().accept( this ); + return expression; + } } diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/BaseSqmToSqlAstConverter.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/BaseSqmToSqlAstConverter.java index c0e75d11fe9c..f1bc084d406f 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/BaseSqmToSqlAstConverter.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/BaseSqmToSqlAstConverter.java @@ -148,6 +148,7 @@ import org.hibernate.query.sqm.produce.function.internal.PatternRenderer; import org.hibernate.query.sqm.spi.BaseSemanticQueryWalker; import org.hibernate.query.sqm.sql.internal.AnyDiscriminatorPathInterpretation; +import org.hibernate.query.sqm.sql.internal.AsWrappedExpression; import org.hibernate.query.sqm.sql.internal.BasicValuedPathInterpretation; import org.hibernate.query.sqm.sql.internal.DiscriminatedAssociationPathInterpretation; import org.hibernate.query.sqm.sql.internal.DiscriminatorPathInterpretation; @@ -192,6 +193,7 @@ import org.hibernate.query.sqm.tree.domain.SqmPluralValuedSimplePath; import org.hibernate.query.sqm.tree.domain.SqmSimplePath; import org.hibernate.query.sqm.tree.domain.SqmTreatedPath; +import org.hibernate.query.sqm.tree.expression.AsWrapperSqmExpression; import org.hibernate.query.sqm.tree.expression.Conversion; import org.hibernate.query.sqm.tree.expression.JpaCriteriaParameter; import org.hibernate.query.sqm.tree.expression.SqmAliasedNodeRef; @@ -7836,6 +7838,14 @@ public Object visitFullyQualifiedClass(Class namedClass) { // .getOrMakeJavaDescriptor( namedClass ); } + @Override + public Object visitAsWrapperExpression(AsWrapperSqmExpression sqmExpression) { + return new AsWrappedExpression<>( + (Expression) sqmExpression.getExpression().accept( this ), + sqmExpression.getNodeType() + ); + } + @Override public Fetch visitIdentifierFetch(EntityResultGraphNode fetchParent) { final EntityIdentifierMapping identifierMapping = fetchParent.getReferencedMappingContainer() diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/internal/AsWrappedExpression.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/internal/AsWrappedExpression.java new file mode 100644 index 000000000000..6ddc4edaddf3 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/internal/AsWrappedExpression.java @@ -0,0 +1,102 @@ +/* + * 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.query.sqm.sql.internal; + +import org.hibernate.metamodel.mapping.JdbcMappingContainer; +import org.hibernate.sql.ast.SqlAstWalker; +import org.hibernate.sql.ast.spi.SqlAstCreationState; +import org.hibernate.sql.ast.spi.SqlSelection; +import org.hibernate.sql.ast.tree.expression.ColumnReference; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.sql.results.graph.DomainResult; +import org.hibernate.sql.results.graph.DomainResultCreationState; +import org.hibernate.sql.results.graph.basic.BasicResult; +import org.hibernate.type.BasicType; +import org.hibernate.type.descriptor.java.JavaType; +import org.hibernate.type.spi.TypeConfiguration; + +public class AsWrappedExpression implements Expression, DomainResultProducer { + private final Expression wrappedExpression; + private final BasicType expressionType; + + public AsWrappedExpression(Expression wrappedExpression, BasicType expressionType) { + assert wrappedExpression instanceof DomainResultProducer : "AsWrappedExpression expected to be an instance of DomainResultProducer"; + this.wrappedExpression = wrappedExpression; + this.expressionType = expressionType; + } + + @Override + public JdbcMappingContainer getExpressionType() { + return expressionType; + } + + @Override + public ColumnReference getColumnReference() { + return wrappedExpression.getColumnReference(); + } + + @Override + public SqlSelection createSqlSelection( + int jdbcPosition, + int valuesArrayPosition, + JavaType javaType, + boolean virtual, + TypeConfiguration typeConfiguration) { + return wrappedExpression.createSqlSelection( + jdbcPosition, + valuesArrayPosition, + javaType, + virtual, + typeConfiguration + ); + } + + @Override + public SqlSelection createDomainResultSqlSelection( + int jdbcPosition, + int valuesArrayPosition, + JavaType javaType, + boolean virtual, + TypeConfiguration typeConfiguration) { + return wrappedExpression.createDomainResultSqlSelection( + jdbcPosition, + valuesArrayPosition, + javaType, + virtual, + typeConfiguration + ); + } + + @Override + public void accept(SqlAstWalker sqlTreeWalker) { + wrappedExpression.accept( sqlTreeWalker ); + } + + @Override + public DomainResult createDomainResult(String resultVariable, DomainResultCreationState creationState) { + final SqlAstCreationState sqlAstCreationState = creationState.getSqlAstCreationState(); + final SqlSelection sqlSelection = sqlAstCreationState.getSqlExpressionResolver() + .resolveSqlSelection( + wrappedExpression, + wrappedExpression.getExpressionType().getSingleJdbcMapping().getJdbcJavaType(), + null, + sqlAstCreationState.getCreationContext() + .getMappingMetamodel().getTypeConfiguration() + ); + return new BasicResult<>( + sqlSelection.getValuesArrayPosition(), + resultVariable, + expressionType.getExpressibleJavaType() + ); + } + + @Override + public void applySqlSelections(DomainResultCreationState creationState) { + //noinspection unchecked + ( (DomainResultProducer) wrappedExpression ).applySqlSelections( creationState ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/SqmBasicValuedSimplePath.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/SqmBasicValuedSimplePath.java index c707111bded4..6a5e137a4c1e 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/SqmBasicValuedSimplePath.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/SqmBasicValuedSimplePath.java @@ -143,4 +143,9 @@ public DomainType getSqmType() { public X accept(SemanticQueryWalker walker) { return walker.visitBasicValuedPath( this ); } + + @Override + public JavaType getRelationalJavaType() { + return super.getExpressible().getRelationalJavaType(); + } } diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/SqmEmbeddedValuedSimplePath.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/SqmEmbeddedValuedSimplePath.java index d63522739328..f1fec2d73b68 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/SqmEmbeddedValuedSimplePath.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/SqmEmbeddedValuedSimplePath.java @@ -119,4 +119,9 @@ public Class getJavaType() { public Class getBindableJavaType() { return getJavaType(); } + + @Override + public JavaType getRelationalJavaType() { + return super.getExpressible().getRelationalJavaType(); + } } diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/AbstractSqmExpression.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/AbstractSqmExpression.java index 8e3afe48ecdf..be940221b8a4 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/AbstractSqmExpression.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/AbstractSqmExpression.java @@ -19,6 +19,7 @@ import org.hibernate.query.sqm.tree.jpa.AbstractJpaSelection; import org.hibernate.query.sqm.tree.predicate.SqmInPredicate; import org.hibernate.query.sqm.tree.predicate.SqmPredicate; +import org.hibernate.type.BasicType; import org.hibernate.type.descriptor.java.JavaType; import jakarta.persistence.criteria.Expression; @@ -102,7 +103,11 @@ public SqmExpression asString() { @Override public SqmExpression as(Class type) { - return nodeBuilder().cast(this, type); + final BasicType basicTypeForJavaType = nodeBuilder().getTypeConfiguration().getBasicTypeForJavaType( type ); + if ( basicTypeForJavaType == null ) { + throw new IllegalArgumentException( "Can't cast expression to unknown type: " + type.getCanonicalName() ); + } + return new AsWrapperSqmExpression<>( basicTypeForJavaType, this ); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/AsWrapperSqmExpression.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/AsWrapperSqmExpression.java new file mode 100644 index 000000000000..3be4774fcbe2 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/AsWrapperSqmExpression.java @@ -0,0 +1,54 @@ +/* + * 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 . + */ +package org.hibernate.query.sqm.tree.expression; + +import org.hibernate.query.sqm.SemanticQueryWalker; +import org.hibernate.query.sqm.SqmExpressible; +import org.hibernate.query.sqm.tree.SqmCopyContext; +import org.hibernate.type.BasicType; + +public class AsWrapperSqmExpression extends AbstractSqmExpression { + private final SqmExpression expression; + + AsWrapperSqmExpression(SqmExpressible type, SqmExpression expression) { + super( type, expression.nodeBuilder() ); + this.expression = expression; + } + + @Override + public X accept(SemanticQueryWalker walker) { + return walker.visitAsWrapperExpression( this ); + } + + @Override + public void appendHqlString(StringBuilder sb) { + sb.append( "wrap(" ); + expression.appendHqlString( sb ); + sb.append( " as " ); + sb.append( getNodeType().getReturnedClass().getName() ); + sb.append( ")" ); + } + + @Override + public SqmExpression as(Class type) { + return expression.as( type ); + } + + @Override + public SqmExpression copy(SqmCopyContext context) { + return new AsWrapperSqmExpression<>( getExpressible(), expression.copy( context ) ); + } + + public SqmExpression getExpression() { + return expression; + } + + @Override + public BasicType getNodeType() { + return (BasicType) super.getNodeType(); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmExpression.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmExpression.java index e6a11eb415c7..f8e1ac1a1863 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmExpression.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmExpression.java @@ -119,4 +119,9 @@ default SqmExpression castAs(DomainType type) { ); } + @Override + default SqmExpression cast(Class type) { + return castAs( nodeBuilder().getTypeConfiguration().getBasicTypeForJavaType( type ) ); + } + } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/criteria/basic/CastTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/criteria/basic/CastTest.java deleted file mode 100644 index e0de08dc0e58..000000000000 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/criteria/basic/CastTest.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * 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 . - */ -package org.hibernate.orm.test.jpa.criteria.basic; - -import java.math.BigDecimal; -import java.math.BigInteger; -import java.util.List; -import jakarta.persistence.criteria.CriteriaBuilder; -import jakarta.persistence.criteria.CriteriaQuery; -import jakarta.persistence.criteria.Root; - -import org.hibernate.dialect.DerbyDialect; -import org.hibernate.orm.test.jpa.metamodel.Product; -import org.hibernate.orm.test.jpa.metamodel.Product_; -import org.hibernate.orm.test.jpa.criteria.AbstractCriteriaTest; - -import org.hibernate.testing.SkipForDialect; -import org.hibernate.testing.TestForIssue; -import org.hibernate.testing.orm.junit.EntityManagerFactoryScope; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; - -public class CastTest extends AbstractCriteriaTest { - private static final int QUANTITY = 2; - - @AfterEach - public void tearDown(EntityManagerFactoryScope scope) { - scope.inTransaction( - entityManager -> { - entityManager.createQuery( "delete from Product" ).executeUpdate(); - } - ); - } - - @Test - @SkipForDialect(value = DerbyDialect.class, comment = "Derby does not support cast from INTEGER to VARCHAR") - @TestForIssue(jiraKey = "HHH-5755") - public void testCastToString(EntityManagerFactoryScope scope) { - scope.inTransaction( - entityManager -> { - Product product = new Product(); - product.setId( "product1" ); - product.setPrice( 1.23d ); - product.setQuantity( QUANTITY ); - product.setPartNumber( ( (long) Integer.MAX_VALUE ) + 1 ); - product.setRating( 1.999f ); - product.setSomeBigInteger( BigInteger.valueOf( 987654321 ) ); - product.setSomeBigDecimal( BigDecimal.valueOf( 987654.321 ) ); - entityManager.persist( product ); - } - ); - - scope.inTransaction( - entityManager -> { - CriteriaBuilder builder = entityManager.getCriteriaBuilder(); - CriteriaQuery criteria = builder.createQuery( Product.class ); - Root root = criteria.from( Product.class ); - criteria.where( builder.equal( - root.get( Product_.quantity ).as( String.class ), - builder.literal( String.valueOf( QUANTITY ) ) - ) ); - List result = entityManager.createQuery( criteria ).getResultList(); - Assertions.assertEquals( 1, result.size() ); - } - ); - } -} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/criteria/basic/CiteriaAsExpressionTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/criteria/basic/CiteriaAsExpressionTest.java new file mode 100644 index 000000000000..f4c26fc08127 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/criteria/basic/CiteriaAsExpressionTest.java @@ -0,0 +1,234 @@ +/* + * 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 . + */ +package org.hibernate.orm.test.jpa.criteria.basic; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.List; + +import org.hibernate.dialect.DerbyDialect; +import org.hibernate.dialect.SybaseASEDialect; +import org.hibernate.dialect.SybaseDialect; +import org.hibernate.orm.test.jpa.metamodel.Product; +import org.hibernate.orm.test.jpa.metamodel.Product_; +import org.hibernate.orm.test.jpa.metamodel.ShelfLife; + +import org.hibernate.testing.jdbc.SQLStatementInspector; +import org.hibernate.testing.orm.junit.EntityManagerFactoryScope; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.Jpa; +import org.hibernate.testing.orm.junit.SkipForDialect; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Convert; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.Root; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.junit.jupiter.api.Assertions.assertFalse; + +@Jpa(annotatedClasses = { + Product.class, + ShelfLife.class, + CiteriaAsExpressionTest.TestEntity.class +}, + useCollectingStatementInspector = true +) +public class CiteriaAsExpressionTest { + private static final int QUANTITY = 2; + private static final String NAME = "a"; + private static final Integer NAME_CONVERTED_VALUE = 1; + + @BeforeEach + public void setup(EntityManagerFactoryScope scope) { + scope.inTransaction( + entityManager -> { + Product product = new Product(); + product.setId( "product1" ); + product.setPrice( 1.23d ); + product.setQuantity( QUANTITY ); + product.setPartNumber( ( (long) Integer.MAX_VALUE ) + 1 ); + product.setRating( 1.999f ); + product.setSomeBigInteger( BigInteger.valueOf( 987654321 ) ); + product.setSomeBigDecimal( BigDecimal.valueOf( 987654.321 ) ); + entityManager.persist( product ); + + TestEntity testEntity = new TestEntity( 1, NAME ); + entityManager.persist( testEntity ); + } + ); + } + + @AfterEach + public void tearDown(EntityManagerFactoryScope scope) { + scope.inTransaction( + entityManager -> { + entityManager.createQuery( "delete from Product" ).executeUpdate(); + entityManager.createQuery( "delete from TestEntity" ).executeUpdate(); + } + ); + } + + @Test + @JiraKey("HHH-5755") + @SkipForDialect(dialectClass = DerbyDialect.class ) + @SkipForDialect(dialectClass = SybaseDialect.class ) + @SkipForDialect(dialectClass = SybaseASEDialect.class ) + public void testAsToString(EntityManagerFactoryScope scope) { + SQLStatementInspector statementInspector = (SQLStatementInspector) scope.getStatementInspector(); + statementInspector.clear(); + scope.inTransaction( + entityManager -> { + CriteriaBuilder builder = entityManager.getCriteriaBuilder(); + CriteriaQuery criteria = builder.createQuery( Product.class ); + Root root = criteria.from( Product.class ); + criteria.where( builder.equal( + root.get( Product_.quantity ).as( String.class ), + builder.literal( String.valueOf( QUANTITY ) ) + ) ); + List result = entityManager.createQuery( criteria ).getResultList(); + Assertions.assertEquals( 1, result.size() ); + + assertExecuteQueryDoesNotContainACast( statementInspector ); + } + ); + } + + @Test + @JiraKey("HHH-15713") + public void testAsIntegerToIntegreDoesNotCreateACast(EntityManagerFactoryScope scope) { + SQLStatementInspector statementInspector = (SQLStatementInspector) scope.getStatementInspector(); + statementInspector.clear(); + scope.inTransaction( + entityManager -> { + CriteriaBuilder builder = entityManager.getCriteriaBuilder(); + CriteriaQuery criteria = builder.createQuery( Product.class ); + Root root = criteria.from( Product.class ); + criteria.where( builder.equal( + root.get( Product_.quantity ).as( Integer.class ), + builder.literal( QUANTITY ) + ) + ); + List result = entityManager.createQuery( criteria ).getResultList(); + Assertions.assertEquals( 1, result.size() ); + + assertExecuteQueryDoesNotContainACast( statementInspector ); + } + ); + } + + @Test + @JiraKey("HHH-15725") + public void testAsAndConvertedAttribute(EntityManagerFactoryScope scope) { + SQLStatementInspector statementInspector = (SQLStatementInspector) scope.getStatementInspector(); + statementInspector.clear(); + scope.inTransaction( + entityManager -> { + CriteriaBuilder builder = entityManager.getCriteriaBuilder(); + CriteriaQuery criteria = builder.createQuery( TestEntity.class ); + Root root = criteria.from( TestEntity.class ); + criteria.where( builder.equal( + root.get( "name" ).as( Integer.class ), + builder.literal( NAME_CONVERTED_VALUE ) + ) + ); + List result = entityManager.createQuery( criteria ).getResultList(); + Assertions.assertEquals( 1, result.size() ); + + assertExecuteQueryDoesNotContainACast( statementInspector ); + } + ); + } + + @Test + @JiraKey("HHH-15725") + public void testAsAndConvertedAttribute2(EntityManagerFactoryScope scope) { + SQLStatementInspector statementInspector = (SQLStatementInspector) scope.getStatementInspector(); + statementInspector.clear(); + + scope.inTransaction( + entityManager -> { + CriteriaBuilder builder = entityManager.getCriteriaBuilder(); + CriteriaQuery criteria = builder.createQuery( Integer.class ); + Root root = criteria.from( TestEntity.class ); + criteria.select( root.get( "name" ).as( Integer.class ) ); + List result = entityManager.createQuery( criteria ).getResultList(); + Assertions.assertEquals( 1, result.size() ); + Integer actual = result.get( 0 ); + // When Expression.as() is used the Converted is not applied + + Assertions.assertEquals( 1, actual ); + + assertExecuteQueryDoesNotContainACast( statementInspector ); + } + ); + } + + + private static void assertExecuteQueryDoesNotContainACast(SQLStatementInspector statementInspector) { + assertFalse( getExecutedQuery( statementInspector ).contains( "cast" ) ); + } + + private static String getExecutedQuery(SQLStatementInspector statementInspector) { + List sqlQueries = statementInspector.getSqlQueries(); + assertThat( sqlQueries.size() ).isEqualTo( 1 ); + + String executedQuery = sqlQueries.get( 0 ); + return executedQuery; + } + + @Entity(name = "TestEntity") + public static class TestEntity { + @Id + private Integer id; + + @Convert(converter = TestConverter.class) + private String name; + + public TestEntity() { + } + + public TestEntity(Integer id, String name) { + this.id = id; + this.name = name; + } + + public Integer getId() { + return id; + } + + public String getName() { + return name; + } + } + + private static class TestConverter implements AttributeConverter { + @Override + public Integer convertToDatabaseColumn(String attribute) { + if ( attribute.equals( NAME ) ) { + return NAME_CONVERTED_VALUE; + } + return 0; + } + + @Override + public String convertToEntityAttribute(Integer dbData) { + if ( dbData == 1 ) { + return "a"; + } + return "b"; + } + } + +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/criteria/CriteriaBuilderNonStandardFunctionsTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/criteria/CriteriaBuilderNonStandardFunctionsTest.java index c7c569d38fba..6cd3d4f9f9f4 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/query/criteria/CriteriaBuilderNonStandardFunctionsTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/criteria/CriteriaBuilderNonStandardFunctionsTest.java @@ -20,6 +20,7 @@ import org.hibernate.dialect.DerbyDialect; import org.hibernate.dialect.PostgreSQLDialect; import org.hibernate.query.criteria.HibernateCriteriaBuilder; +import org.hibernate.query.criteria.JpaExpression; import org.hibernate.query.sqm.TemporalUnit; import org.hibernate.testing.orm.domain.StandardDomainModel; @@ -237,7 +238,7 @@ public void testOverlay(SessionFactoryScope scope) { Expression theString = from.get( "theString" ); query.multiselect( cb.overlay( theString, "33", 6 ), - cb.overlay( theString, from.get( "theInt" ).as( String.class ), 6 ), + cb.overlay( theString, ( (JpaExpression) from.get( "theInt" ) ).cast( String.class ), 6 ), cb.overlay( theString, "1234", from.get( "theInteger" ), 2 ) ).where( cb.equal( from.get( "id" ), 4 ) ); @@ -300,7 +301,7 @@ public void testReplace(SessionFactoryScope scope) { Expression theString = from.get( "theString" ); query.multiselect( cb.replace( theString, "thi", "12345" ), - cb.replace( theString, "t", from.get( "theInteger" ).as( String.class ) ) + cb.replace( theString, "t", ( (JpaExpression) from.get( "theInteger" ) ).cast( String.class ) ) ).where( cb.equal( from.get( "id" ), 4 ) ); Tuple result = session.createQuery( query ).getSingleResult(); diff --git a/migration-guide.adoc b/migration-guide.adoc index f1aab5a5744c..270042b2bc1d 100644 --- a/migration-guide.adoc +++ b/migration-guide.adoc @@ -429,5 +429,18 @@ session.createNativeQuery( .getResultList(); ``` +[[criteria-query]] +== Criteria: `jakarta.persistence.criteria.Expression#as(Class)` + +The behaviour of `jakarta.persistence.criteria.Expression#as(Class)` has been changed to conform to the Jakarta Persistence specification. + +`Expression.as()` doesn’t do anymore a real type conversions, it’s just an unsafe typecast on the Expression object itself. + +In order to perform an actual typecast, `org.hibernate.query.criteria.JpaExpression#cast(Class)` can be used. + +E.g. +``` +( (JpaExpression) from.get( "theInt" ) ).cast( String.class ) +```