diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/AbstractSqmSelectionQuery.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/AbstractSqmSelectionQuery.java index 9f97c9e15889..3b3b0dabe489 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/AbstractSqmSelectionQuery.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/AbstractSqmSelectionQuery.java @@ -9,6 +9,7 @@ import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.query.spi.QueryParameterImplementor; import org.hibernate.query.sqm.tree.expression.JpaCriteriaParameter; +import org.hibernate.query.sqm.tree.expression.ValueBindJpaCriteriaParameter; import org.hibernate.type.BindableType; import org.hibernate.query.KeyedPage; import org.hibernate.query.KeyedResultList; @@ -149,15 +150,12 @@ protected static void bindValueBindCriteriaParameters( if ( sqmParameter instanceof SqmJpaCriteriaParameterWrapper wrapper ) { @SuppressWarnings("unchecked") final var criteriaParameter = (JpaCriteriaParameter) wrapper.getJpaCriteriaParameter(); - final var value = criteriaParameter.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 || criteriaParameter.getNodeType() == null ) { + if ( criteriaParameter instanceof ValueBindJpaCriteriaParameter ) { // Use the anticipated type for binding the value if possible //noinspection unchecked final var parameter = (QueryParameterImplementor) entry.getKey(); bindings.getBinding( parameter ) - .setBindValue( value, criteriaParameter.getAnticipatedType() ); + .setBindValue( criteriaParameter.getValue(), criteriaParameter.getAnticipatedType() ); } } } diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmCriteriaNodeBuilder.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmCriteriaNodeBuilder.java index 402b077c2ee8..55dd0d159137 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmCriteriaNodeBuilder.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmCriteriaNodeBuilder.java @@ -2146,7 +2146,10 @@ else if ( value instanceof SqmExpression ) { } private boolean isInstance(BindableType bindableType, T value) { - if ( bindableType instanceof SqmExpressible expressible ) { + if ( value == null ) { + return true; + } + else if ( bindableType instanceof SqmExpressible expressible ) { return expressible.getExpressibleJavaType().isInstance( value ); } else { diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/criteria/CriteriaUpdateAssociationSetNullValueTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/criteria/CriteriaUpdateAssociationSetNullValueTest.java new file mode 100644 index 000000000000..aa4e2583f5da --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/criteria/CriteriaUpdateAssociationSetNullValueTest.java @@ -0,0 +1,132 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.jpa.criteria; + +import jakarta.persistence.Basic; +import jakarta.persistence.CascadeType; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaUpdate; +import jakarta.persistence.criteria.Root; +import org.hibernate.SessionFactory; +import org.hibernate.testing.orm.junit.EntityManagerFactoryScope; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.Jpa; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + + +@Jpa( + annotatedClasses = { + CriteriaUpdateAssociationSetNullValueTest.Parent.class, + CriteriaUpdateAssociationSetNullValueTest.Child.class} +) +@JiraKey("HHH-19085") +public class CriteriaUpdateAssociationSetNullValueTest { + + private static final Long PARENT_ID = 1L; + + @BeforeEach + public void setUp(EntityManagerFactoryScope scope) { + scope.inTransaction( + em -> { + em.persist( new Parent( PARENT_ID, "Lionello", new Child( 2L, "Andrea" ) ) ); + } + + ); + } + + @AfterEach + public void tearDown(EntityManagerFactoryScope scope) { + scope.getEntityManagerFactory().unwrap( SessionFactory.class ).getSchemaManager().truncateMappedObjects(); + } + + @Test + void testUpdateSetAssociationToNullValue(EntityManagerFactoryScope scope) { + scope.inTransaction( + em -> { + CriteriaBuilder cb = em.getCriteriaBuilder(); + CriteriaUpdate update = cb.createCriteriaUpdate( Parent.class ); + Root msg = update.from( Parent.class ); + update.set( msg.get( "child" ), (Child) null ); + em.createQuery( update ).executeUpdate(); + } + ); + + scope.inTransaction( + em -> { + Parent parent = em.find( Parent.class, PARENT_ID ); + assertThat( parent ).isNotNull(); + assertThat( parent.getName() ).isNotNull(); + assertThat( parent.getChild() ).isNull(); + } + ); + } + + @Entity(name = "Parent") + public static class Parent { + + @Id + private Long id; + + @Basic + private String name; + + @ManyToOne(cascade = CascadeType.PERSIST) + private Child child; + + public Parent() { + } + + public Parent(Long id, String name, Child child) { + this.id = id; + this.name = name; + this.child = child; + } + + public Long getId() { + return id; + } + + public String getName() { + return name; + } + + public Child getChild() { + return child; + } + } + + @Entity(name = "Child") + public static class Child { + + @Id + private Long id; + + private String name; + + public Child() { + } + + public Child(Long id, String name) { + this.id = id; + this.name = name; + } + + public Long getId() { + return id; + } + + public String getName() { + return name; + } + } + +}