diff --git a/hibernate-core/src/main/java/org/hibernate/loader/MultipleBagFetchException.java b/hibernate-core/src/main/java/org/hibernate/loader/MultipleBagFetchException.java index 2b553c9ec4..cac58f2ce9 100644 --- a/hibernate-core/src/main/java/org/hibernate/loader/MultipleBagFetchException.java +++ b/hibernate-core/src/main/java/org/hibernate/loader/MultipleBagFetchException.java @@ -19,7 +19,7 @@ public class MultipleBagFetchException extends HibernateException { private final List bagRoles; public MultipleBagFetchException(List bagRoles) { - super( "cannot simultaneously fetch multiple bags" ); + super( "cannot simultaneously fetch multiple bags: " + bagRoles ); this.bagRoles = bagRoles; } diff --git a/hibernate-core/src/main/java/org/hibernate/loader/plan/exec/internal/AbstractLoadQueryDetails.java b/hibernate-core/src/main/java/org/hibernate/loader/plan/exec/internal/AbstractLoadQueryDetails.java index 26ea764a0c..07fdd2421c 100644 --- a/hibernate-core/src/main/java/org/hibernate/loader/plan/exec/internal/AbstractLoadQueryDetails.java +++ b/hibernate-core/src/main/java/org/hibernate/loader/plan/exec/internal/AbstractLoadQueryDetails.java @@ -10,6 +10,7 @@ import java.util.ArrayList; import java.util.List; import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.loader.MultipleBagFetchException; import org.hibernate.loader.plan.build.spi.LoadPlanTreePrinter; import org.hibernate.loader.plan.exec.process.internal.ResultSetProcessorImpl; import org.hibernate.loader.plan.exec.process.spi.CollectionReferenceInitializer; @@ -20,6 +21,7 @@ import org.hibernate.loader.plan.exec.query.internal.SelectStatementBuilder; import org.hibernate.loader.plan.exec.query.spi.QueryBuildingParameters; import org.hibernate.loader.plan.exec.spi.AliasResolutionContext; import org.hibernate.loader.plan.exec.spi.LoadQueryDetails; +import org.hibernate.loader.plan.spi.CollectionAttributeFetch; import org.hibernate.loader.plan.spi.CollectionReturn; import org.hibernate.loader.plan.spi.FetchSource; import org.hibernate.loader.plan.spi.LoadPlan; @@ -168,6 +170,14 @@ public abstract class AbstractLoadQueryDetails implements LoadQueryDetails { // TODO: what about index??? } + if ( fetchStats != null && fetchStats.getJoinedBagAttributeFetches().size() > 1 ) { + final List bagRoles = new ArrayList<>(); + for ( CollectionAttributeFetch bagFetch : fetchStats.getJoinedBagAttributeFetches() ) { + bagRoles.add( bagFetch.getCollectionPersister().getRole() ); + } + throw new MultipleBagFetchException( bagRoles ); + } + LoadPlanTreePrinter.INSTANCE.logTree( loadPlan, queryProcessor.getAliasResolutionContext() ); this.sqlStatement = select.toStatementString(); diff --git a/hibernate-core/src/main/java/org/hibernate/loader/plan/exec/internal/FetchStats.java b/hibernate-core/src/main/java/org/hibernate/loader/plan/exec/internal/FetchStats.java index 4730613305..74e847509c 100644 --- a/hibernate-core/src/main/java/org/hibernate/loader/plan/exec/internal/FetchStats.java +++ b/hibernate-core/src/main/java/org/hibernate/loader/plan/exec/internal/FetchStats.java @@ -6,6 +6,10 @@ */ package org.hibernate.loader.plan.exec.internal; +import java.util.Set; + +import org.hibernate.loader.plan.spi.CollectionAttributeFetch; + /** * Contract used to report collected information about fetches. For now that is only whether there were * subselect fetches found @@ -19,4 +23,11 @@ public interface FetchStats { * @return {@code true} if subselect fetches were encountered; {@code false} otherwise. */ public boolean hasSubselectFetches(); + + /** + * Returns a set of bag attributes that are join-fetched. + * + * @return a set of bag attributes that are join-fetched. + */ + public Set getJoinedBagAttributeFetches(); } diff --git a/hibernate-core/src/main/java/org/hibernate/loader/plan/exec/internal/LoadQueryJoinAndFetchProcessor.java b/hibernate-core/src/main/java/org/hibernate/loader/plan/exec/internal/LoadQueryJoinAndFetchProcessor.java index 76eeaa57ad..8d801b81a6 100644 --- a/hibernate-core/src/main/java/org/hibernate/loader/plan/exec/internal/LoadQueryJoinAndFetchProcessor.java +++ b/hibernate-core/src/main/java/org/hibernate/loader/plan/exec/internal/LoadQueryJoinAndFetchProcessor.java @@ -6,6 +6,10 @@ */ package org.hibernate.loader.plan.exec.internal; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + import org.hibernate.AssertionFailure; import org.hibernate.engine.FetchStyle; import org.hibernate.engine.FetchTiming; @@ -41,6 +45,7 @@ import org.hibernate.persister.walking.internal.FetchStrategyHelper; import org.hibernate.sql.JoinFragment; import org.hibernate.sql.JoinType; import org.hibernate.type.AssociationType; +import org.hibernate.type.BagType; import org.hibernate.type.Type; import org.jboss.logging.Logger; @@ -657,6 +662,7 @@ public class LoadQueryJoinAndFetchProcessor { */ private static class FetchStatsImpl implements FetchStats { private boolean hasSubselectFetch; + private Set joinedBagAttributeFetches; public void processingFetch(Fetch fetch) { if ( ! hasSubselectFetch ) { @@ -665,12 +671,32 @@ public class LoadQueryJoinAndFetchProcessor { hasSubselectFetch = true; } } + if ( isJoinFetchedBag( fetch ) ) { + if ( joinedBagAttributeFetches == null ) { + joinedBagAttributeFetches = new HashSet<>(); + } + joinedBagAttributeFetches.add( (CollectionAttributeFetch) fetch ); + } } @Override public boolean hasSubselectFetches() { return hasSubselectFetch; } + + @Override + public Set getJoinedBagAttributeFetches() { + return joinedBagAttributeFetches == null ? Collections.emptySet() : joinedBagAttributeFetches; + } + + private boolean isJoinFetchedBag(Fetch fetch) { + if ( FetchStrategyHelper.isJoinFetched( fetch.getFetchStrategy() ) && + CollectionAttributeFetch.class.isInstance( fetch ) ) { + final CollectionAttributeFetch collectionAttributeFetch = (CollectionAttributeFetch) fetch; + return collectionAttributeFetch.getFetchedType().getClass().isAssignableFrom( BagType.class ); + } + return false; + } } } diff --git a/hibernate-core/src/test/java/org/hibernate/test/collection/bag/MultipleBagFetchHqlTest.java b/hibernate-core/src/test/java/org/hibernate/test/collection/bag/MultipleBagFetchHqlTest.java new file mode 100644 index 0000000000..ce3778de41 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/test/collection/bag/MultipleBagFetchHqlTest.java @@ -0,0 +1,195 @@ +/* + * 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.test.collection.bag; + +import java.util.ArrayList; +import java.util.List; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.JoinTable; +import javax.persistence.ManyToMany; +import javax.persistence.OneToMany; +import javax.persistence.Table; + +import org.junit.Test; + +import org.hibernate.Session; +import org.hibernate.Transaction; +import org.hibernate.loader.MultipleBagFetchException; +import org.hibernate.testing.junit4.BaseCoreFunctionalTestCase; + +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +public class MultipleBagFetchHqlTest extends BaseCoreFunctionalTestCase { + + @Override + protected Class[] getAnnotatedClasses() { + return new Class[] { + Post.class, + PostComment.class, + Tag.class + }; + } + + @Test + public void testMultipleBagFetchHql() throws Exception { + + Session session = openSession(); + Transaction transaction = session.beginTransaction(); + + Post post = new Post(); + post.setId( 1L ); + post.setTitle( String.format( "Post nr. %d", 1 ) ); + PostComment comment = new PostComment(); + comment.setId(1L); + comment.setReview( "Excellent!" ); + session.persist(post); + session.persist( comment ); + post.comments.add( comment ); + + transaction.commit(); + session.close(); + + + session = openSession(); + session.beginTransaction(); + + try { + post = (Post) session.createQuery( + "select p " + + "from Post p " + + "join fetch p.tags " + + "join fetch p.comments " + + "where p.id = :id" + ) + .setParameter( "id", 1L ) + .uniqueResult(); + fail("Should throw org.hibernate.loader.MultipleBagFetchException: cannot simultaneously fetch multiple bags"); + } + catch ( IllegalArgumentException expected ) { + session.getTransaction().rollback(); + // MultipleBagFetchException was converted to IllegalArgumentException + assertTrue( MultipleBagFetchException.class.isInstance( expected.getCause() ) ); + } + finally { + session.close(); + } + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + @OneToMany(fetch = FetchType.LAZY) + private List comments = new ArrayList(); + + @ManyToMany(fetch = FetchType.LAZY) + @JoinTable(name = "post_tag", + joinColumns = @JoinColumn(name = "post_id"), + inverseJoinColumns = @JoinColumn(name = "tag_id") + ) + private List tags = new ArrayList(); + + public Post() { + } + + public Post(Long id) { + this.id = id; + } + + public Post(String title) { + this.title = title; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public List getTags() { + return tags; + } + } + + @Entity(name = "PostComment") + @Table(name = "post_comment") + public static class PostComment { + + @Id + private Long id; + + private String review; + + public PostComment() { + } + + public PostComment(String review) { + this.review = review; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getReview() { + return review; + } + + public void setReview(String review) { + this.review = review; + } + } + + @Entity(name = "Tag") + @Table(name = "tag") + public static class Tag { + + @Id + private Long id; + + private String name; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/test/collection/bag/MultipleBagFetchTest.java b/hibernate-core/src/test/java/org/hibernate/test/collection/bag/MultipleBagFetchTest.java new file mode 100644 index 0000000000..bf4128fe73 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/test/collection/bag/MultipleBagFetchTest.java @@ -0,0 +1,160 @@ +/* + * 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.test.collection.bag; + +import java.util.ArrayList; +import java.util.List; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.JoinTable; +import javax.persistence.ManyToMany; +import javax.persistence.OneToMany; +import javax.persistence.Table; + +import org.hibernate.boot.Metadata; +import org.hibernate.boot.MetadataSources; +import org.hibernate.boot.registry.StandardServiceRegistry; +import org.hibernate.boot.registry.StandardServiceRegistryBuilder; + +import org.hibernate.loader.MultipleBagFetchException; + +import org.junit.Test; + +import static org.junit.Assert.fail; + +public class MultipleBagFetchTest { + + @Test + public void testEntityWithMultipleJoinFetchedBags() { + StandardServiceRegistry standardRegistry = new StandardServiceRegistryBuilder().build(); + + Metadata metadata = new MetadataSources( standardRegistry ) + .addAnnotatedClass( Post.class ) + .addAnnotatedClass( PostComment.class ) + .addAnnotatedClass( Tag.class ) + .getMetadataBuilder() + .build(); + try { + metadata.buildSessionFactory(); + fail( "MultipleBagFetchException should have been thrown." ); + } + catch (MultipleBagFetchException expected) { + } + } + + @Entity(name = "Post") + @Table(name = "post") + public static class Post { + + @Id + private Long id; + + private String title; + + @OneToMany(fetch = FetchType.EAGER) + private List comments = new ArrayList(); + + @ManyToMany(fetch = FetchType.EAGER) + @JoinTable(name = "post_tag", + joinColumns = @JoinColumn(name = "post_id"), + inverseJoinColumns = @JoinColumn(name = "tag_id") + ) + private List tags = new ArrayList(); + + public Post() { + } + + public Post(Long id) { + this.id = id; + } + + public Post(String title) { + this.title = title; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public List getTags() { + return tags; + } + } + + @Entity(name = "PostComment") + @Table(name = "post_comment") + public static class PostComment { + + @Id + private Long id; + + private String review; + + public PostComment() { + } + + public PostComment(String review) { + this.review = review; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getReview() { + return review; + } + + public void setReview(String review) { + this.review = review; + } + } + + @Entity(name = "Tag") + @Table(name = "tag") + public static class Tag { + + @Id + private Long id; + + private String name; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } +}