diff --git a/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaCrossJoin.java b/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaCrossJoin.java new file mode 100644 index 0000000000..2b1687ee85 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaCrossJoin.java @@ -0,0 +1,16 @@ +/* + * 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 http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.query.criteria; + +import org.hibernate.Incubating; + +/** + * @author Marco Belladelli + */ +@Incubating +public interface JpaCrossJoin extends JpaFrom { +} diff --git a/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaFrom.java b/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaFrom.java index a566b8c104..72a2e93dd0 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaFrom.java +++ b/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaFrom.java @@ -10,13 +10,8 @@ import org.hibernate.Incubating; import org.hibernate.metamodel.model.domain.EntityDomainType; import org.hibernate.query.sqm.tree.SqmJoinType; -import jakarta.persistence.criteria.CollectionJoin; import jakarta.persistence.criteria.From; -import jakarta.persistence.criteria.Join; import jakarta.persistence.criteria.JoinType; -import jakarta.persistence.criteria.ListJoin; -import jakarta.persistence.criteria.MapJoin; -import jakarta.persistence.criteria.SetJoin; import jakarta.persistence.criteria.Subquery; import jakarta.persistence.metamodel.CollectionAttribute; import jakarta.persistence.metamodel.ListAttribute; @@ -62,6 +57,12 @@ public interface JpaFrom extends JpaPath, JpaFetchParent, From @Incubating JpaJoinedFrom join(JpaCteCriteria cte, SqmJoinType joinType); + @Incubating + JpaCrossJoin crossJoin(Class entityJavaType); + + @Incubating + JpaCrossJoin crossJoin(EntityDomainType entity); + // Covariant overrides @Override diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/AbstractSqmFrom.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/AbstractSqmFrom.java index 77519220fe..c2d0def927 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/AbstractSqmFrom.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/AbstractSqmFrom.java @@ -24,6 +24,7 @@ import org.hibernate.metamodel.model.domain.MapPersistentAttribute; import org.hibernate.metamodel.model.domain.PluralPersistentAttribute; import org.hibernate.metamodel.model.domain.SetPersistentAttribute; import org.hibernate.metamodel.model.domain.SingularPersistentAttribute; +import org.hibernate.query.criteria.JpaCrossJoin; import org.hibernate.query.criteria.JpaCteCriteria; import org.hibernate.query.criteria.JpaDerivedJoin; import org.hibernate.query.criteria.JpaJoinedFrom; @@ -622,6 +623,19 @@ public abstract class AbstractSqmFrom extends AbstractSqmPath implements } } + @Override + public JpaCrossJoin crossJoin(Class entityJavaType) { + return crossJoin( nodeBuilder().getDomainModel().entity( entityJavaType ) ); + } + + @Override + public JpaCrossJoin crossJoin(EntityDomainType entity) { + final SqmCrossJoin crossJoin = new SqmCrossJoin<>( entity, null, findRoot() ); + // noinspection unchecked + addSqmJoin( (SqmJoin) crossJoin ); + return crossJoin; + } + @Override public Set> getFetches() { //noinspection unchecked diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/from/SqmCrossJoin.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/from/SqmCrossJoin.java index 3d2a84f025..7e109ee1f0 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/from/SqmCrossJoin.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/from/SqmCrossJoin.java @@ -7,6 +7,7 @@ package org.hibernate.query.sqm.tree.from; import org.hibernate.metamodel.model.domain.EntityDomainType; +import org.hibernate.query.criteria.JpaCrossJoin; import org.hibernate.spi.NavigablePath; import org.hibernate.query.PathException; import org.hibernate.query.hql.spi.SqmCreationProcessingState; @@ -24,7 +25,7 @@ import static org.hibernate.query.sqm.spi.SqmCreationHelper.buildRootNavigablePa /** * @author Steve Ebersole */ -public class SqmCrossJoin extends AbstractSqmFrom implements SqmJoin { +public class SqmCrossJoin extends AbstractSqmFrom implements JpaCrossJoin, SqmJoin { private final SqmRoot sqmRoot; public SqmCrossJoin( diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/criteria/CriteriaCrossJoinTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/criteria/CriteriaCrossJoinTest.java new file mode 100644 index 0000000000..577bcae76e --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/criteria/CriteriaCrossJoinTest.java @@ -0,0 +1,110 @@ +/* + * 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 http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.test.query.criteria; + +import java.time.LocalDateTime; +import java.util.Date; +import java.util.List; + +import org.hibernate.query.criteria.HibernateCriteriaBuilder; +import org.hibernate.query.criteria.JpaCrossJoin; +import org.hibernate.query.criteria.JpaRoot; + +import org.hibernate.testing.orm.domain.gambit.EntityOfBasics; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Tuple; +import jakarta.persistence.criteria.CriteriaQuery; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * @author Marco Belladelli + */ +@DomainModel(annotatedClasses = EntityOfBasics.class) +@SessionFactory +public class CriteriaCrossJoinTest { + + @BeforeEach + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( em -> { + Date now = new Date(); + + EntityOfBasics entity1 = new EntityOfBasics(); + entity1.setId( 1 ); + entity1.setTheString( "5" ); + entity1.setTheInt( 5 ); + entity1.setTheInteger( -1 ); + entity1.setTheDouble( 1.0 ); + entity1.setTheDate( now ); + entity1.setTheLocalDateTime( LocalDateTime.now() ); + entity1.setTheBoolean( true ); + em.persist( entity1 ); + + EntityOfBasics entity2 = new EntityOfBasics(); + entity2.setId( 2 ); + entity2.setTheString( "6" ); + entity2.setTheInt( 6 ); + entity2.setTheInteger( -2 ); + entity2.setTheDouble( 6.0 ); + entity2.setTheBoolean( true ); + em.persist( entity2 ); + + EntityOfBasics entity3 = new EntityOfBasics(); + entity3.setId( 3 ); + entity3.setTheString( "7" ); + entity3.setTheInt( 7 ); + entity3.setTheInteger( 3 ); + entity3.setTheDouble( 7.0 ); + entity3.setTheBoolean( false ); + entity3.setTheDate( new Date( now.getTime() + 200000L ) ); + em.persist( entity3 ); + + EntityOfBasics entity4 = new EntityOfBasics(); + entity4.setId( 4 ); + entity4.setTheString( "thirteen" ); + entity4.setTheInt( 13 ); + entity4.setTheInteger( 4 ); + entity4.setTheDouble( 13.0 ); + entity4.setTheBoolean( false ); + entity4.setTheDate( new Date( now.getTime() + 300000L ) ); + em.persist( entity4 ); + + EntityOfBasics entity5 = new EntityOfBasics(); + entity5.setId( 5 ); + entity5.setTheString( "5" ); + entity5.setTheInt( 5 ); + entity5.setTheInteger( 5 ); + entity5.setTheDouble( 9.0 ); + entity5.setTheBoolean( false ); + em.persist( entity5 ); + } ); + } + + @AfterEach + public void tearDown(SessionFactoryScope scope) { + scope.inTransaction( session -> session.createMutationQuery( "delete from EntityOfBasics" ).executeUpdate() ); + } + + @Test + public void testSimpleCrossJoin(SessionFactoryScope scope) { + scope.inTransaction( session -> { + HibernateCriteriaBuilder cb = session.getCriteriaBuilder(); + CriteriaQuery query = cb.createTupleQuery(); + JpaRoot from = (JpaRoot) query.from( EntityOfBasics.class ); + JpaCrossJoin crossJoin = from.crossJoin( EntityOfBasics.class ); + query.multiselect( from.get( "id" ), crossJoin.get( "id" ) ).where( cb.gt( crossJoin.get( "theInt" ), 5 ) ); + List resultList = session.createQuery( query ).getResultList(); + assertEquals( 15, resultList.size() ); + } ); + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/criteria/CriteriaOrderedSetAggregateTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/criteria/CriteriaOrderedSetAggregateTest.java index 58fdd42fd8..a0ab45518b 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/query/criteria/CriteriaOrderedSetAggregateTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/criteria/CriteriaOrderedSetAggregateTest.java @@ -15,7 +15,9 @@ import org.hibernate.dialect.H2Dialect; import org.hibernate.dialect.PostgreSQLDialect; import org.hibernate.dialect.PostgresPlusDialect; import org.hibernate.query.criteria.HibernateCriteriaBuilder; +import org.hibernate.query.criteria.JpaCrossJoin; import org.hibernate.query.criteria.JpaExpression; +import org.hibernate.query.criteria.JpaRoot; import org.hibernate.query.criteria.JpaWindow; import org.hibernate.query.sqm.NullPrecedence; import org.hibernate.query.sqm.SortOrder; @@ -309,17 +311,16 @@ public class CriteriaOrderedSetAggregateTest { @RequiresDialectFeature(feature = DialectFeatureChecks.SupportsHypotheticalSetFunctions.class) @RequiresDialect(H2Dialect.class) public void testHypotheticalSetRankWithGroupByHavingOrderByLimit(SessionFactoryScope scope) { - // note : cross joins are not supported in criteria and this query structure causes problems with many dbs scope.inTransaction( session -> { HibernateCriteriaBuilder cb = session.getCriteriaBuilder(); CriteriaQuery cr = cb.createQuery( Tuple.class ); - Root root1 = cr.from( EntityOfBasics.class ); - Root root2 = cr.from( EntityOfBasics.class ); + JpaRoot e1 = (JpaRoot) cr.from( EntityOfBasics.class ); + JpaCrossJoin e2 = e1.crossJoin( EntityOfBasics.class ); - JpaExpression function = cb.rank( cb.asc( root1.get( "theInt" ) ), cb.literal( 5 ) ); + JpaExpression function = cb.rank( cb.asc( e1.get( "theInt" ) ), cb.literal( 5 ) ); - cr.multiselect( root2.get( "id" ), function ) - .groupBy( root2.get( "id" ) ).having( cb.gt( root2.get( "id" ), cb.literal( 1 ) ) ) + cr.multiselect( e2.get( "id" ), function ) + .groupBy( e2.get( "id" ) ).having( cb.gt( e2.get( "id" ), cb.literal( 1 ) ) ) .orderBy( cb.asc( cb.literal( 1 ) ), cb.asc( cb.literal( 2 ) ) ); List resultList = session.createQuery( cr ).setFirstResult( 1 ).getResultList();