diff --git a/core/impl/src/main/java/com/blazebit/persistence/impl/JoinManager.java b/core/impl/src/main/java/com/blazebit/persistence/impl/JoinManager.java index 73c32a7211..748073facc 100644 --- a/core/impl/src/main/java/com/blazebit/persistence/impl/JoinManager.java +++ b/core/impl/src/main/java/com/blazebit/persistence/impl/JoinManager.java @@ -1017,15 +1017,20 @@ private JoinResult correlate(JoinResult result, String rootAlias, Expression cor if (!aliasManager.isAliasAvailable(alias)) { alias = aliasManager.generateRootAlias(alias); } - String baseAlias = addRoot(result.baseNode.getEntityType(), alias, false); - JoinNode joinNode = ((JoinAliasInfo) aliasManager.getAliasInfo(baseAlias)).getJoinNode(); - joinNode.getAliasInfo().setImplicit(true); - Predicate correlationPredicate = expressionFactory.createBooleanExpression(createCorrelationPredicate(result.baseNode.getEntityType(), result.baseNode.getAliasExpression(), baseAlias), false); + JoinAliasInfo rootAliasInfo = new JoinAliasInfo(alias, alias, true, true, aliasManager); + JoinNode joinNode = JoinNode.createEntityJoinNode(result.baseNode, JoinType.LEFT, result.baseNode.getEntityType(), rootAliasInfo, false); + result.baseNode.addEntityJoin(joinNode); + rootAliasInfo.setJoinNode(joinNode); + explicitJoinNodes.add(joinNode); + // register root alias in aliasManager + aliasManager.registerAliasInfo(rootAliasInfo); + + Predicate correlationPredicate = expressionFactory.createBooleanExpression(createCorrelationPredicate(result.baseNode.getEntityType(), result.baseNode.getAliasExpression(), alias), false); correlationPredicate.accept(joinVisitor); joinNode.setOnPredicate(new CompoundPredicate(CompoundPredicate.BooleanOperator.AND, correlationPredicate)); if (implicit || !(correlatedAttributeExpr instanceof ArrayExpression)) { PathExpression pathExpression = new PathExpression(); - pathExpression.getExpressions().add(new PropertyExpression(baseAlias)); + pathExpression.getExpressions().add(new PropertyExpression(alias)); if (correlatedAttributeExpr instanceof PathExpression) { pathExpression.getExpressions().addAll(((PathExpression) correlatedAttributeExpr).getExpressions()); } else { @@ -2031,6 +2036,14 @@ private boolean renderCorrelationJoinPath(StringBuilder sb, JoinNode joinBase, J } whereConjuncts.add(whereSb.toString()); return true; + } else if (!externalRepresentation && !node.isLateral()) { + sb.append(joinBase.getEntityType().getName()); + sb.append(" _synthetic_"); + sb.append(node.getAlias()); + sb.append(" JOIN _synthetic_"); + sb.append(node.getAlias()); + sb.append('.').append(correlationPath); + return true; } } else { boolean renderAlias = true; diff --git a/core/testsuite/src/test/java/com/blazebit/persistence/testsuite/SubqueryTest.java b/core/testsuite/src/test/java/com/blazebit/persistence/testsuite/SubqueryTest.java index 3beed1229c..5abfff72af 100644 --- a/core/testsuite/src/test/java/com/blazebit/persistence/testsuite/SubqueryTest.java +++ b/core/testsuite/src/test/java/com/blazebit/persistence/testsuite/SubqueryTest.java @@ -262,7 +262,13 @@ public void testSubqueryImplicitCorrelate() { .where("d.owner.friend.name").isNotNull() .where("d.owner.defaultLanguage").isNotNull() .end(); - String expectedQuery = "SELECT d FROM Document d WHERE EXISTS (SELECT 1 FROM Person p, Document d_owner_base JOIN d_owner_base.owner owner_1 LEFT JOIN owner_1.friend friend_1 WHERE d.id = d_owner_base.id AND friend_1.name IS NOT NULL AND owner_1.defaultLanguage IS NOT NULL)"; + String expectedSubQuery; + if (jpaProvider.supportsEntityJoin()) { + expectedSubQuery = "SELECT 1 FROM Person p JOIN Document d_owner_base ON (d.id = d_owner_base.id) JOIN d_owner_base.owner owner_1 LEFT JOIN owner_1.friend friend_1 WHERE friend_1.name IS NOT NULL AND owner_1.defaultLanguage IS NOT NULL"; + } else { + expectedSubQuery = "SELECT 1 FROM Person p, Document d_owner_base JOIN d_owner_base.owner owner_1 LEFT JOIN owner_1.friend friend_1 WHERE d.id = d_owner_base.id AND friend_1.name IS NOT NULL AND owner_1.defaultLanguage IS NOT NULL"; + } + String expectedQuery = "SELECT d FROM Document d WHERE EXISTS (" + expectedSubQuery + ")"; assertEquals(expectedQuery, crit.getQueryString()); crit.getResultList(); } @@ -494,11 +500,20 @@ public void testSubqueryCollectionAccessAddsJoin() { .where("LENGTH(d.partners.localized[1])").gt(1) .end() .like().value("%dld").noEscape(); - String expectedQuery = "SELECT d FROM Document d" - + " WHERE (SELECT p.name FROM Person p, Document d_partners_base " + + String expectedSubQuery; + if (jpaProvider.supportsEntityJoin()) { + expectedSubQuery = "SELECT p.name FROM Person p JOIN Document d_partners_base ON (d.id = d_partners_base.id) " + + "LEFT JOIN d_partners_base.partners partners_1 " + + "LEFT JOIN partners_1.localized localized_1_1" + onClause("KEY(localized_1_1) = 1") + + " WHERE LENGTH("+ joinAliasValue("localized_1_1") + ") > :param_0"; + } else { + expectedSubQuery = "SELECT p.name FROM Person p, Document d_partners_base " + "LEFT JOIN d_partners_base.partners partners_1 " + "LEFT JOIN partners_1.localized localized_1_1" + onClause("KEY(localized_1_1) = 1") + - " WHERE d.id = d_partners_base.id AND LENGTH("+ joinAliasValue("localized_1_1") + ") > :param_0) LIKE :param_1"; + " WHERE d.id = d_partners_base.id AND LENGTH("+ joinAliasValue("localized_1_1") + ") > :param_0"; + } + String expectedQuery = "SELECT d FROM Document d" + + " WHERE (" + expectedSubQuery + ") LIKE :param_1"; assertEquals(expectedQuery, crit.getQueryString()); crit.getResultList(); } @@ -536,7 +551,12 @@ public void testSubqueryAddsJoin() { .groupBy("id") .orderByAsc("localizedCount"); - String expectedSubQuery = "ABS((SELECT COUNT(" + joinAliasValue("localized_1") + ") FROM Person p LEFT JOIN p.localized localized_1, Document d_contacts_base LEFT JOIN d_contacts_base.contacts contacts_1 WHERE d.id = d_contacts_base.id AND p.id = " + joinAliasValue("contacts_1", "id") + "))"; + String expectedSubQuery; + if (jpaProvider.supportsEntityJoin()) { + expectedSubQuery = "ABS((SELECT COUNT(" + joinAliasValue("localized_1") + ") FROM Person p LEFT JOIN p.localized localized_1 JOIN Document d_contacts_base ON (d.id = d_contacts_base.id) LEFT JOIN d_contacts_base.contacts contacts_1 WHERE p.id = " + joinAliasValue("contacts_1", "id") + "))"; + } else { + expectedSubQuery = "ABS((SELECT COUNT(" + joinAliasValue("localized_1") + ") FROM Person p LEFT JOIN p.localized localized_1, Document d_contacts_base LEFT JOIN d_contacts_base.contacts contacts_1 WHERE d.id = d_contacts_base.id AND p.id = " + joinAliasValue("contacts_1", "id") + "))"; + } String expectedQuery = "SELECT d.id, " + expectedSubQuery + " AS localizedCount " + "FROM Document d GROUP BY d.id ORDER BY localizedCount ASC"; assertEquals(expectedQuery, cb.getQueryString()); @@ -553,11 +573,20 @@ public void testSubqueryCollectionAccess() { .end() .like().value("%dld").noEscape(); - String expectedQuery = "SELECT d FROM Document d" - + " WHERE (SELECT p.name FROM Person p, Document d_partners_base " + + String expectedSubQuery; + if (jpaProvider.supportsEntityJoin()) { + expectedSubQuery = "SELECT p.name FROM Person p JOIN Document d_partners_base ON (d.id = d_partners_base.id) " + "LEFT JOIN d_partners_base.partners partners_1 " + "LEFT JOIN partners_1.localized localized_1_1" + onClause("KEY(localized_1_1) = 1") + - " WHERE d.id = d_partners_base.id AND LENGTH("+ joinAliasValue("localized_1_1") + ") > :param_0) LIKE :param_1"; + " WHERE LENGTH("+ joinAliasValue("localized_1_1") + ") > :param_0"; + } else { + expectedSubQuery = "SELECT p.name FROM Person p, Document d_partners_base " + + "LEFT JOIN d_partners_base.partners partners_1 " + + "LEFT JOIN partners_1.localized localized_1_1" + onClause("KEY(localized_1_1) = 1") + + " WHERE d.id = d_partners_base.id AND LENGTH("+ joinAliasValue("localized_1_1") + ") > :param_0"; + } + String expectedQuery = "SELECT d FROM Document d" + + " WHERE (" + expectedSubQuery + ") LIKE :param_1"; assertEquals(expectedQuery, crit.getQueryString()); crit.getResultList(); } @@ -573,12 +602,21 @@ public void testMultipleJoinPathSubqueryCollectionAccess() { .end() .like().value("%dld").noEscape(); + String expectedSubQuery; + if (jpaProvider.supportsEntityJoin()) { + expectedSubQuery = "SELECT p.name FROM Person p JOIN Person d_partners_localized_base ON (partners_1.id = d_partners_localized_base.id) " + + "LEFT JOIN d_partners_localized_base.localized localized_1_1" + + onClause("KEY(localized_1_1) = 1") + + " WHERE LENGTH("+ joinAliasValue("localized_1_1") + ") > :param_0"; + } else { + expectedSubQuery = "SELECT p.name FROM Person p, Person d_partners_localized_base LEFT JOIN d_partners_localized_base.localized localized_1_1" + + onClause("KEY(localized_1_1) = 1") + + " WHERE partners_1.id = d_partners_localized_base.id AND LENGTH("+ joinAliasValue("localized_1_1") + ") > :param_0"; + } String expectedQuery = "SELECT d FROM Document d " + "LEFT JOIN d.partners partners_1 " + "LEFT JOIN partners_1.localized l " - + "WHERE (SELECT p.name FROM Person p, Person d_partners_localized_base LEFT JOIN d_partners_localized_base.localized localized_1_1" - + onClause("KEY(localized_1_1) = 1") - + " WHERE partners_1.id = d_partners_localized_base.id AND LENGTH("+ joinAliasValue("localized_1_1") + ") > :param_0) LIKE :param_1"; + + "WHERE (" + expectedSubQuery + ") LIKE :param_1"; assertEquals(expectedQuery, crit.getQueryString()); crit.getResultList(); } diff --git a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/limit/LimitOneToManyTest.java b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/limit/LimitOneToManyTest.java new file mode 100644 index 0000000000..9b335a2e6b --- /dev/null +++ b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/limit/LimitOneToManyTest.java @@ -0,0 +1,116 @@ +/* + * Copyright 2014 - 2023 Blazebit. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.blazebit.persistence.view.testsuite.limit; + +import com.blazebit.persistence.PaginatedCriteriaBuilder; +import com.blazebit.persistence.testsuite.base.jpa.category.NoDatanucleus; +import com.blazebit.persistence.testsuite.base.jpa.category.NoEclipselink; +import com.blazebit.persistence.testsuite.base.jpa.category.NoH2; +import com.blazebit.persistence.testsuite.base.jpa.category.NoHibernate42; +import com.blazebit.persistence.testsuite.base.jpa.category.NoHibernate43; +import com.blazebit.persistence.testsuite.base.jpa.category.NoHibernate50; +import com.blazebit.persistence.testsuite.base.jpa.category.NoHibernate51; +import com.blazebit.persistence.testsuite.base.jpa.category.NoMySQLOld; +import com.blazebit.persistence.testsuite.base.jpa.category.NoOpenJPA; +import com.blazebit.persistence.testsuite.entity.Document; +import com.blazebit.persistence.testsuite.entity.Person; +import com.blazebit.persistence.testsuite.tx.TxVoidWork; +import com.blazebit.persistence.view.ConfigurationProperties; +import com.blazebit.persistence.view.EntityView; +import com.blazebit.persistence.view.EntityViewManager; +import com.blazebit.persistence.view.EntityViewSetting; +import com.blazebit.persistence.view.IdMapping; +import com.blazebit.persistence.view.Limit; +import com.blazebit.persistence.view.Mapping; +import com.blazebit.persistence.view.testsuite.AbstractEntityViewTest; +import com.blazebit.persistence.view.testsuite.limit.model.DocumentLimitView; +import com.blazebit.persistence.view.testsuite.limit.model.PersonLimitJoinExpressionView; +import com.blazebit.persistence.view.testsuite.limit.model.PersonLimitJoinView; +import com.blazebit.persistence.view.testsuite.limit.model.PersonLimitMultisetView; +import com.blazebit.persistence.view.testsuite.limit.model.PersonLimitSelectView; +import com.blazebit.persistence.view.testsuite.limit.model.PersonLimitSubselectView; +import com.blazebit.persistence.view.testsuite.limit.model.PersonLimitView; +import org.junit.Test; +import org.junit.experimental.categories.Category; + +import javax.persistence.EntityManager; +import java.util.List; + +import static org.junit.Assert.assertEquals; + +/** + * + * @author Christian Beikov + * @since 1.5.0 + */ +public class LimitOneToManyTest extends AbstractEntityViewTest { + + @Override + public void setUpOnce() { + cleanDatabase(); + transactional(new TxVoidWork() { + @Override + public void work(EntityManager em) { + Person o1 = new Person("pers1"); + Document doc1 = new Document("doc1", o1); + doc1.setAge(10); + Document doc2 = new Document("doc2", o1); + doc2.setAge(5); + Document doc3 = new Document("doc3", o1); + doc3.setAge(10); + + em.persist(o1); + em.persist(doc1); + em.persist(doc2); + em.persist(doc3); + } + }); + } + + @Test + @Category({ NoMySQLOld.class, NoHibernate42.class, NoHibernate43.class, NoHibernate50.class, NoHibernate51.class, NoEclipselink.class, NoDatanucleus.class, NoOpenJPA.class }) + // We need a left entity join for this so Hibernate < 5.1 can't be used + // MySQL before 8 didn't support lateral and also don't support correlated LIMIT subqueries in quantified predicates + // EclipseLink doesn't support subqueries in functions which is required for LIMIT + // Datanucleus fails because of a NPE? + // OpenJPA has no function support + public void testLimitJoin() { + EntityViewManager evm = build(SimplePersonView.class, PersonView.class); + PaginatedCriteriaBuilder paginatedCriteriaBuilder = evm.applySetting( + EntityViewSetting.create(PersonView.class, 0, 1), + cbf.create(em, Person.class, "p").orderByDesc("p.id") + ); + List list = paginatedCriteriaBuilder.getResultList(); + assertEquals(1, list.size()); +// assertEquals(1, list.get(0).getOwnedDocuments().size()); +// assertEquals("doc2", list.get(0).getOwnedDocuments().get(0).getName()); + } + + @EntityView(Person.class) + interface SimplePersonView { + @IdMapping + Long getId(); + } + + @EntityView(Person.class) + interface PersonView extends SimplePersonView { + @Mapping("partnerDocument.contacts") + @Limit(limit = "1", order = "id desc") + SimplePersonView getBiggest(); + + } +}