diff --git a/hibernate-core/src/main/java/org/hibernate/query/specification/internal/MutationSpecificationImpl.java b/hibernate-core/src/main/java/org/hibernate/query/specification/internal/MutationSpecificationImpl.java index a9db084bd2be..89ad2d9eb247 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/specification/internal/MutationSpecificationImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/query/specification/internal/MutationSpecificationImpl.java @@ -40,6 +40,7 @@ import java.util.function.BiConsumer; import static org.hibernate.query.sqm.tree.SqmCopyContext.noParamCopyContext; +import static org.hibernate.query.sqm.tree.SqmCopyContext.simpleContext; /** * Standard implementation of {@link MutationSpecification}. @@ -122,7 +123,7 @@ public MutationQuery createQuery(StatelessSession session) { public MutationQuery createQuery(SharedSessionContract session) { final var sessionImpl = session.unwrap(SharedSessionContractImplementor.class); final var sqmStatement = build( sessionImpl.getFactory().getQueryEngine() ); - return new SqmQueryImpl<>( sqmStatement, true, null, sessionImpl ); + return new SqmQueryImpl<>( sqmStatement, false, null, sessionImpl ); } private SqmDeleteOrUpdateStatement build(QueryEngine queryEngine) { @@ -133,7 +134,8 @@ private SqmDeleteOrUpdateStatement build(QueryEngine queryEngine) { mutationTargetRoot = resolveSqmRoot( sqmStatement, mutationTarget ); } else if ( deleteOrUpdateStatement != null ) { - sqmStatement = deleteOrUpdateStatement; + sqmStatement = (SqmDeleteOrUpdateStatement) deleteOrUpdateStatement + .copy( simpleContext() ); mutationTargetRoot = resolveSqmRoot( sqmStatement, sqmStatement.getTarget().getManagedType().getJavaType() ); } diff --git a/hibernate-core/src/main/java/org/hibernate/query/specification/internal/SelectionSpecificationImpl.java b/hibernate-core/src/main/java/org/hibernate/query/specification/internal/SelectionSpecificationImpl.java index 990085005ec6..cf9f98645b45 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/specification/internal/SelectionSpecificationImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/query/specification/internal/SelectionSpecificationImpl.java @@ -40,6 +40,7 @@ import static org.hibernate.internal.util.collections.CollectionHelper.isEmpty; import static org.hibernate.query.sqm.internal.SqmUtil.validateCriteriaQuery; import static org.hibernate.query.sqm.tree.SqmCopyContext.noParamCopyContext; +import static org.hibernate.query.sqm.tree.SqmCopyContext.simpleContext; /** * Standard implementation of {@link SelectionSpecification}. @@ -160,7 +161,7 @@ public SelectionQuery createQuery(StatelessSession session) { public SelectionQuery createQuery(SharedSessionContract session) { final var sessionImpl = session.unwrap(SharedSessionContractImplementor.class); final var sqmStatement = build( sessionImpl.getFactory().getQueryEngine() ); - return new SqmSelectionQueryImpl<>( sqmStatement, true, resultType, sessionImpl ); + return new SqmSelectionQueryImpl<>( sqmStatement, false, resultType, sessionImpl ); } private SqmSelectStatement build(QueryEngine queryEngine) { @@ -171,7 +172,7 @@ private SqmSelectStatement build(QueryEngine queryEngine) { sqmRoot = extractRoot( sqmStatement, resultType, hql ); } else if ( criteriaQuery != null ) { - sqmStatement = (SqmSelectStatement) criteriaQuery; + sqmStatement = ((SqmSelectStatement) criteriaQuery).copy( simpleContext() ); sqmRoot = extractRoot( sqmStatement, resultType, "criteria query" ); } else { diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/dynamic/BasicEntity.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/dynamic/BasicEntity.java index 0e5e9a2f4556..718da0242b1d 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/query/dynamic/BasicEntity.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/dynamic/BasicEntity.java @@ -22,4 +22,36 @@ public class BasicEntity { @ManyToOne OtherEntity other; + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public int getPosition() { + return position; + } + + public void setPosition(int position) { + this.position = position; + } + + public OtherEntity getOther() { + return other; + } + + public void setOther(OtherEntity other) { + this.other = other; + } } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/dynamic/SpecificationReuseTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/dynamic/SpecificationReuseTest.java new file mode 100644 index 000000000000..ebe1b8bd9992 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/dynamic/SpecificationReuseTest.java @@ -0,0 +1,175 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.query.dynamic; + +import java.util.function.Consumer; + +import org.hibernate.query.Order; +import org.hibernate.query.restriction.Restriction; +import org.hibernate.query.specification.MutationSpecification; +import org.hibernate.query.specification.SelectionSpecification; + +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +@DomainModel(annotatedClasses = { + BasicEntity.class, + OtherEntity.class, +}) +@SessionFactory(useCollectingStatementInspector = true) +@Jira( "https://hibernate.atlassian.net/browse/HHH-19781" ) +public class SpecificationReuseTest { + @Test + public void dynamicSelect(SessionFactoryScope scope) { + final var inspector = scope.getCollectingStatementInspector(); + scope.inTransaction( session -> { + final var spec = SelectionSpecification.create( BasicEntity.class ) + .sort( Order.asc( BasicEntity_.position ) ) + .restrict( Restriction.like( BasicEntity_.name, "entity_%" ) ); + nTimes( spec, 3, s -> { + inspector.clear(); + assertThat( s.createQuery( session ).list() ).extracting( BasicEntity::getId ).containsExactly( 2, 1 ); + inspector.assertNumberOfOccurrenceInQueryNoSpace( 0, "position", 2 ); + inspector.assertNumberOfOccurrenceInQueryNoSpace( 0, "name", 2 ); + } ); + } ); + } + + @Test + public void hqlSelect(SessionFactoryScope scope) { + final var inspector = scope.getCollectingStatementInspector(); + scope.inTransaction( session -> { + final var spec = SelectionSpecification.create( BasicEntity.class, "from BasicEntity" ) + .sort( Order.asc( BasicEntity_.position ) ) + .restrict( Restriction.like( BasicEntity_.name, "entity_%" ) ); + nTimes( spec, 3, s -> { + inspector.clear(); + assertThat( s.createQuery( session ).list() ).extracting( BasicEntity::getId ).containsExactly( 2, 1 ); + inspector.assertNumberOfOccurrenceInQueryNoSpace( 0, "position", 2 ); + inspector.assertNumberOfOccurrenceInQueryNoSpace( 0, "name", 2 ); + } ); + } ); + } + + @Test + public void criteriaSelect(SessionFactoryScope scope) { + final var inspector = scope.getCollectingStatementInspector(); + scope.inTransaction( session -> { + final var cb = session.getCriteriaBuilder(); + final var query = cb.createQuery( BasicEntity.class ); + final var root = query.from( BasicEntity.class ); + final var spec = SelectionSpecification.create( query.select( root ) ) + .sort( Order.asc( BasicEntity_.position ) ) + .restrict( Restriction.like( BasicEntity_.name, "entity_%" ) ); + nTimes( spec, 3, s -> { + inspector.clear(); + assertThat( s.createQuery( session ).list() ).extracting( BasicEntity::getId ).containsExactly( 2, 1 ); + inspector.assertNumberOfOccurrenceInQueryNoSpace( 0, "position", 2 ); + inspector.assertNumberOfOccurrenceInQueryNoSpace( 0, "name", 2 ); + } ); + } ); + } + + @Test + public void hqlMutation(SessionFactoryScope scope) { + final var inspector = scope.getCollectingStatementInspector(); + scope.inTransaction( session -> { + final var e = new BasicEntity(); + e.setId( 33 ); + session.persist( e ); + } ); + scope.inTransaction( session -> { + final var spec = MutationSpecification.create( BasicEntity.class, "update BasicEntity set name = 'entity_33'" ) + .restrict( Restriction.equal( BasicEntity_.id, 33 ) ); + nTimes( spec, 3, s -> { + inspector.clear(); + assertThat( s.createQuery( session ).executeUpdate() ).isEqualTo( 1 ); + inspector.assertNumberOfOccurrenceInQueryNoSpace( 0, "id", 1 ); + } ); + } ); + scope.inTransaction( session -> { + final var spec = MutationSpecification.create( BasicEntity.class, "delete BasicEntity" ) + .restrict( Restriction.equal( BasicEntity_.name, "entity_33" ) ); + nTimes( spec, 3, s -> { + inspector.clear(); + s.createQuery( session ).executeUpdate(); + inspector.assertNumberOfOccurrenceInQueryNoSpace( 0, "name", 1 ); + } ); + } ); + } + + @Test + public void criteriaMutation(SessionFactoryScope scope) { + final var inspector = scope.getCollectingStatementInspector(); + scope.inTransaction( session -> { + final var e = new BasicEntity(); + e.setId( 44 ); + session.persist( e ); + } ); + scope.inTransaction( session -> { + final var cb = session.getCriteriaBuilder(); + final var cu = cb.createCriteriaUpdate( BasicEntity.class ); + final var spec = MutationSpecification.create( cu.set( BasicEntity_.name, "entity_44" ) ) + .restrict( Restriction.equal( BasicEntity_.id, 44 ) ); + nTimes( spec, 3, s -> { + inspector.clear(); + assertThat( s.createQuery( session ).executeUpdate() ).isEqualTo( 1 ); + inspector.assertNumberOfOccurrenceInQueryNoSpace( 0, "id", 1 ); + } ); + } ); + scope.inTransaction( session -> { + final var cb = session.getCriteriaBuilder(); + final var cd = cb.createCriteriaDelete( BasicEntity.class ); + final var spec = MutationSpecification.create( cd ) + .restrict( Restriction.equal( BasicEntity_.name, "entity_44" ) ); + nTimes( spec, 3, s -> { + inspector.clear(); + s.createQuery( session ).executeUpdate(); + inspector.assertNumberOfOccurrenceInQueryNoSpace( 0, "name", 1 ); + } ); + } ); + } + + void nTimes(SelectionSpecification spec, int n, Consumer> consumer) { + for ( int i = 0; i < n; i++ ) { + consumer.accept( spec ); + } + } + + void nTimes(MutationSpecification spec, int n, Consumer> consumer) { + for ( int i = 0; i < n; i++ ) { + consumer.accept( spec ); + } + } + + @BeforeAll + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var e1 = new BasicEntity(); + e1.setId( 1 ); + e1.setName( "entity_1" ); + e1.setPosition( 99 ); + session.persist( e1 ); + + final var e2 = new BasicEntity(); + e2.setId( 2 ); + e2.setName( "entity_2" ); + e2.setPosition( 42 ); + session.persist( e2 ); + } ); + } + + @AfterAll + public void tearDown(SessionFactoryScope scope) { + scope.getSessionFactory().getSchemaManager().truncateMappedObjects(); + } +}