diff --git a/hibernate-core/src/main/java/org/hibernate/annotations/SQLRestriction.java b/hibernate-core/src/main/java/org/hibernate/annotations/SQLRestriction.java index 3d15fca6cf8c..ca36dc202e4f 100644 --- a/hibernate-core/src/main/java/org/hibernate/annotations/SQLRestriction.java +++ b/hibernate-core/src/main/java/org/hibernate/annotations/SQLRestriction.java @@ -37,8 +37,21 @@ * List<Document> documents; * *
+ * If a restriction declared by an entity should be applied to a to-one + * association to that entity type, the association should be mapped to + * an {@linkplain jakarta.persistence.JoinTable association table}. + *
+ * @ManyToOne + * @JoinTable(name = "application_document") + * Document document; + *+ * The {@code SQLRestriction} annotation may not be directly applied to + * a field or property annotated {@link jakarta.persistence.OneToOne} or + * {@link jakarta.persistence.ManyToOne}, and restrictions on foreign + * key associations are dangerous. + *
* The {@link SQLJoinTableRestriction} annotation lets a restriction be - * applied to an {@linkplain jakarta.persistence.JoinTable association table}: + * applied to the columns of an association table: *
* @ManyToMany * @JoinTable(name = "collaborations") diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/manytoone/jointable/ManyToOneImplicitJoinTableRestrictionTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/manytoone/jointable/ManyToOneImplicitJoinTableRestrictionTest.java new file mode 100644 index 000000000000..d1daab59b9e8 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/manytoone/jointable/ManyToOneImplicitJoinTableRestrictionTest.java @@ -0,0 +1,94 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.mapping.manytoone.jointable; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.JoinTable; +import jakarta.persistence.ManyToOne; +import org.hibernate.annotations.SQLRestriction; +import org.hibernate.dialect.SybaseDialect; +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.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +@Jpa(annotatedClasses = + {ManyToOneImplicitJoinTableRestrictionTest.X.class, + ManyToOneImplicitJoinTableRestrictionTest.Y.class}) +@SkipForDialect(dialectClass = SybaseDialect.class, matchSubTypes = true, reason = "Sybase doesn't have support for upserts") +class ManyToOneImplicitJoinTableRestrictionTest { + @JiraKey("HHH-19555") @Test + void test(EntityManagerFactoryScope scope) { + scope.inTransaction( s -> { + X x = new X(); + Y y = new Y(); + x.id = -1; + y.x = x; + s.persist( x ); + s.persist( y ); + } ); + scope.inTransaction( s -> { + Y y = s.find( Y.class, 0L ); + y.name = "Gavin"; + assertNull(y.x); + } ); + scope.inTransaction( s -> { + Y y = s.find( Y.class, 0L ); + assertEquals("Gavin", y.name); + assertNull(y.x); + var id = s.createNativeQuery( "select x_id from Y_X", long.class ).getSingleResult(); + assertEquals( -1L, id ); + } ); + scope.inTransaction( s -> { + Y y = s.find( Y.class, 0L ); + X x = new X(); + x.id = 1; + s.persist( x ); + y.x = x; + // uses a SQL merge to update the join table + } ); + scope.inTransaction( s -> { + Y y = s.find( Y.class, 0L ); + assertEquals("Gavin", y.name); + assertNotNull(y.x); + assertEquals( 1L, y.x.id ); + var id = s.createNativeQuery( "select x_id from Y_X", long.class ).getSingleResult(); + assertEquals( 1L, id ); + } ); + scope.inTransaction( s -> { + Y y = s.find( Y.class, 0L ); + y.x = null; + // uses a SQL merge to update the join table + } ); + scope.inTransaction( s -> { + Y y = s.find( Y.class, 0L ); + assertEquals("Gavin", y.name); + assertNull(y.x); + var id = s.createNativeQuery( "select x_id from Y_X", long.class ).getSingleResultOrNull(); + assertNull( id ); + } ); + } + + @Entity(name="Y") + static class Y { + @Id + long id; + String name; + @JoinTable + @ManyToOne X x; + } + @Entity(name="X") + @SQLRestriction("id>0") + static class X { + @Id + long id; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/manytoone/jointable/ManyToOneImplicitJoinTableTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/manytoone/jointable/ManyToOneImplicitJoinTableTest.java index a3b486f76d17..e9c0c44d77b2 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/manytoone/jointable/ManyToOneImplicitJoinTableTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/manytoone/jointable/ManyToOneImplicitJoinTableTest.java @@ -11,7 +11,8 @@ import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.AssertionsKt.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; @Jpa(annotatedClasses = {ManyToOneImplicitJoinTableTest.X.class, @@ -21,6 +22,7 @@ class ManyToOneImplicitJoinTableTest { void test(EntityManagerFactoryScope scope) { scope.inTransaction( s -> { X x = new X(); + x.id = 1; Y y = new Y(); y.x = x; s.persist( x ); @@ -34,8 +36,34 @@ void test(EntityManagerFactoryScope scope) { Y y = s.find( Y.class, 0L ); assertEquals("Gavin", y.name); assertNotNull(y.x); + assertEquals( 1L, y.x.id ); + } ); + scope.inTransaction( s -> { + Y y = s.find( Y.class, 0L ); + X x = new X(); + x.id = -1; + s.persist( x ); + y.x = x; + // uses a SQL merge to update the join table + } ); + scope.inTransaction( s -> { + Y y = s.find( Y.class, 0L ); + assertEquals("Gavin", y.name); + assertNotNull(y.x); + assertEquals( -1L, y.x.id ); + } ); + scope.inTransaction( s -> { + Y y = s.find( Y.class, 0L ); + y.x = null; + // uses a SQL merge to update the join table + } ); + scope.inTransaction( s -> { + Y y = s.find( Y.class, 0L ); + assertEquals("Gavin", y.name); + assertNull(y.x); } ); } + @Entity(name="Y") static class Y { @Id