From 25993b95e7b817f33d61e869b3242402087352c4 Mon Sep 17 00:00:00 2001 From: Gavin King Date: Sat, 21 Jun 2025 10:32:51 +0200 Subject: [PATCH 1/5] HHH-19555 add tests showing things actually working nicely --- ...ToOneImplicitJoinTableRestrictionTest.java | 91 +++++++++++++++++++ .../ManyToOneImplicitJoinTableTest.java | 30 +++++- 2 files changed, 120 insertions(+), 1 deletion(-) create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/mapping/manytoone/jointable/ManyToOneImplicitJoinTableRestrictionTest.java 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..a692265f3829 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/manytoone/jointable/ManyToOneImplicitJoinTableRestrictionTest.java @@ -0,0 +1,91 @@ +/* + * 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.testing.orm.junit.EntityManagerFactoryScope; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.Jpa; +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}) +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 From edaa7fbae003b27627de850e12d2fbe6d2687927 Mon Sep 17 00:00:00 2001 From: Gavin King Date: Sat, 21 Jun 2025 10:59:48 +0200 Subject: [PATCH 2/5] HHH-19555 improve javadoc for SQLRestriction --- .../org/hibernate/annotations/SQLRestriction.java | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) 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")

From 363334691f2ad679d5328cba3b5cfdcdd20ca6a6 Mon Sep 17 00:00:00 2001
From: Jan Schatteman 
Date: Mon, 7 Jul 2025 14:33:07 +0200
Subject: [PATCH 3/5] HHH-19555 - disable test because Sybase doesn't offer
 support for upserts

Signed-off-by: Jan Schatteman 
---
 .../jointable/ManyToOneImplicitJoinTableRestrictionTest.java   | 3 +++
 1 file changed, 3 insertions(+)

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
index a692265f3829..d1daab59b9e8 100644
--- 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
@@ -9,9 +9,11 @@
 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;
@@ -21,6 +23,7 @@
 @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) {

From c9730549d4fa2a4f5612a547e4fd6af200779728 Mon Sep 17 00:00:00 2001
From: Gavin King 
Date: Tue, 7 Oct 2025 09:14:06 +0200
Subject: [PATCH 4/5] HHH-19555 improve tests

---
 ...ToOneImplicitJoinTableRestrictionTest.java | 31 ++++++++++++++++++-
 .../ManyToOneImplicitJoinTableTest.java       | 28 +++++++++++++++++
 2 files changed, 58 insertions(+), 1 deletion(-)

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
index d1daab59b9e8..ed6e3da54886 100644
--- 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
@@ -23,7 +23,8 @@
 @Jpa(annotatedClasses =
 		{ManyToOneImplicitJoinTableRestrictionTest.X.class,
 		ManyToOneImplicitJoinTableRestrictionTest.Y.class})
-@SkipForDialect(dialectClass = SybaseDialect.class, matchSubTypes = true, reason = "Sybase doesn't have support for upserts")
+@SkipForDialect(dialectClass = SybaseDialect.class, matchSubTypes = true,
+		reason = "Sybase doesn't have support for upserts")
 class ManyToOneImplicitJoinTableRestrictionTest {
 	@JiraKey("HHH-19555") @Test
 	void test(EntityManagerFactoryScope scope) {
@@ -47,6 +48,16 @@ void test(EntityManagerFactoryScope scope) {
 			var id = s.createNativeQuery( "select x_id from Y_X", long.class ).getSingleResult();
 			assertEquals( -1L, id );
 		} );
+		scope.inTransaction( s -> {
+			Y y =
+					s.createQuery( "from Y where id = ?1", Y.class )
+							.setParameter( 1, 0L )
+							.getSingleResult();
+			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();
@@ -63,6 +74,16 @@ void test(EntityManagerFactoryScope scope) {
 			var id = s.createNativeQuery( "select x_id from Y_X", long.class ).getSingleResult();
 			assertEquals( 1L, id );
 		} );
+		scope.inTransaction( s -> {
+			Y y =
+					s.createQuery( "from Y where id = ?1", Y.class )
+							.setParameter( 1, 0L )
+							.getSingleResult();
+			assertEquals("Gavin", y.name);
+			assertNotNull(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 );
 			y.x = null;
@@ -75,6 +96,14 @@ void test(EntityManagerFactoryScope scope) {
 			var id = s.createNativeQuery( "select x_id from Y_X", long.class ).getSingleResultOrNull();
 			assertNull( id );
 		} );
+		scope.inTransaction( s -> {
+			Y y =
+					s.createQuery( "from Y where id = ?1", Y.class )
+							.setParameter( 1, 0L )
+							.getSingleResult();
+			assertEquals("Gavin", y.name);
+			assertNull(y.x);
+		} );
 	}
 
 	@Entity(name="Y")
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 e9c0c44d77b2..4302f43666e3 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
@@ -38,6 +38,16 @@ void test(EntityManagerFactoryScope scope) {
 			assertNotNull(y.x);
 			assertEquals( 1L, y.x.id );
 		} );
+		scope.inTransaction( s -> {
+			Y y =
+					s.createQuery( "from Y where id = ?1", Y.class )
+							.setParameter( 1, 0L )
+							.getSingleResult();
+			assertEquals("Gavin", y.name);
+			assertNotNull(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();
@@ -52,6 +62,16 @@ void test(EntityManagerFactoryScope scope) {
 			assertNotNull(y.x);
 			assertEquals( -1L, y.x.id );
 		} );
+		scope.inTransaction( s -> {
+			Y y =
+					s.createQuery( "from Y where id = ?1", Y.class )
+							.setParameter( 1, 0L )
+							.getSingleResult();
+			assertEquals("Gavin", y.name);
+			assertNotNull(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 );
 			y.x = null;
@@ -62,6 +82,14 @@ void test(EntityManagerFactoryScope scope) {
 			assertEquals("Gavin", y.name);
 			assertNull(y.x);
 		} );
+		scope.inTransaction( s -> {
+			Y y =
+					s.createQuery( "from Y where id = ?1", Y.class )
+							.setParameter( 1, 0L )
+							.getSingleResult();
+			assertEquals("Gavin", y.name);
+			assertNull(y.x);
+		} );
 	}
 
 	@Entity(name="Y")

From 3a79ba2c1a01174c479d59186b94ccd867b7f1e1 Mon Sep 17 00:00:00 2001
From: Gavin King 
Date: Tue, 7 Oct 2025 09:19:47 +0200
Subject: [PATCH 5/5] HHH-19555 add a @FailureExpected test for lazy fetching

---
 ...eLazyImplicitJoinTableRestrictionTest.java | 129 ++++++++++++++++++
 1 file changed, 129 insertions(+)
 create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/mapping/manytoone/jointable/ManyToOneLazyImplicitJoinTableRestrictionTest.java

diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/manytoone/jointable/ManyToOneLazyImplicitJoinTableRestrictionTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/manytoone/jointable/ManyToOneLazyImplicitJoinTableRestrictionTest.java
new file mode 100644
index 000000000000..e49199389fd8
--- /dev/null
+++ b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/manytoone/jointable/ManyToOneLazyImplicitJoinTableRestrictionTest.java
@@ -0,0 +1,129 @@
+/*
+ * 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.FetchType;
+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.FailureExpected;
+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 =
+		{ManyToOneLazyImplicitJoinTableRestrictionTest.X.class,
+		ManyToOneLazyImplicitJoinTableRestrictionTest.Y.class})
+@SkipForDialect(dialectClass = SybaseDialect.class, matchSubTypes = true,
+		reason = "Sybase doesn't have support for upserts")
+@FailureExpected(jiraKey = "HHH-19555",
+		// need to fix this using @ConcreteProxy-style lookahead
+		reason = "restriction is not applied until entity is actually fetched")
+class ManyToOneLazyImplicitJoinTableRestrictionTest {
+	@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.createQuery( "from Y where id = ?1", Y.class )
+							.setParameter( 1, 0L )
+							.getSingleResult();
+			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.createQuery( "from Y where id = ?1", Y.class )
+							.setParameter( 1, 0L )
+							.getSingleResult();
+			assertEquals("Gavin", y.name);
+			assertNotNull(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 );
+			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 );
+		} );
+		scope.inTransaction( s -> {
+			Y y =
+					s.createQuery( "from Y where id = ?1", Y.class )
+							.setParameter( 1, 0L )
+							.getSingleResult();
+			assertEquals("Gavin", y.name);
+			assertNull(y.x);
+		} );
+	}
+
+	@Entity(name="Y")
+	static class Y {
+		@Id
+		long id;
+		String name;
+		@JoinTable
+		@ManyToOne(fetch = FetchType.LAZY)
+		X x;
+	}
+	@Entity(name="X")
+	@SQLRestriction("id>0")
+	static class X {
+		@Id
+		long id;
+	}
+}