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