diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ba322ac58..af79204a2a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ Not yet released * Entity View Spring integration now allows the use of `includeFilters` and `excludeFilters` on `@EnableEntityViews` * Extended `SubqueryInitiator` by most of the `from()` variants * Support enum and entity type literal like the JPA spec says +* Support for join fetching with scalar selects ### Bug fixes diff --git a/core/api/src/main/java/com/blazebit/persistence/FullQueryBuilder.java b/core/api/src/main/java/com/blazebit/persistence/FullQueryBuilder.java index 94c436c4df..b69f7b596c 100644 --- a/core/api/src/main/java/com/blazebit/persistence/FullQueryBuilder.java +++ b/core/api/src/main/java/com/blazebit/persistence/FullQueryBuilder.java @@ -158,6 +158,16 @@ public interface FullQueryBuilder> extends Q */ public X fetch(String... paths); + /** + * Adds an implicit join fetch for every given path that is relative to the fetch base to the query. + * The fetch base itself and it's parent joins are not fetched, only the given child paths. + * + * @param fetchBase The fetch basis of the paths to fetch + * @param paths The paths to join fetch + * @return The query builder for chaining calls + */ + public X fetchOnly(String fetchBase, String... paths); + /** * Like {@link FullQueryBuilder#join(java.lang.String, java.lang.String, com.blazebit.persistence.JoinType, boolean) } but with * {@link JoinType#INNER} and fetch set to true. diff --git a/core/impl/src/main/java/com/blazebit/persistence/impl/AbstractCommonQueryBuilder.java b/core/impl/src/main/java/com/blazebit/persistence/impl/AbstractCommonQueryBuilder.java index 16c1a76f1c..af16e0e210 100644 --- a/core/impl/src/main/java/com/blazebit/persistence/impl/AbstractCommonQueryBuilder.java +++ b/core/impl/src/main/java/com/blazebit/persistence/impl/AbstractCommonQueryBuilder.java @@ -91,6 +91,7 @@ import java.util.Date; import java.util.EnumSet; import java.util.HashMap; +import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; @@ -1367,12 +1368,23 @@ protected void applyImplicitJoins() { } final JoinVisitor joinVisitor = new JoinVisitor(joinManager); + final Set fetchParents = new HashSet<>(); final JoinNodeVisitor joinNodeVisitor = new OnClauseJoinNodeVisitor(joinVisitor) { @Override public void visit(JoinNode node) { super.visit(node); node.registerDependencies(); + + if (node.isFetch()) { + // Only child nodes can be fetched, the root is never fetched + JoinNode parentNode = node.getParent(); + while (parentNode.isFetch()) { + parentNode = parentNode.getParent(); + } + + fetchParents.add(parentNode); + } } }; @@ -1385,6 +1397,15 @@ public void visit(JoinNode node) { selectManager.acceptVisitor(joinVisitor); joinVisitor.setJoinRequired(true); + // Check if fetch parents are selected + if (!fetchParents.isEmpty()) { + for (JoinNode node : fetchParents) { + if (!selectManager.containsSelect(node.getAlias())) { + throw new IllegalStateException("The fetch parent with alias '" + node.getAlias() + "' is missing in the select clause. Remove the fetches of it's child joins or add the fetch parent to the select clause!"); + } + } + } + joinVisitor.setFromClause(ClauseType.WHERE); whereManager.acceptVisitor(joinVisitor); joinVisitor.setFromClause(ClauseType.GROUP_BY); diff --git a/core/impl/src/main/java/com/blazebit/persistence/impl/AbstractFullQueryBuilder.java b/core/impl/src/main/java/com/blazebit/persistence/impl/AbstractFullQueryBuilder.java index c7add2e576..7f58d8d8a2 100644 --- a/core/impl/src/main/java/com/blazebit/persistence/impl/AbstractFullQueryBuilder.java +++ b/core/impl/src/main/java/com/blazebit/persistence/impl/AbstractFullQueryBuilder.java @@ -27,6 +27,7 @@ import com.blazebit.persistence.ObjectBuilder; import com.blazebit.persistence.PaginatedCriteriaBuilder; import com.blazebit.persistence.SelectObjectBuilder; +import com.blazebit.persistence.impl.expression.Expression; /** * @@ -176,19 +177,12 @@ private void checkEntityId(Object entityId) { return (FullQueryBuilder) this; } - private void checkFetchJoinAllowed() { - if (selectManager.getSelectInfos().size() > 0) { - throw new IllegalStateException("Fetch joins are only possible if the root entity is selected"); - } - } - @Override @SuppressWarnings("unchecked") public X fetch(String path) { prepareForModification(); - checkFetchJoinAllowed(); verifyBuilderEnded(); - joinManager.implicitJoin(expressionFactory.createPathExpression(path), true, null, null, false, false, true, true); + joinManager.implicitJoin(expressionFactory.createPathExpression(path), true, null, null, false, false, true, true, null); return (X) this; } @@ -196,11 +190,29 @@ public X fetch(String path) { @SuppressWarnings("unchecked") public X fetch(String... paths) { prepareForModification(); - checkFetchJoinAllowed(); verifyBuilderEnded(); for (String path : paths) { - joinManager.implicitJoin(expressionFactory.createPathExpression(path), true, null, null, false, false, true, true); + joinManager.implicitJoin(expressionFactory.createPathExpression(path), true, null, null, false, false, true, true, null); + } + + return (X) this; + } + + @Override + @SuppressWarnings("unchecked") + public X fetchOnly(String fetchBase, String... paths) { + prepareForModification(); + verifyBuilderEnded(); + + Expression fetchBaseExpression = expressionFactory.createJoinPathExpression(fetchBase); + JoinManager.JoinResult result = joinManager.implicitJoin(fetchBaseExpression, true, null, null, false, false, true, false, null); + + // There must always be a base node + JoinNode fetchBaseNode = result.baseNode; + + for (String path : paths) { + joinManager.implicitJoin(expressionFactory.createPathExpression(path), true, null, null, false, false, true, true, fetchBaseNode); } return (X) this; @@ -262,10 +274,6 @@ private X join(String path, String alias, JoinType type, boolean fetch, boolean throw new IllegalArgumentException("Empty alias"); } - if (fetch == true) { - checkFetchJoinAllowed(); - } - verifyBuilderEnded(); joinManager.join(path, alias, type, fetch, defaultJoin); return (X) this; 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 6277500cbd..8139f3d107 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 @@ -605,8 +605,12 @@ void removeRoot() { } JoinNode getRootNodeOrFail(String string) { + return getRootNodeOrFail(string, "", ""); + } + + JoinNode getRootNodeOrFail(String prefix, Object middle, String suffix) { if (rootNodes.size() > 1) { - throw new IllegalArgumentException(string); + throw new IllegalArgumentException(prefix + middle + suffix); } return rootNodes.get(0); @@ -1195,6 +1199,7 @@ JoinNode join(String path, String alias, JoinType type, boolean fetch, boolean d PathElementExpression elementExpr; JoinResult result; JoinNode current; + JoinNode fetchBase = null; if (expr instanceof PathExpression) { PathExpression pathExpression = (PathExpression) expr; @@ -1204,6 +1209,9 @@ JoinNode join(String path, String alias, JoinType type, boolean fetch, boolean d List pathElements = pathExpression.getExpressions(); elementExpr = pathElements.get(pathElements.size() - 1); + if (fetch) { + fetchBase = getFetchBase(expr, pathElements.get(0)); + } result = implicitJoin(null, null, pathExpression, null, 0, pathElements.size() - 1); current = result.baseNode; } else if (expr instanceof TreatExpression) { @@ -1219,6 +1227,9 @@ JoinNode join(String path, String alias, JoinType type, boolean fetch, boolean d PathExpression pathExpression = (PathExpression) expression; List pathElements = pathExpression.getExpressions(); elementExpr = pathElements.get(pathElements.size() - 1); + if (fetch) { + fetchBase = getFetchBase(expr, pathElements.get(0)); + } result = implicitJoin(null, null, pathExpression, null, 0, pathElements.size() - 1); current = result.baseNode; } else { @@ -1246,24 +1257,24 @@ JoinNode join(String path, String alias, JoinType type, boolean fetch, boolean d } else { List joinRelationAttributes = result.addToList(new ArrayList()); joinRelationAttributes.add(elementExpr.toString()); - current = current == null ? getRootNodeOrFail("Could not join path [" + path + "] because it did not use an absolute path but multiple root nodes are available!") : current; + current = current == null ? getRootNodeOrFail("Could not join path [", path, "] because it did not use an absolute path but multiple root nodes are available!") : current; result = createOrUpdateNode(current, result.typeName, joinRelationAttributes, treatType, alias, type, false, defaultJoin); } } if (fetch) { - fetchPath(result.baseNode); + fetchPath(result.baseNode, fetchBase); } return result.baseNode; } - public void implicitJoin(Expression expression, boolean objectLeafAllowed, String targetType, ClauseType fromClause, boolean fromSubquery, boolean fromSelectAlias, boolean joinRequired) { - implicitJoin(expression, objectLeafAllowed, targetType, fromClause, fromSubquery, fromSelectAlias, joinRequired, false); + public JoinResult implicitJoin(Expression expression, boolean objectLeafAllowed, String targetType, ClauseType fromClause, boolean fromSubquery, boolean fromSelectAlias, boolean joinRequired) { + return implicitJoin(expression, objectLeafAllowed, targetType, fromClause, fromSubquery, fromSelectAlias, joinRequired, false, null); } @SuppressWarnings("checkstyle:methodlength") - public void implicitJoin(Expression expression, boolean objectLeafAllowed, String targetTypeName, ClauseType fromClause, boolean fromSubquery, boolean fromSelectAlias, boolean joinRequired, boolean fetch) { + public JoinResult implicitJoin(Expression expression, boolean objectLeafAllowed, String targetTypeName, ClauseType fromClause, boolean fromSubquery, boolean fromSelectAlias, boolean joinRequired, boolean fetch, JoinNode fetchBase) { PathExpression pathExpression; if (expression instanceof PathExpression) { pathExpression = (PathExpression) expression; @@ -1274,14 +1285,13 @@ public void implicitJoin(Expression expression, boolean objectLeafAllowed, Strin // this check is necessary to prevent infinite recursion in the case of e.g. SELECT name AS name if (!fromSelectAlias) { // we have to do this implicit join because we might have to adjust the selectOnly flag in the referenced join nodes - implicitJoin(aliasedExpression, true, null, fromClause, fromSubquery, true, joinRequired); + return implicitJoin(aliasedExpression, true, null, fromClause, fromSubquery, true, joinRequired); } - return; + return null; } else if (isExternal(pathExpression)) { // try to set base node and field for the external expression based // on existing joins in the super query - parent.implicitJoin(pathExpression, true, targetTypeName, fromClause, true, fromSelectAlias, joinRequired); - return; + return parent.implicitJoin(pathExpression, true, targetTypeName, fromClause, true, fromSelectAlias, joinRequired); } // First try to implicit join indices of array expressions since we will need their base nodes @@ -1304,10 +1314,23 @@ public void implicitJoin(Expression expression, boolean objectLeafAllowed, Strin JoinNode possibleRoot; int startIndex = 0; - // Skip root speculation if this is just a single element path - if (pathElements.size() > 1 && (possibleRoot = getRootNode(pathElements.get(0))) != null) { - startIndex = 1; - current = possibleRoot; + if (fetchBase != null) { + // If a fetch base is given, we must use this join node as relative base node + current = fetchBase; + } else { + // Skip root speculation if this is just a single element path + if (pathElements.size() > 1 && (possibleRoot = getRootNode(pathElements.get(0))) != null) { + startIndex = 1; + current = possibleRoot; + } + } + + if (fetch && fetchBase == null) { + if (startIndex > 0) { + fetchBase = current; + } else { + fetchBase = getFetchBase(expression, pathElements.get(0)); + } } if (mainQuery.jpaProvider.supportsSingleValuedAssociationIdExpressions() && pathElements.size() > startIndex + 1) { @@ -1391,7 +1414,7 @@ public void implicitJoin(Expression expression, boolean objectLeafAllowed, Strin } else if (elementExpr instanceof MapEntryExpression) { baseNode = joinMapEntry((MapEntryExpression) elementExpr, null, fromClause, fromSubquery, fromSelectAlias, true, fetch, true, true); } else if (elementExpr instanceof MapValueExpression) { - implicitJoin(qualifiedExpression.getPath(), objectLeafAllowed, targetTypeName, fromClause, fromSubquery, fromSelectAlias, joinRequired, fetch); + implicitJoin(qualifiedExpression.getPath(), objectLeafAllowed, targetTypeName, fromClause, fromSubquery, fromSelectAlias, joinRequired, fetch, null); baseNode = (JoinNode) qualifiedExpression.getPath().getBaseNode(); } else { throw new IllegalArgumentException("Unknown qualified expression type: " + elementExpr); @@ -1401,11 +1424,7 @@ public void implicitJoin(Expression expression, boolean objectLeafAllowed, Strin } else { // current might be null if (current == null) { - if (rootNodes.size() > 1) { - throw new IllegalArgumentException("Could not join path [" + expression + "] because it did not use an absolute path but multiple root nodes are available!"); - } - - current = rootNodes.get(0); + current = getRootNodeOrFail("Could not join path [", expression, "] because it did not use an absolute path but multiple root nodes are available!"); } if (singleValuedAssociationIdExpression) { @@ -1492,7 +1511,7 @@ public void implicitJoin(Expression expression, boolean objectLeafAllowed, Strin } if (fetch) { - fetchPath(result.baseNode); + fetchPath(result.baseNode, fetchBase); } // Don't forget to update the clause dependencies!! @@ -1509,20 +1528,53 @@ public void implicitJoin(Expression expression, boolean objectLeafAllowed, Strin if (result.hasTreatedSubpath) { pathExpression.setHasTreatedSubpath(true); } + + return result; } else if (expression instanceof FunctionExpression) { List expressions = ((FunctionExpression) expression).getExpressions(); int size = expressions.size(); for (int i = 0; i < size; i++) { implicitJoin(expressions.get(i), objectLeafAllowed, null, fromClause, fromSubquery, fromSelectAlias, joinRequired); } + + return null; } else if (expression instanceof MapKeyExpression) { MapKeyExpression mapKeyExpression = (MapKeyExpression) expression; - joinMapKey(mapKeyExpression, null, fromClause, fromSubquery, fromSelectAlias, joinRequired, fetch, true, true); + JoinNode node = joinMapKey(mapKeyExpression, null, fromClause, fromSubquery, fromSelectAlias, joinRequired, fetch, true, true); + return new JoinResult(node, null); } else if (expression instanceof QualifiedExpression) { - implicitJoin(((QualifiedExpression) expression).getPath(), objectLeafAllowed, null, fromClause, fromSubquery, fromSelectAlias, joinRequired); + return implicitJoin(((QualifiedExpression) expression).getPath(), objectLeafAllowed, null, fromClause, fromSubquery, fromSelectAlias, joinRequired); } else if (expression instanceof ArrayExpression || expression instanceof GeneralCaseExpression || expression instanceof TreatExpression) { + // TODO: Having a treat expression actually makes sense here for fetchOnly // NOTE: I haven't found a use case for this yet, so I'd like to throw an exception instead of silently not supporting this throw new IllegalArgumentException("Unsupported expression for implicit joining found: " + expression); + } else { + // Other expressions don't need handling + return null; + } + } + + private JoinNode getFetchBase(Expression expression, PathElementExpression firstElement) { + String alias; + + if (firstElement instanceof PropertyExpression) { + alias = ((PropertyExpression) firstElement).getProperty(); + } else if (firstElement instanceof MapKeyExpression) { + alias = ((MapKeyExpression) firstElement).getQualificationExpression(); + } else { + throw new IllegalArgumentException("Illegal fetch base: " + firstElement); + } + + AliasInfo firstElementAliasInfo = aliasManager.getAliasInfo(alias); + if (firstElementAliasInfo != null) { + if (!(firstElementAliasInfo instanceof JoinAliasInfo)) { + throw new IllegalArgumentException("Cannot use select alias as fetch base: " + firstElement); + } + + return ((JoinAliasInfo) firstElementAliasInfo).getJoinNode(); + } else { + // If it isn't a join node, it can only be a relative node, so the root is our fetch base + return getRootNodeOrFail("Ambiguous join path [", expression, "] because of multiple root nodes!"); } } @@ -1634,7 +1686,7 @@ private boolean isSingleValuedAssociationId(JoinResult joinResult, List mapAttribute = (MapAttribute) current.getParentTreeNode().getAttribute(); @@ -1904,7 +1956,7 @@ private JoinNode joinMapKey(MapKeyExpression mapKeyExpression, String alias, Cla } private JoinNode joinMapEntry(MapEntryExpression mapEntryExpression, String alias, ClauseType fromClause, boolean fromSubquery, boolean fromSelectAlias, boolean joinRequired, boolean fetch, boolean implicit, boolean defaultJoin) { - implicitJoin(mapEntryExpression.getPath(), true, null, fromClause, fromSubquery, fromSelectAlias, joinRequired, fetch); + implicitJoin(mapEntryExpression.getPath(), true, null, fromClause, fromSubquery, fromSelectAlias, joinRequired, fetch, null); JoinNode current = (JoinNode) mapEntryExpression.getPath().getBaseNode(); String joinRelationName = "ENTRY(" + current.getParentTreeNode().getRelationName() + ")"; MapAttribute mapAttribute = (MapAttribute) current.getParentTreeNode().getAttribute(); @@ -1916,7 +1968,7 @@ private JoinNode joinMapEntry(MapEntryExpression mapEntryExpression, String alia } private JoinNode joinListIndex(ListIndexExpression listIndexExpression, String alias, ClauseType fromClause, boolean fromSubquery, boolean fromSelectAlias, boolean joinRequired, boolean fetch, boolean implicit, boolean defaultJoin) { - implicitJoin(listIndexExpression.getPath(), true, null, fromClause, fromSubquery, fromSelectAlias, joinRequired, fetch); + implicitJoin(listIndexExpression.getPath(), true, null, fromClause, fromSubquery, fromSelectAlias, joinRequired, fetch, null); JoinNode current = (JoinNode) listIndexExpression.getPath().getBaseNode(); String joinRelationName = "INDEX(" + current.getParentTreeNode().getRelationName() + ")"; ListAttribute listAttribute = (ListAttribute) current.getParentTreeNode().getAttribute(); @@ -1939,7 +1991,7 @@ private JoinResult implicitJoinSingle(JoinNode baseNode, String baseNodeTreatTyp // If we have no base node, root is assumed if (baseNode == null) { - baseNode = getRootNodeOrFail("Ambiguous join path [" + attributeName + "] because of multiple root nodes!"); + baseNode = getRootNodeOrFail("Ambiguous join path [", attributeName, "] because of multiple root nodes!"); } // check if the path is joinable, assuming it is relative to the root (implicit root prefix) @@ -2192,9 +2244,9 @@ private boolean findPredicate(CompoundPredicate compoundPredicate, Predicate pre * * @param node */ - private void fetchPath(JoinNode node) { + private void fetchPath(JoinNode node, JoinNode fetchBase) { JoinNode currentNode = node; - while (currentNode != null) { + while (currentNode != fetchBase) { currentNode.setFetch(true); // fetches implicitly need to be selected currentNode.getClauseDependencies().add(ClauseType.SELECT); @@ -2203,7 +2255,7 @@ private void fetchPath(JoinNode node) { } // TODO: needs equals-hashCode implementation - private static class JoinResult { + public static class JoinResult { final JoinNode baseNode; final List fields; diff --git a/core/impl/src/main/java/com/blazebit/persistence/impl/SelectManager.java b/core/impl/src/main/java/com/blazebit/persistence/impl/SelectManager.java index 41c97ee665..7ebea237fc 100644 --- a/core/impl/src/main/java/com/blazebit/persistence/impl/SelectManager.java +++ b/core/impl/src/main/java/com/blazebit/persistence/impl/SelectManager.java @@ -130,6 +130,32 @@ public boolean containsSizeSelect() { return hasSizeSelect; } + public boolean containsSelect(String alias) { + List infos = selectInfos; + int size = selectInfos.size(); + + for (int i = 0; i < size; i++) { + final SelectInfo selectInfo = infos.get(i); + Expression expression = selectInfo.getExpression(); + if (!(expression instanceof PathExpression)) { + // We only look for entity selects and those can only be path expressions + continue; + } + PathExpression pathExpression = (PathExpression) expression; + JoinNode node = (JoinNode) pathExpression.getBaseNode(); + if (pathExpression.getField() == null && alias.equals(node.getAlias())) { + return true; + } + } + + if (size == 0) { + JoinNode rootNode = joinManager.getRootNodeOrFail("Empty select not allowed when having multiple roots!"); + return alias.equals(rootNode.getAlias()); + } + + return false; + } + void acceptVisitor(Visitor v) { List infos = selectInfos; int size = selectInfos.size(); @@ -168,13 +194,7 @@ void buildGroupByClauses(final EntityMetamodel m, Set clauses) { // When no select infos are available, it can only be a root entity select if (selectInfos.isEmpty()) { // TODO: GroupByTest#testGroupByEntitySelect uses this. It's problematic because it's not aware of VALUES clause - List roots = joinManager.getRoots(); - - if (roots.size() > 1) { - throw new IllegalArgumentException("Empty select not allowed when having multiple roots!"); - } - - JoinNode rootNode = roots.get(0); + JoinNode rootNode = joinManager.getRootNodeOrFail("Empty select not allowed when having multiple roots!"); String rootAlias = rootNode.getAliasInfo().getAlias(); List path = Arrays.asList((PathElementExpression) new PropertyExpression(rootAlias)); @@ -231,13 +251,8 @@ void buildSelect(StringBuilder sb, boolean isInsertInto) { List infos = selectInfos; int size = selectInfos.size(); if (size == 0) { - List roots = joinManager.getRoots(); - - if (roots.size() > 1) { - throw new IllegalArgumentException("Empty select not allowed when having multiple roots!"); - } - - roots.get(0).appendAlias(sb, null); + JoinNode rootNode = joinManager.getRootNodeOrFail("Empty select not allowed when having multiple roots!"); + rootNode.appendAlias(sb, null); } else { // we must not replace select alias since we would loose the original expressions queryGenerator.setQueryBuffer(sb); diff --git a/core/impl/src/main/java/com/blazebit/persistence/impl/transform/OuterFunctionVisitor.java b/core/impl/src/main/java/com/blazebit/persistence/impl/transform/OuterFunctionVisitor.java index d754e89c83..6e299a275f 100644 --- a/core/impl/src/main/java/com/blazebit/persistence/impl/transform/OuterFunctionVisitor.java +++ b/core/impl/src/main/java/com/blazebit/persistence/impl/transform/OuterFunctionVisitor.java @@ -52,7 +52,7 @@ public void visit(FunctionExpression expression) { PathExpression path = (PathExpression) expression.getExpressions().get(0); if (joinManager.getParent() != null) { - joinManager.getParent().implicitJoin(path, true, null, fromClause, false, true, joinRequired, false); + joinManager.getParent().implicitJoin(path, true, null, fromClause, false, true, joinRequired, false, null); } } diff --git a/core/parser/src/main/java/com/blazebit/persistence/impl/expression/AbstractExpressionFactory.java b/core/parser/src/main/java/com/blazebit/persistence/impl/expression/AbstractExpressionFactory.java index abcfdb3b19..040bfb218a 100644 --- a/core/parser/src/main/java/com/blazebit/persistence/impl/expression/AbstractExpressionFactory.java +++ b/core/parser/src/main/java/com/blazebit/persistence/impl/expression/AbstractExpressionFactory.java @@ -108,14 +108,13 @@ private Expression createExpression(RuleInvoker ruleInvoker, String expression, @Override public PathExpression createPathExpression(String expression, MacroConfiguration macroConfiguration) { - PathExpression path = (PathExpression) createExpression(new RuleInvoker() { + return (PathExpression) createExpression(new RuleInvoker() { @Override public ParserRuleContext invokeRule(JPQLSelectExpressionParser parser) { return parser.parsePath(); } }, expression, false, false, false, macroConfiguration); - return path; } @Override diff --git a/core/testsuite/src/test/java/com/blazebit/persistence/testsuite/JoinTest.java b/core/testsuite/src/test/java/com/blazebit/persistence/testsuite/JoinTest.java index 6195255ba0..ab451d7e05 100644 --- a/core/testsuite/src/test/java/com/blazebit/persistence/testsuite/JoinTest.java +++ b/core/testsuite/src/test/java/com/blazebit/persistence/testsuite/JoinTest.java @@ -16,8 +16,10 @@ package com.blazebit.persistence.testsuite; +import static com.googlecode.catchexception.CatchException.caughtException; import static com.googlecode.catchexception.CatchException.verifyException; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; import java.util.List; @@ -26,6 +28,11 @@ import com.blazebit.persistence.testsuite.base.category.NoDatanucleus; import com.blazebit.persistence.testsuite.base.category.NoEclipselink; +import com.blazebit.persistence.testsuite.base.category.NoHibernate; +import com.blazebit.persistence.testsuite.base.category.NoHibernate42; +import com.blazebit.persistence.testsuite.base.category.NoHibernate43; +import com.blazebit.persistence.testsuite.base.category.NoHibernate50; +import com.blazebit.persistence.testsuite.base.category.NoHibernate51; import com.blazebit.persistence.testsuite.base.category.NoOpenJPA; import com.blazebit.persistence.testsuite.entity.DocumentForEntityKeyMaps; import com.blazebit.persistence.testsuite.entity.PersonForEntityKeyMaps; @@ -314,14 +321,22 @@ public void testCallOrderInvariance() { public void testFetchJoinCheck1() { CriteriaBuilder crit = cbf.create(em, Tuple.class).from(Document.class, "d") .select("name"); - verifyException(crit, IllegalStateException.class).join("d.versions", "versions", JoinType.LEFT, true); + crit.join("d.versions", "versions", JoinType.LEFT, true); + verifyException(crit, IllegalStateException.class).getQueryString(); + String message = caughtException().getMessage(); + assertTrue(message.contains("fetch parent")); + assertTrue(message.contains("'d'")); } @Test public void testFetchJoinCheck2() { CriteriaBuilder crit = cbf.create(em, Tuple.class).from(Document.class, "d") .select("name"); - verifyException(crit, IllegalStateException.class).fetch("d.versions"); + crit.fetch("d.versions"); + verifyException(crit, IllegalStateException.class).getQueryString(); + String message = caughtException().getMessage(); + assertTrue(message.contains("fetch parent")); + assertTrue(message.contains("'d'")); } @Test @@ -350,6 +365,57 @@ public void testFetchAmbiguousImplicitAlias() { assertEquals("SELECT a FROM Document a JOIN FETCH a.owner owner_1 LEFT JOIN FETCH owner_1.partnerDocument partnerDocument_1 LEFT JOIN FETCH partnerDocument_1.owner owner_2", crit.getQueryString()); crit.getResultList(); } + + @Test + public void fetchOnlySimpleRelation() { + CriteriaBuilder crit = cbf.create(em, Document.class, "a"); + crit.select("owner"); + crit.fetchOnly("owner", "partnerDocument.owner"); + + assertEquals("SELECT owner_1 FROM Document a JOIN a.owner owner_1 LEFT JOIN FETCH owner_1.partnerDocument partnerDocument_1 LEFT JOIN FETCH partnerDocument_1.owner owner_2", crit.getQueryString()); + crit.getResultList(); + } + + @Test + public void fetchOnlyMultipleRelations() { + CriteriaBuilder crit = cbf.create(em, Document.class, "a"); + crit.select("owner"); + crit.fetchOnly("owner", "partnerDocument.owner", "ownedDocuments"); + + assertEquals("SELECT owner_1 FROM Document a JOIN a.owner owner_1 LEFT JOIN FETCH owner_1.ownedDocuments ownedDocuments_1 LEFT JOIN FETCH owner_1.partnerDocument partnerDocument_1 LEFT JOIN FETCH partnerDocument_1.owner owner_2", crit.getQueryString()); + crit.getResultList(); + } + + @Test + // Older Hibernate versions don't like fetch joining an element collection at all: https://hibernate.atlassian.net/browse/HHH-11140 + @Category({ NoHibernate42.class, NoHibernate43.class, NoHibernate50.class, NoHibernate51.class }) + public void fetchOnlyElementCollectionOnly() { + CriteriaBuilder crit = cbf.create(em, Document.class, "a"); + crit.select("owner"); + crit.fetchOnly("owner", "localized"); + + assertEquals("SELECT owner_1 FROM Document a JOIN a.owner owner_1 LEFT JOIN FETCH owner_1.localized localized_1", crit.getQueryString()); + crit.getResultList(); + } + + @Test + // But fetching the element collection together with other properties is still problematic + @Category({ NoHibernate.class }) + public void fetchOnlyElementCollectionAndOtherRelations() { + CriteriaBuilder crit = cbf.create(em, Document.class, "a"); + crit.select("owner"); + crit.fetchOnly("owner", "partnerDocument.owner", "localized"); + + assertEquals("SELECT owner_1 FROM Document a JOIN a.owner owner_1 LEFT JOIN FETCH owner_1.localized localized_1 LEFT JOIN FETCH owner_1.partnerDocument partnerDocument_1 LEFT JOIN FETCH partnerDocument_1.owner owner_2", crit.getQueryString()); + crit.getResultList(); + } + + @Test + public void fetchOnlyBasesFetchesOnFetchBase() { + CriteriaBuilder crit = cbf.create(em, Document.class, "a"); + crit.select("owner"); + verifyException(crit, IllegalArgumentException.class).fetchOnly("owner", "intIdEntity"); + } @Test public void testImplicitJoinNodeReuse() { diff --git a/documentation/src/main/asciidoc/core/manual/en_US/04_from_clause.adoc b/documentation/src/main/asciidoc/core/manual/en_US/04_from_clause.adoc index c86cac4016..9cad41ffec 100644 --- a/documentation/src/main/asciidoc/core/manual/en_US/04_from_clause.adoc +++ b/documentation/src/main/asciidoc/core/manual/en_US/04_from_clause.adoc @@ -216,6 +216,26 @@ WHERE dad IS NULL WARNING: Although the JPA spec does not specifically allow aliasing fetch joins, every major JPA provider supports this. +When a scalar select instead of a query root select is needed, it might be required to specify the fetch base which can be done by using link:{core_jdoc}/persistence/FullQueryBuilder.html#fetchOnly(java.lang.String,java.lang.String...)[`fetchOnly()`]. + +[source,java] +---- +CriteriaBuilder cb = cbf.create(em, Cat.class) + .from(Cat.class) + .select("father") + .fetchOnly("father", "kittens"); +---- + +In this case we select the `father` relation and fetch the relation `kittens` relative to the `father` relation. + +[source,sql] +---- +SELECT father_1 +FROM Cat cat +LEFT JOIN cat.father father_1 +LEFT JOIN FETCH father_1.kittens kittens_1 +---- + ==== Array joins Array joins are an extension to the JPQL grammar which offer a convenient way to create joins with an `ON` clause condition. diff --git a/jpa-criteria/testsuite/src/test/java/com/blazebit/persistence/criteria/JoinTest.java b/jpa-criteria/testsuite/src/test/java/com/blazebit/persistence/criteria/JoinTest.java index 540a3a2fa4..2c098d9e7b 100644 --- a/jpa-criteria/testsuite/src/test/java/com/blazebit/persistence/criteria/JoinTest.java +++ b/jpa-criteria/testsuite/src/test/java/com/blazebit/persistence/criteria/JoinTest.java @@ -75,17 +75,17 @@ public void joinSet() { @Test public void fetchSet() { - BlazeCriteriaQuery cq = BlazeCriteria.get(em, cbf, Long.class); + BlazeCriteriaQuery cq = BlazeCriteria.get(em, cbf, Document.class); BlazeCriteriaBuilder cb = cq.getCriteriaBuilder(); BlazeRoot root = cq.from(Document.class, "document"); BlazeJoin partners = root.fetch(Document_.partners, "partner"); BlazeJoin ownerDocuments = partners.join(Person_.ownedDocuments, "doc"); ownerDocuments.fetch(); - cq.select(root.get(Document_.id)); + cq.select(root); CriteriaBuilder criteriaBuilder = cq.createCriteriaBuilder(); - assertEquals("SELECT document.id FROM Document document JOIN FETCH document.partners partner JOIN FETCH partner.ownedDocuments doc", criteriaBuilder.getQueryString()); + assertEquals("SELECT document FROM Document document JOIN FETCH document.partners partner JOIN FETCH partner.ownedDocuments doc", criteriaBuilder.getQueryString()); } @Test