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: + *
    + *
  1. the annotation member {@link #applyInToManyFetch()}, and + *
  2. 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; + } +}