diff --git a/hibernate-core/src/main/java/org/hibernate/annotations/Where.java b/hibernate-core/src/main/java/org/hibernate/annotations/Where.java
index a318e8e0a0..486694eec9 100644
--- a/hibernate-core/src/main/java/org/hibernate/annotations/Where.java
+++ b/hibernate-core/src/main/java/org/hibernate/annotations/Where.java
@@ -18,14 +18,39 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME;
* Specifies a restriction written in native SQL to add to the generated
* SQL when querying an entity or collection.
*
- * For example, {@code @Where("deleted = false")} could be used to hide
- * entity instances which have been soft-deleted.
- *
- * Note that {@code Where} restrictions are always applied and cannot be
- * disabled. They're therefore much less flexible than {@link Filter filters}.
+ * For example, {@code @Where} could be used to hide entity instances which
+ * have been soft-deleted, either for the entity class itself:
+ *
{@code
+ * @Entity
+ * @Where(clause = "status <> 'DELETED'")
+ * class Document {
+ * ...
+ * @Enumerated(STRING)
+ * Status status;
+ * ...
+ * }
+ * }
+ * or, at the level of an association to the entity:
+ * {@code
+ * @OneToMany(mappedBy = "owner")
+ * @Where(clause = "status <> 'DELETED'")
+ * List documents;
+ * }
+ * By default, {@code @Where} restrictions declared for an entity are not
+ * applied when loading a collection of that entity type. This behavior is
+ * controlled by:
+ *
+ * - the annotation member {@link #applyInToManyFetch()}, and
+ *
- the configuration property
+ * {@value org.hibernate.cfg.AvailableSettings#USE_ENTITY_WHERE_CLAUSE_FOR_COLLECTIONS}.
+ *
+ * Note that {@code @Where} restrictions are always applied and cannot be
+ * disabled. Nor may they be parameterized. They're therefore much
+ * less flexible than {@linkplain Filter filters}.
*
* @see Filter
* @see DialectOverride.Where
+ * @see org.hibernate.cfg.AvailableSettings#USE_ENTITY_WHERE_CLAUSE_FOR_COLLECTIONS
*
* @author Emmanuel Bernard
*/
@@ -36,4 +61,21 @@ public @interface Where {
* A predicate, written in native SQL.
*/
String clause();
+
+ /**
+ * If this restriction applies to an entity type, should it also be
+ * applied when fetching a {@link jakarta.persistence.OneToMany} or
+ * {@link jakarta.persistence.ManyToOne} association that targets
+ * the entity type?
+ *
+ * By default, the restriction is not applied unless the property
+ * {@value org.hibernate.cfg.AvailableSettings#USE_ENTITY_WHERE_CLAUSE_FOR_COLLECTIONS}
+ * is explicitly enabled.
+ *
+ * @return {@code true} if the restriction should be applied even
+ * if the configuration property is not enabled
+ *
+ * @since 6.2
+ */
+ boolean applyInToManyFetch() default false;
}
diff --git a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/CollectionBinder.java b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/CollectionBinder.java
index 1a9a2bee33..962ac4cb00 100644
--- a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/CollectionBinder.java
+++ b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/CollectionBinder.java
@@ -1752,9 +1752,12 @@ public abstract class CollectionBinder {
}
private String getWhereOnClassClause() {
- if ( useEntityWhereClauseForCollections() && property.getElementClass() != null ) {
+ if ( property.getElementClass() != null ) {
final Where whereOnClass = getOverridableAnnotation( property.getElementClass(), Where.class, getBuildingContext() );
- return whereOnClass != null ? whereOnClass.clause() : null;
+ return whereOnClass != null
+ && ( whereOnClass.applyInToManyFetch() || useEntityWhereClauseForCollections() )
+ ? whereOnClass.clause()
+ : null;
}
else {
return null;
diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/where/annotations/EagerToManyWhereUseClassWhereViaAnnotationTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/where/annotations/EagerToManyWhereUseClassWhereViaAnnotationTest.java
new file mode 100644
index 0000000000..d23daeabc3
--- /dev/null
+++ b/hibernate-core/src/test/java/org/hibernate/orm/test/where/annotations/EagerToManyWhereUseClassWhereViaAnnotationTest.java
@@ -0,0 +1,212 @@
+/*
+ * Hibernate, Relational Persistence for Idiomatic Java
+ *
+ * License: GNU Lesser General Public License (LGPL), version 2.1 or later.
+ * See the lgpl.txt file in the root directory or .
+ */
+package org.hibernate.orm.test.where.annotations;
+
+import jakarta.persistence.Entity;
+import jakarta.persistence.FetchType;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.Id;
+import jakarta.persistence.JoinColumn;
+import jakarta.persistence.JoinTable;
+import jakarta.persistence.ManyToMany;
+import jakarta.persistence.OneToMany;
+import jakarta.persistence.Table;
+import org.hibernate.annotations.Where;
+import org.hibernate.annotations.WhereJoinTable;
+import org.hibernate.cfg.AvailableSettings;
+import org.hibernate.testing.TestForIssue;
+import org.hibernate.testing.junit4.BaseNonConfigCoreFunctionalTestCase;
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import static org.hibernate.testing.transaction.TransactionUtil.doInHibernate;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Tests association collections with AvailableSettings.USE_ENTITY_WHERE_CLAUSE_FOR_COLLECTIONS = true
+ *
+ * @author Gail Badner
+ */
+public class EagerToManyWhereUseClassWhereViaAnnotationTest extends BaseNonConfigCoreFunctionalTestCase {
+
+ @Override
+ protected Class[] getAnnotatedClasses() {
+ return new Class[] { Product.class, Category.class };
+ }
+
+ @Override
+ protected void addSettings(Map settings) {
+ settings.put( AvailableSettings.USE_ENTITY_WHERE_CLAUSE_FOR_COLLECTIONS, "false" );
+ }
+
+ @Test
+ @TestForIssue( jiraKey = "HHH-15936" )
+ public void testAssociatedWhereClause() {
+
+ Product product = new Product();
+ Category flowers = new Category();
+ flowers.id = 1;
+ flowers.name = "flowers";
+ flowers.description = "FLOWERS";
+ product.categoriesOneToMany.add( flowers );
+ product.categoriesWithDescOneToMany.add( flowers );
+ product.categoriesManyToMany.add( flowers );
+ product.categoriesWithDescManyToMany.add( flowers );
+ product.categoriesWithDescIdLt4ManyToMany.add( flowers );
+ Category vegetables = new Category();
+ vegetables.id = 2;
+ vegetables.name = "vegetables";
+ vegetables.description = "VEGETABLES";
+ product.categoriesOneToMany.add( vegetables );
+ product.categoriesWithDescOneToMany.add( vegetables );
+ product.categoriesManyToMany.add( vegetables );
+ product.categoriesWithDescManyToMany.add( vegetables );
+ product.categoriesWithDescIdLt4ManyToMany.add( vegetables );
+ Category dogs = new Category();
+ dogs.id = 3;
+ dogs.name = "dogs";
+ dogs.description = null;
+ product.categoriesOneToMany.add( dogs );
+ product.categoriesWithDescOneToMany.add( dogs );
+ product.categoriesManyToMany.add( dogs );
+ product.categoriesWithDescManyToMany.add( dogs );
+ product.categoriesWithDescIdLt4ManyToMany.add( dogs );
+ Category building = new Category();
+ building.id = 4;
+ building.name = "building";
+ building.description = "BUILDING";
+ product.categoriesOneToMany.add( building );
+ product.categoriesWithDescOneToMany.add( building );
+ product.categoriesManyToMany.add( building );
+ product.categoriesWithDescManyToMany.add( building );
+ product.categoriesWithDescIdLt4ManyToMany.add( building );
+
+ doInHibernate(
+ this::sessionFactory,
+ session -> {
+ session.persist( flowers );
+ session.persist( vegetables );
+ session.persist( dogs );
+ session.persist( building );
+ session.persist( product );
+ }
+ );
+
+ doInHibernate(
+ this::sessionFactory,
+ session -> {
+ Product p = session.get( Product.class, product.id );
+ assertNotNull( p );
+ assertEquals( 4, p.categoriesOneToMany.size() );
+ checkIds( p.categoriesOneToMany, new Integer[] { 1, 2, 3, 4 } );
+ assertEquals( 3, p.categoriesWithDescOneToMany.size() );
+ checkIds( p.categoriesWithDescOneToMany, new Integer[] { 1, 2, 4 } );
+ assertEquals( 4, p.categoriesManyToMany.size() );
+ checkIds( p.categoriesManyToMany, new Integer[] { 1, 2, 3, 4 } );
+ assertEquals( 3, p.categoriesWithDescManyToMany.size() );
+ checkIds( p.categoriesWithDescManyToMany, new Integer[] { 1, 2, 4 } );
+ assertEquals( 2, p.categoriesWithDescIdLt4ManyToMany.size() );
+ checkIds( p.categoriesWithDescIdLt4ManyToMany, new Integer[] { 1, 2 } );
+ }
+ );
+
+ doInHibernate(
+ this::sessionFactory,
+ session -> {
+ Category c = session.get( Category.class, flowers.id );
+ assertNotNull( c );
+ c.inactive = 1;
+ }
+ );
+
+ doInHibernate(
+ this::sessionFactory,
+ session -> {
+ Category c = session.get( Category.class, flowers.id );
+ assertNull( c );
+ }
+ );
+
+ doInHibernate(
+ this::sessionFactory,
+ session -> {
+ Product p = session.get( Product.class, product.id );
+ assertNotNull( p );
+ assertEquals( 3, p.categoriesOneToMany.size() );
+ checkIds( p.categoriesOneToMany, new Integer[] { 2, 3, 4 } );
+ assertEquals( 2, p.categoriesWithDescOneToMany.size() );
+ checkIds( p.categoriesWithDescOneToMany, new Integer[] { 2, 4 } );
+ assertEquals( 3, p.categoriesManyToMany.size() );
+ checkIds( p.categoriesManyToMany, new Integer[] { 2, 3, 4 } );
+ assertEquals( 2, p.categoriesWithDescManyToMany.size() );
+ checkIds( p.categoriesWithDescManyToMany, new Integer[] { 2, 4 } );
+ assertEquals( 1, p.categoriesWithDescIdLt4ManyToMany.size() );
+ checkIds( p.categoriesWithDescIdLt4ManyToMany, new Integer[] { 2 } );
+ }
+ );
+ }
+
+ private void checkIds(Set categories, Integer[] expectedIds) {
+ final Set expectedIdSet = new HashSet<>( Arrays.asList( expectedIds ) );
+ for ( Category category : categories ) {
+ expectedIdSet.remove( category.id );
+ }
+ assertTrue( expectedIdSet.isEmpty() );
+ }
+
+ @Entity(name = "Product")
+ public static class Product {
+ @Id
+ @GeneratedValue
+ private int id;
+
+ @OneToMany(fetch = FetchType.EAGER)
+ @JoinColumn
+ private Set categoriesOneToMany = new HashSet<>();
+
+ @OneToMany(fetch = FetchType.EAGER)
+ @JoinColumn
+ @Where( clause = "description is not null" )
+ private Set categoriesWithDescOneToMany = new HashSet<>();
+
+ @ManyToMany(fetch = FetchType.EAGER)
+ @JoinTable(name = "categoriesManyToMany")
+ private Set categoriesManyToMany = new HashSet<>();
+
+ @ManyToMany(fetch = FetchType.EAGER)
+ @JoinTable(name = "categoriesWithDescManyToMany", inverseJoinColumns = { @JoinColumn( name = "categoryId" )})
+ @Where( clause = "description is not null" )
+ private Set categoriesWithDescManyToMany = new HashSet<>();
+
+ @ManyToMany(fetch = FetchType.EAGER)
+ @JoinTable(name = "categoriesWithDescIdLt4MToM", inverseJoinColumns = { @JoinColumn( name = "categoryId" )})
+ @Where( clause = "description is not null" )
+ @WhereJoinTable( clause = "categoryId < 4")
+ private Set categoriesWithDescIdLt4ManyToMany = new HashSet<>();
+ }
+
+ @Entity(name = "Category")
+ @Table(name = "CATEGORY")
+ @Where(clause = "inactive = 0", applyInToManyFetch = true)
+ public static class Category {
+ @Id
+ private int id;
+
+ private String name;
+
+ private String description;
+
+ private int inactive;
+ }
+}