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 be7d75e4f289..23569230db92 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 @@ -301,4 +301,9 @@ public BindableType getBindableType() { public SqmPath createSqmPath(SqmPath lhs, SqmPathSource intermediatePathSource) { return sqmPathSource.createSqmPath( lhs, intermediatePathSource ); } + + @Override + public JavaType getRelationalJavaType() { + return sqmPathSource.getRelationalJavaType(); + } } 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 b9fdb7f5ab0f..5e46888cca37 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 @@ -56,4 +56,6 @@ public interface JpaExpression extends JpaSelection, Expression { JpaPredicate equalTo(Expression that); JpaPredicate equalTo(T that); + + 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 b1be2ba3a4d7..60fdb465ae59 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 @@ -45,6 +45,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; @@ -417,4 +418,6 @@ default T visitCorrelatedRoot(SqmCorrelatedRoot correlatedRoot){ T visitMapEntryFunction(SqmMapEntryReference function); 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 515a736951e3..03ba75d415fd 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 @@ -34,6 +34,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; @@ -1194,6 +1195,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 f5505c421ec5..b3121d1bc390 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 @@ -34,6 +34,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; @@ -981,4 +982,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 d1d1cd416c91..da3f5833c5a2 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 @@ -128,6 +128,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; @@ -174,6 +175,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; @@ -8268,6 +8270,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..d45b128bafd3 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/internal/AsWrappedExpression.java @@ -0,0 +1,106 @@ +/* + * 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(), + null, + null, + false, + false + ); + } + + @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 1543504bab45..0cbe86a6f013 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 @@ -197,4 +197,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 1c69a62461dc..2c07d79fa844 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 @@ -121,4 +121,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 b9eadf62c432..ffc38b02b740 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 @@ -10,13 +10,13 @@ import java.math.BigInteger; import java.util.Collection; -import org.hibernate.query.criteria.JpaSelection; import org.hibernate.query.sqm.NodeBuilder; import org.hibernate.query.sqm.SqmExpressible; import org.hibernate.query.sqm.SqmTreeCreationLogger; import org.hibernate.query.sqm.internal.SqmCriteriaNodeBuilder; import org.hibernate.query.sqm.tree.jpa.AbstractJpaSelection; 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; @@ -94,7 +94,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..d3be23caef20 --- /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().getReturnedClassName() ); + 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 8ed07fae2103..6146346185a1 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 @@ -125,4 +125,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 6a6795afef5f..41a8fa09b6cf 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 56197c85c26b..c2c366657569 100644 --- a/migration-guide.adoc +++ b/migration-guide.adoc @@ -93,4 +93,18 @@ You can find more details about embeddable inheritance in the dedicated link:{us [[h2-dialect]] == H2 database and bulk mutation strategy -With ORM 6.6 when a bulk mutation involves multiple tables, H2 dialect will make use of global temporary tables instead of local ones. \ No newline at end of file +With ORM 6.6 when a bulk mutation involves multiple tables, H2 dialect will make use of global temporary tables instead of local ones. + +[[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 ) +```